이전 이야기
https://record47584.tistory.com/88
Spring 프로젝트 되살리기 3 - 코드 리펙토링 1 (중첩 if문 제거 등)
이전 이야기https://record47584.tistory.com/87 Spring 프로젝트 되살리기 2 - ArgumentResolver 이슈이전 이야기https://record47584.tistory.com/86 Spring 프로젝트 되살리기 1 - 빌드 에러 해결시나리오 1누군가가 git에 올
record47584.tistory.com
시나리오 4
간단하게 코드만 변경하는 리팩토링은 끝났다. 이제 좀 더 깊게 리팩토링 해보자!
리팩토링 1 - @valid를 이용한 유효성 검사
@Transactional
public void changePassword(long userId, UserChangePasswordRequest userChangePasswordRequest) {
if (userChangePasswordRequest.getNewPassword().length() < 8 ||
!userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
!userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
}
User user = userRepository.findById(userId)
.orElseThrow(() -> new InvalidRequestException("User not found"));
if (passwordEncoder.matches(userChangePasswordRequest.getNewPassword(), user.getPassword())) {
throw new InvalidRequestException("새 비밀번호는 기존 비밀번호와 같을 수 없습니다.");
}
if (!passwordEncoder.matches(userChangePasswordRequest.getOldPassword(), user.getPassword())) {
throw new InvalidRequestException("잘못된 비밀번호입니다.");
}
user.changePassword(passwordEncoder.encode(userChangePasswordRequest.getNewPassword()));
}
유저 비밀번호를 바꾸는 service 코드 이다. 비밀번호 유효성을 체크하는 코드가 어지럽게 위치하고 있다. 이는 객체지향에도 맞지 않고 가독성도 나쁘다. @Valid를 통해 유효성 검사를 DTO에게 맡기면서 간단한 코드로 변경할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Valid를 쓰기 위해서 위 종속성을 추가하고,
@PutMapping("/users")
public void changePassword(@Auth AuthUser authUser, @Valid @RequestBody UserChangePasswordRequest userChangePasswordRequest) {
userService.changePassword(authUser.getId(), userChangePasswordRequest);
}
위와 같이 Controller의 Request DTO 앞 @RequestBody 앞에 @Valid를 추가해준다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserChangePasswordRequest {
@NotBlank
private String oldPassword;
@NotBlank
// @Pattern을 통한 유효성 검사
@Pattern(regexp="^(?=.*[0-9])(?=.*[A-Z]).{8,}$",
message = "비밀번호는 영문자와 숫자, 특수기호가 적어도 1개 이상 포함된 6자~12자의 비밀번호여야 합니다.")
private String newPassword;
}
위와 같이 @Pattern 안에 알맞는 정규식을 추가하면 DTO에서 비밀번호 유효성을 체크하게 된다.
@Transactional
public void changePassword(long userId, UserChangePasswordRequest userChangePasswordRequest) {
/* 비밀번호 유효성 검사 코드 제거 */
User user = userRepository.findById(userId)
.orElseThrow(() -> new InvalidRequestException("User not found"));
if (passwordEncoder.matches(userChangePasswordRequest.getNewPassword(), user.getPassword())) {
throw new InvalidRequestException("새 비밀번호는 기존 비밀번호와 같을 수 없습니다.");
}
if (!passwordEncoder.matches(userChangePasswordRequest.getOldPassword(), user.getPassword())) {
throw new InvalidRequestException("잘못된 비밀번호입니다.");
}
user.changePassword(passwordEncoder.encode(userChangePasswordRequest.getNewPassword()));
}
위와 같이 service의 changePassword 메서드에서 비밀번호 유효성 검사 코드는 필요 없으니 제거해준다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException ex) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return getErrorResponse(status, ex.getMessage());
}
public ResponseEntity<Map<String, Object>> getErrorResponse(HttpStatus status, String message) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", status.name());
errorResponse.put("code", status.value());
errorResponse.put("message", message);
return new ResponseEntity<>(errorResponse, status);
}
}
@Valid에서 에러가 발생한 경우, MethodArgumentNotValidException가 발생한다. 이 예외를 체크하는 핸들러를 GlobalExceptionHandler에 추가해줘야 한다.
(@RestControllerAdvice와 GlobalExceptionHandler에 대해서는 아래를 참고)
Spring에서의 예외 처리 (+ 커스텀 에러 구현)
Spring에서의 예외 처리자바의 try-catch문을 대신해서, 스프링에서의 핸드링 방식을 정리해 보았다. 1. Spring 기본 예외 처리2. @ExceptionHandler3. @RestControllerAdvice 이렇게 3가지가 존재하고, 동시에 존재
record47584.tistory.com
리팩토링 2 - 아예 커스텀 어노테이션으로 빼기
위처럼 비밀번호 유효성 검사를 @Pattern으로 정의하면, 비밀번호 유효 검사 기준이나 메시지가 바뀌면 비밀번호를 받는 모든 DTO를 수정해야 한다. 아예 비밀번호 유효성 검사를 커스텀 어노테이션으로 만들면, 이 어노테이션만 관리하면 된다.
// 이 어노테이션이 어디에 붙일 수 있는가 (클래스 필드, 파라미터)
@Target({ElementType.FIELD, ElementType.PARAMETER})
// 언제까지 유지 될 것인지 (런타임 내내 유지)
@Retention(RetentionPolicy.RUNTIME)
// 검증 기준 Constraint 선언
@Constraint(validatedBy = PasswordValidator.class)
public @interface ValidPassword {
// 에러 메시지
String message() default "새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.";
// 검증 그룹화를 위한 필드 (잘 쓰지 않음)
Class<?>[] groups() default {};
// 검증 실패 시 부가 메타데이터를 전달하는 필드 (잘 쓰지 않음)
Class<? extends Payload>[] payload() default {};
}
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}
return password.length() >= 8
&& password.matches(".*\\d.*")
&& password.matches(".*[A-Z].*");
}
}
위 코드들을 추가한 뒤,
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserChangePasswordRequest {
@NotBlank
private String oldPassword;
@NotBlank
@ValidPassword // 커스텀 어노테이션
private String newPassword;
}
DTO의 필드에 다시 어노테이션을 달아준다.
결과

유효성 검사에 실패해서 에러를 클라이언트에 전달한다.
'Spring > - [시리즈] Spring 프로젝트 되살리기' 카테고리의 다른 글
| Spring 프로젝트 되살리기 6 - 유닛 테스트 코드 고치기 (0) | 2026.07.25 |
|---|---|
| Spring 프로젝트 되살리기 5 - 코드 리팩토링 3 (@EntityGraph 적용) (0) | 2026.07.25 |
| Spring 프로젝트 되살리기 3 - 코드 리팩토링 1 (중첩 if문 제거 등) (0) | 2026.07.24 |
| Spring 프로젝트 되살리기 2 - ArgumentResolver 이슈 해결 (0) | 2026.07.24 |
| Spring 프로젝트 되살리기 1 - 빌드 에러 해결 (0) | 2026.07.24 |