✨ 개요
🏃 목표
📢 회원가입을 구현하자.
📢 요구사항
- 모든 회원은 회원 가입을 통해 회원이 된다.
- 회원 가입 성공 시
"회원가입 성공"
을 리턴한다.
userName
이 존재할 시 예외처리를 한다.
- POST
/join
- 입력 폼 (JSON 형식)
{
"userName" : "user1",
"password" : "user1234"
}
- 리턴 (JSON 형식)
{
"resultCode": "SUCCESS",
"result": {
"userId": 5,
"userName": "test1"
}
}
📜 접근방법
✅ TO-DO
🔧 구현
회원가입 컨트롤러 구현
UserController
<@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/join")
public ResponseEntity<Response> join(@RequestBody UserJoinRequest userJoinRequest) {
UserJoinResponse userJoinResponse = userService.join(userJoinRequest.getUserName(), userJoinRequest.getPassword());
return ResponseEntity.ok().body(Response.of("SUCCESS", userJoinResponse));
}
}
DTO
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class UserJoinRequest {
private String userName;
private String password;
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class UserJoinResponse {
private Long userId;
private String userName;
public static UserJoinResponse of(Long userId, String userName) {
return UserJoinResponse.builder()
.userId(userId)
.userName(userName)
.build();
}
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class Response<T> {
private String resultCode;
private T result;
public static <T> Response of(String resultCode, T result) {
return Response.builder()
.resultCode(resultCode)
.result(result)
.build();
}
}
회원가입 컨트롤러 테스트
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test:5.7.3'
UserController
@WebMvcTest(UserController.class)
@MockBean(JpaMetamodelMappingContext.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("회원가입 성공")
void join_SUCCESS() throws Exception {
String userName = "홍길동";
String password = "0000";
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new UserJoinRequest(userName, password))))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("회원가입 실패_userName이 중복인 경우")
void join_FAILED() throws Exception {
String userName = "홍길동";
String password = "0000";
when(userService.join(any(),any()))
.thenThrow(new AppException(ErrorCode.DUPLICATED_USER_NAME,ErrorCode.DUPLICATED_USER_NAME.getMessage()));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new UserJoinRequest(userName, password))))
.andDo(print())
.andExpect(status().isConflict());
}
}
UserRepository 구현
엔티티
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String userName;
private String password;
public static User of(String userName, String password) {
return User.builder()
.userName(userName)
.password(password)
.build();
}
}
UserRepository
public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByUserName(String userName);
}
회원가입 서비스 구현
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder encoder;
public UserJoinResponse join(String userName, String password) {
userRepository.findByUserName(userName).ifPresent(user -> {
throw new AppException(ErrorCode.DUPLICATED_USER_NAME, ErrorCode.DUPLICATED_USER_NAME.getMessage());
});
User savedUser = User.of(userName, encoder.encode(password));
savedUser = userRepository.save(savedUser);
UserJoinResponse userJoinResponse = UserJoinResponse.of(savedUser.getUserId(), savedUser.getUserName());
return userJoinResponse;
}
}
예외 처리
AppException
@AllArgsConstructor
@Getter
public class AppException extends RuntimeException {
private ErrorCode errorCode;
private String message;
}
ErrorCode
@AllArgsConstructor
@Getter
public enum ErrorCode {
DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "UserName이 중복됩니다."),
USERNAME_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 UserName입니다."),
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "패스워드가 다릅니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 토큰입니다."),
INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "사용자가 권한이 없습니다."),
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 포스트가 없습니다."),
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "DB에러");
private HttpStatus httpStatus;
private String message;
}
ExceptionManager
@RestControllerAdvice
public class ExceptionManager {
@ExceptionHandler(AppException.class)
public ResponseEntity<Response> appExceptionHandler(AppException e) {
ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode().name(), e.getMessage());
Response response = Response.of("ERROR", errorResponse);
return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response);
}
}
ErrorResponse
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class ErrorResponse {
private String ErrorCode;
private String message;
public static ErrorResponse of(String errorCode, String message) {
return ErrorResponse.builder()
.ErrorCode(errorCode)
.message(message)
.build();
}
}
패스워드 인코딩 처리
SecurityConfig
@Configuration
public class SecurityConfig {
private String[] PERMIT_URL = {
"/api/v1/join"
};
private String[] SWAGGER = {
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/v3/api-docs/**",
"/swagger-ui/**"
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers(SWAGGER).permitAll()
.antMatchers(PERMIT_URL).permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
}
EncoderConfig
@Configuration
public class EncoderConfig {
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
🌉 회고
- UserConroller의 테스를 하는 방법에 대해 복습을 해보면서 복기를 했다.
- SpringSecurity 의존성을 추가하면 Test에서 api에 접근하는 것이 막힌다는 사실을 다시 인지하게 되었고, Spring Security Test 의존성을 추가하여 해결해야 한다는 사실을 복습했다.
- 아직 Security에 대한 개념이 부족한 것 같아 이 주제에 대한 자세한 공부가 필요할 것 같다.