회원 가입 시 아이디, 닉네임, 이메일 중복을 확인한 후 없으면 회원가입을 할 수 있게 만들었었다.
@Service
@RequiredArgsConstructor
public class UsersService {
@Transactional
public UserJoinRequestDto register(UserJoinRequestDto userJoinRequestDto){
Optional<Users> usersId = usersRepository.findByLoginId(userJoinRequestDto.getLoginId());
Optional<Users> usersNickname = usersRepository.findByNickname(userJoinRequestDto.getNickname());
Optional<Users> usersEmail = usersRepository.findByEmail(userJoinRequestDto.getEmail());
// id, nickname, email 중복
if(usersId.isPresent() || usersNickname.isPresent() || usersEmail.isPresent()){
return null;
}
// 회원가입
userJoinRequestDto.setLoginPassword(passwordEncoder.encode(userJoinRequestDto.getLoginPassword()));
usersRepository.save(userJoinRequestDto.toEntity());
return userJoinRequestDto;
}
이 코드를 보면 메모리 낭비가 있고, 재사용성은 볼 수도 없다.
그래서 RegisterValidator를 만들려고 한다.
@Test
public void 이메일_중복_확인_테스트() {
// Given
UserJoinRequestDto userJoinRequestDto = UserJoinRequestDto.builder()
.email("test@example.com")
.build();
Mockito.when(usersRepository.existsByEmail("test@example.com")).thenReturn(true);
// When
Errors errors = new BeanPropertyBindingResult(userJoinRequestDto, "userJoinRequestDto");
registerValidator.validate(userJoinRequestDto, errors);
// Then
assertTrue(errors.hasErrors());
assertEquals("이메일 중복", errors.getFieldError("email").getDefaultMessage());
}
이메일 설정후 vaildator를 통해 유효한지 확인하는 테스트 코드이다.
테스트 코드에서 필요한 메서드는 validate(Object target, Errors erros)
@Component
@RequiredArgsConstructor
public class RegisterValidator implements Validator {
private final UsersRepository usersRepository;
/**
* supports() 메서드는 Validator 인터페이스의 메서드로서, 주어진 클래스가 Validator가 유효성 검사를 수행할 수 있는 대상인지 여부를 결정
*
* @param clazz the {@link Class} that this {@link Validator} is
* being asked if it can {@link #validate(Object, Errors) validate}
* @return boolean
*/
@Override
public boolean supports(Class<?> clazz) {
return UserJoinRequestDto.class.equals(clazz);
}
/**
* 중복 체크
*
* @param target the object that is to be validated
* @param errors contextual state about the validation process
*/
@Override
public void validate(Object target, Errors errors) {
UserJoinRequestDto userJoinRequestDto = (UserJoinRequestDto) target;
// 이메일 중복 체크
if (usersRepository.existsByEmail(userJoinRequestDto.getEmail())) {
errors.rejectValue("email", "400", "이메일 중복");
}
// 아이디 중복 체크
if (usersRepository.existsByLoginId(userJoinRequestDto.getLoginId())) {
errors.rejectValue("loginId", "400", "아이디 중복");
}
// 닉네임 중복 체크
if (usersRepository.existsByNickname(userJoinRequestDto.getNickname())) {
errors.rejectValue("nickname", "400", "닉네임 중복");
}
}
}
Validator interface를 이용하여 RegisterValidator클래스를 만들었다.
validate 메서드는 repository에서 email, loginId, nickname이 존재하는지 확인한 후 존재한다면 error를 binding한다.
errors에서 rejectValue와 reject 메서드 둘 중 rejectValue를 사용하였는데 가시성과 유지보수 측면에서 rejectValue가 우수하다.
org.springframework.validation.Errors
reject : 전체 객체에 대한 유효성 검사 오류를 처리
rejectValue : 특정 필드에 대한 유효성 검사 오류를 처리
테스트 코드 성공
validator를 만들었으니 이제 활용할 차례이다.
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/users")
public class UsersApiController {
private final UsersService usersService;
// 회원가입
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserJoinRequestDto userJoinRequestDto) {
// 중복된 아이디 또는 닉네임 또는 이메일이 있을 때
if(usersService.register(userJoinRequestDto) == null){
return new ResponseEntity<>(new ExceptionDto(ErrorMessage.DUPLICATE_ID_OR_NICKNAME_OR_EMAIL),
HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(new ResponseDto(SuccessMessage.SUCCESS_REGISTER), HttpStatus.OK);
}
validator가 적용되기 전 코드이다. 3가지를 한 번에 검사하여 응답메시지를 보낸다. 이렇게 될 시 어떤 곳에서 중복이 일어났는지 알 수 없다.
우선 중복확인은 service Layer에서 확인하려고 한다. 이유는 비즈니스 로직을 처리하는 주요 layer이기 때문이다.
@Service
@RequiredArgsConstructor
public class UsersService {
private final UsersRepository usersRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
private final RegisterValidator registerValidator;
@Transactional
public void register(UserJoinRequestDto userJoinRequestDto, BindingResult bindingResult) {
checkUserValidate(userJoinRequestDto, bindingResult);
userJoinRequestDto.setLoginPassword(passwordEncoder.encode(userJoinRequestDto.getLoginPassword()));
usersRepository.save(userJoinRequestDto.toEntity());
}
...
/**
* 회원가입 유효성 검사
*
* @param bindingResult
*/
public void checkUserValidate(UserJoinRequestDto userJoinRequestDto, BindingResult bindingResult) {
registerValidator.validate(userJoinRequestDto,bindingResult);
if (bindingResult.hasErrors()) {
List<String> errorList =
bindingResult.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
throw new NotValidException(errorList);
}
}
}
여기에서 Errors 대신 BindingResult를 사용한다.
Spring MVC에서 폼 데이터를 검증할 때 사용되는 BindingResult와 Errors는 둘 다 같은 역할을 한다. 그러나 BindingResult는 Errors의 하위 클래스이기 때문에 BindingResult를 사용하는 것이 더 좋다.
BindingResult는 폼 데이터의 유효성 검사 결과를 개별 필드별로 확인할 수 있다. Errors는 폼 데이터의 유효성 검사 결과를 전체 객체로만 확인할 수 있다. 따라서 BindingResult를 사용하는 것이 더 효율적이다.
@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/users")
public class UsersApiController {
private final UsersService usersService;
// 회원가입
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody UserJoinRequestDto userJoinRequestDto, BindingResult bindingResult) {
usersService.register(userJoinRequestDto, bindingResult);
return new ResponseEntity<>(new ResponseDto(SuccessMessage.SUCCESS_REGISTER), HttpStatus.OK);
}
}
비즈니스 로직을 서비스 레이어에 옮겨 보기 좋아진 거 같다.
이제 Validator를 만들었으니 조건에 일치하지 않는 값이 들어오면 예외처리를 만들어야 한다.
@Data
@NoArgsConstructor
@SuperBuilder
public class ExceptionDto {
private int stateCode;
private String message;
public ExceptionDto(ErrorMessage errorMessage) {
this.stateCode = errorMessage.getCode();
this.message = errorMessage.getMessage();
}
}
상태 코드와 메시지 dto
@Getter
@SuperBuilder
public class NotValidExceptionResponseDto extends ExceptionDto {
private final List<String> errors;
}
ExceptionDto를 상속받은 클래스
validator에서 Exception 발생시 어떤 에러인지 알려주는 dto
@ResponseStatus(HttpStatus.BAD_REQUEST)
@Getter
public class NotValidException extends RuntimeException {
private final List<String> errorList;
public NotValidException(List<String> errList) {
this.errorList = errList;
}
}
예외가 발생하면 errors를 담는 클래스
@RestController
@ControllerAdvice // 사전 컨트롤러
public class CustomizedGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(NotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<NotValidExceptionResponseDto> handleCustomizedNotValidException(NotValidException ex) {
return new ResponseEntity<>(
NotValidExceptionResponseDto.builder()
.stateCode(HttpStatus.BAD_REQUEST.value())
.message("아이디 또는 이메일 또는 닉네임 중복")
.errors(ex.getErrorList())
.build(), HttpStatus.BAD_REQUEST
);
}
}
NotValidException.class에서 발생하는 예외를 처리하는 핸들러
지금 보니깐 하드코딩이 있는데 조만간 다 정리해주지..ㅎㅎ




이로써 중복 검사 리펙토링이 끝이 났다고 생각을 했는데 아직 좀 더 있다...
그래도 어느정도 진행완료
글을 쓰면서 느끼는건데 설명을 잘 못하는 거 같다. 다음 포스팅에는 기록이 아닌 공유라는 느낌으로 작성해 보겠다.