Skip to content

Commit

Permalink
예측 가능한 코드를 만들기 위해 메소드 파라미터에 final 키워드를 붙입니다
Browse files Browse the repository at this point in the history
  • Loading branch information
wisdom08 committed Jul 31, 2023
2 parents 91794d5 + 92a6d1a commit 192340c
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 0 deletions.
34 changes: 34 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '17'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mindrot:jbcrypt:0.4'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.1'
}

tasks.named('test') {
useJUnitPlatform()
}
29 changes: 29 additions & 0 deletions src/main/java/com/bookbook/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.bookbook.controller;


import com.bookbook.dto.SignUpRequest;
import com.bookbook.service.UserService;
import com.bookbook.util.response.CommonResponse;
import com.bookbook.util.response.ResponseUtil;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/v1/users")
@RestController
public class UserController {

private final UserService userService;

public UserController(final UserService userService) {
this.userService = userService;
}

@PostMapping
public CommonResponse<Long> signUp(@RequestBody @Valid final SignUpRequest signUpRequest) {
userService.signUp(signUpRequest);
return ResponseUtil.success(200, signUpRequest.getId());
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/bookbook/dto/SignUpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bookbook.dto;

import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;

@Getter
public class SignUpRequest {
private Long id;
@NotEmpty
private String userId;
@NotEmpty
private String password;

private String introduce = "";

private SignUpRequest() {}

public SignUpRequest(final String userId, final String password, final String introduce) {
this.userId = userId;
this.password = password;
this.introduce = introduce;
}

public SignUpRequest(final String userId, final String password) {
this.userId = userId;
this.password = password;
}

public void updateHashedPassword(final String hashedPassword) {
this.password = hashedPassword;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/bookbook/dto/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bookbook.dto;

public enum UserRole {
ADMIN,
USER
}
11 changes: 11 additions & 0 deletions src/main/java/com/bookbook/exception/UserIdExistsException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.bookbook.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.CONFLICT)
public class UserIdExistsException extends RuntimeException {
public UserIdExistsException(final String message) {
super(message);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/bookbook/mapper/UserMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.bookbook.mapper;

import com.bookbook.dto.SignUpRequest;
import org.apache.ibatis.annotations.Mapper;

/*
* @Mapper 정리
* - MyBatis의 mappers를 위한 marker interface로 사용한다. -> 특정 매퍼 등록을 위한 어노테이션
* - 여러 개의 매퍼를 사용하기 위해선 @MapperScan 어노테이션을 사용하고, @Mapper 는 생략할 수도 있다.
* - 매퍼 스캔은 basePackages 속성에 지정된 패키지 아래의 모든 인터페이스가 매퍼로 추가된다.
* */
@Mapper
public interface UserMapper {
void insertUser(SignUpRequest user);
boolean selectUserId(String userId);
}
34 changes: 34 additions & 0 deletions src/main/java/com/bookbook/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.bookbook.service;

import com.bookbook.dto.SignUpRequest;
import com.bookbook.exception.UserIdExistsException;
import com.bookbook.mapper.UserMapper;
import com.bookbook.util.password.Encoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {
private final UserMapper userMapper;
private final Encoder passwordEncoder;

public UserService(final UserMapper userMapper, final Encoder passwordEncoder) {
this.userMapper = userMapper;
this.passwordEncoder = passwordEncoder;
}

public void signUp(final SignUpRequest signUpRequest) {
if (checkUniqueUserId(signUpRequest.getUserId())) {
throw new UserIdExistsException("이미 가입된 아이디입니다. " + signUpRequest.getUserId());
}
signUpRequest.updateHashedPassword(encryptPassword(signUpRequest.getPassword()));
userMapper.insertUser(signUpRequest);
}

public String encryptPassword(final String password) {
return passwordEncoder.hashPassword(password);
}

private boolean checkUniqueUserId(final String userId) {
return userMapper.selectUserId(userId);
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/bookbook/util/ResponseEntityConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bookbook.util;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public class ResponseEntityConstants {
public static final ResponseEntity<Void> RESPONSE_OK = new ResponseEntity(HttpStatus.OK);
}
17 changes: 17 additions & 0 deletions src/main/java/com/bookbook/util/password/BcryptEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bookbook.util.password;

import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Component;

@Component
public class BcryptEncoder implements Encoder {
@Override
public String hashPassword(final String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt());
}

@Override
public boolean isMatched(final String rawPassword, final String hashedPassword) {
return BCrypt.checkpw(rawPassword, hashedPassword);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/bookbook/util/password/Encoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bookbook.util.password;

public interface Encoder {
String hashPassword(final String rawPassword);

boolean isMatched(final String rawPassword, final String hashedPassword);
}
5 changes: 5 additions & 0 deletions src/main/java/com/bookbook/util/response/CommonResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.bookbook.util.response;

import com.fasterxml.jackson.annotation.JsonInclude;

public record CommonResponse<T>(int code, boolean success, @JsonInclude(JsonInclude.Include.NON_NULL) T result) {}
10 changes: 10 additions & 0 deletions src/main/java/com/bookbook/util/response/ResponseUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bookbook.util.response;

public class ResponseUtil {

private ResponseUtil() {}

public static <T> CommonResponse<T> success(final int code, final T result) {
return new CommonResponse<>(code, true, result);
}
}
18 changes: 18 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.bookbook.dto

debug: false
management.endpoints.web.exposure.include: "*"

logging:
level:
com.com.bookbook: debug
org.springframework.web.servlet: debug

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:https://localhost:3306/bookbook
username: wisdom
password: asdfasdf33
15 changes: 15 additions & 0 deletions src/main/resources/mapper/userMapper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.bookbook.mapper.UserMapper">
<insert id="insertUser" parameterType="SignUpRequest" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO user (user_id, hashed_password, introduce)
VALUES (#{userId}, #{password}, #{introduce})
</insert>

<select id="selectUserId" parameterType="java.lang.String" resultType="boolean">
SELECT EXISTS (SELECT 1 FROM user WHERE user_id = #{userId});
</select>
</mapper>
84 changes: 84 additions & 0 deletions src/test/java/com/bookbook/service/UserServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.bookbook.service;

import com.bookbook.dto.SignUpRequest;
import com.bookbook.exception.UserIdExistsException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/*
* @SpringBootTest 정리
* - 애플리케이션을 테스트하기 위한 많은 기능을 제공한다.
* - Spring Boot Test, JUnit, AssertJ, Hamcrest, Mockito ...
* - ApplicationContext를 쉽게 생성하고 조작할 수 있다.
* - classes 속성을 통해 빈을 생성할 클래스들을 지정할 수 있고, 지정하지 않으면 애플리케이션 상에 정의된 모든 빈을 생성한다.
* - @Import 어노테이션을 사용해서 별도로 TestConfiguration을 명시하고 가져다 쓸 수 있다.
* - @Transactional 어노테이션과 같이 사용하면 rollback 된다. spring-boot-test는 그저 spring-test를 확장한 것이기 때문
* - 다만 RANDOM_PORT나 DEFINED_PORT로 테스트를 설정하면 실제 테스트 서버는 별도의 스레드에서 수행되기 때문에 rollback되지 않는다.
*
* 언제 사용하나?
* - 일반적으로 통합테스트 시 사용한다.
* - business~data layer 와 같이 계층에 걸쳐 테스트할 때 사용하기 좋다.
* - 참고로 유닛테스트의 경우에는 @WebMvcTest, @DataJpaTest, @MyBatisTest 등을 사용한다
*
* 장점
* - 애플리케이션의 컨텍스트를 전부 셋업하기에 편리하다
*
* 단점
* - 기본적으로 모든 빈을 탐색하고 등록하기 때문에 특정 계층만 테스트할 목적으로 사용하기에는 무겁고 시간이 오래 걸린다.
* */
@Sql("classpath:init.sql")
@SpringBootTest
class UserServiceTest {

@Autowired
private UserService userService;

SignUpRequest user;
SignUpRequest userWithoutIntroduce;

@BeforeEach
void setUpUser() {
user = new SignUpRequest(
"wisdom",
"pwpwpwpw123",
"introduce"
);

userWithoutIntroduce = new SignUpRequest(
"jihye",
"pasdlfkjaslkdf123"
);
}

@DisplayName("선택 입력값인 introduce 값이 없어도 유저가 정상적으로 회원가입에 성공합니다")
@Test
void signUpWithoutIntroduce() {
userService.signUp(userWithoutIntroduce);
assertThat(user.getUserId()).isNotNull();
}

@DisplayName("선택 입력값인 introduce 값이 있는 상황에서 유저가 정상적으로 회원가입에 성공합니다")
@Test
void signUpWithIntroduce() {
userService.signUp(user);
assertThat(user.getUserId()).isNotNull();
}

@DisplayName("이미 가입된 아이디 입력으로 회원가입에 실패합니다")
@Test
void signUpWithDuplicatedId() {
userService.signUp(user);
assertThatThrownBy(() -> userService.signUp(user))
.isInstanceOf(UserIdExistsException.class)
.hasMessageContaining("이미 가입된 아이디입니다.")
.hasMessageContaining(user.getUserId())
;
}
}
39 changes: 39 additions & 0 deletions src/test/java/com/bookbook/util/password/BcryptEncoderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.bookbook.util.password;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mindrot.jbcrypt.BCrypt;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BcryptEncoderTest {

private final BcryptEncoder encoder = new BcryptEncoder();

@DisplayName("BCrypt 에 의해 비밀번호가 정상적으로 해싱 처리된다.")
@Test
void passwordIsHashedByBCrypt() {
String password = "test-password";
String hashed = encoder.hashPassword(password);

assertThat(hashed).hasSize(60);
assertTrue(hashed.startsWith("$2a$10$"));
}

@DisplayName("패스워드 입력값이 같으면 해싱 결과값이 동일하다.")
@Test
void isMatched() {
String password = "test-password";
assertTrue(encoder.isMatched(password, BCrypt.hashpw(password, BCrypt.gensalt())));
}

@DisplayName("패스워드 입력값이 다르면 해싱 결과값이 동일하지 않다.")
@Test
void isNotMatchedWithDifferentValue() {
String one = "one-password";
String another = "another-password";
assertFalse(encoder.isMatched(another, BCrypt.hashpw(one, BCrypt.gensalt())));
}
}
2 changes: 2 additions & 0 deletions src/test/resources/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DELETE FROM bookbook.user;
ALTER TABLE bookbook.user AUTO_INCREMENT = 1;

0 comments on commit 192340c

Please sign in to comment.