개요
지금까지 사용자의 아이디, 비밀번호를 검증하는 코드를 Service 레이어에서 조건문을 이용해 작성했었다. 부트캠프 멘토님의 테스트 코드를 분석하면서 @Pattern과 @Valid를 이용해 입력값을 검증할 수 있다는 사실을 알게되었고 Service 레이어의 코드를 줄이고 예외처리의 가독성이 높아지는 것에 매력을 느껴 해당 방식을 사용하기로 결정하였다. 해당 방식의 사용 방법을 알아보자.
DTO에서 @Pattern 사용
@Pattern은 DTO레벨에 정규표현식과 함께 선언해주면 되는데 이를 이용하기 위해서는 아래와 같이 dependency를 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
다양한 방면에서 클라이언의 입력값을 검증하겠지만, 이번 글에서는 아이디, 비밀번호를 기준으로 유효성을 검증한다. 유효한 입력값의 기준과 DTO에서 @Pattern을 적용한 코드는 다음과 같다.
//정규표현식에 대한 깊이가 얕아 올바르지 않을 수 있습니다.
//아이디(이메일): '@'와 '.' 이 한개씩 입력되어야하고 문자와 숫자를 이용해 입력해야한다.
@Pattern(regexp = "\\w+@\\w+\\.\\w+(\\.\\w+)?")
private String email;
//비밀번호: 영문 대/소문자와 숫자, 특수문자를 1개 이상 포함한 8-15자의 비밀번호를 입력해야한다.
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[~@#$%^&+=!])(?=\\S+$).{8,15}$")
private String password;
Controller에서 @Valid 사용
DTO에 @Pattern을 적용했으면 Controller 레이어에서 @Valid를 추가하여 검증을 진행하면 된다. @Valid를 아래와 같이 DTO에 선언해주면 DTO의 @Pattern이 적용된 입력값을 검증하고 정규표현식과 일치하지 않는다면 MethodArgumentNotValidException이 발생한다.
@PostMapping("/register")
public void register(@Valid @RequestBody UserRegisterRequest request) {
//@Valid를 이용해 request의 @Pattern을 검사한다.
//검사 결과 @Pattern이 걸려있는 email과 password의 값이 유효하지 않으면 MethodArgumentNotValidException이 발생한다.
//MethodArgumentNotValidException이 발생하면 CustomExceptionHandler를 이용해 클라이언트에 예외를 반환한다.
userService.register(request);
}
유효하지 않은 값을 입력해 테스트를 해보면 클라이언트로는 403을 반환하고 서버 로그에는 MethodArgumentNotValidException이 발생했다는 로그를 찍는다. 클라이언트에게 해당 에러를 잘 전달해주고자 @ExceptionHandler를 이용해 위 예외가 발생하면 정해진 응답을 반환하도록 하였다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CustomExceptionResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return ResponseEntity.status(e.getStatusCode()).body(
new CustomExceptionResponse(
CustomErrorCode.INVALID_INPUT_VALUE,
CustomErrorCode.INVALID_INPUT_VALUE.getStatusMessage()
));
}
물론 아래와 같은 기존 코드와 비교하면 DTO와 Controller에 어노테이션을 넣어주고 예외처리 코드를 추가해서 전체적으로 코드가 더 많아지긴 했으나, Service레이어의 코드를 줄이고 기존 register()가 담당했던 입력값 검증의 책임을 다른 곳으로 분리했다는 것에 만족감을 느끼고 있다.
public User register(UserRegisterRequest request) {
String email = request.getEmail();
String password = request.getPassword();
//email & password 유효성 검사
if (!email.matches("\\w+@\\w+\\.\\w+(\\.\\w+)?"))
throw new CustomException(CustomErrorCode.EMAIL_INVALID_EXCEPTION, 400);
if (!password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[~@#$%^&+=!])(?=\\S+$).{8,15}$"))
throw new CustomException(CustomErrorCode.PASSWORD_INVALID_EXCEPTION, 400);
//...
}