[Spring Boot] 회원가입 시 사용자가 입력한 값 검사하기

hellonayeon·2021년 12월 16일
0
post-thumbnail

프론트엔드와 백엔드 개발을 같이하며 궁금했던 점이 있다. 여태까지 프론트에서 사용자가 입력한 값을 검사해서 유효하지 않으면 요청을 보내지 못하도록 했었다. 근데 입력값 검사는 원래 프론트에서 하는건가? 프론트에서 해줬으니 백엔드에서는 안해줘도 되는걸까? 사용자로부터 입력을 받을때마다 하는 고민이었다. 하지만 이는 너무 단편적인 생각이었고, 정답은 프론트엔드와 백엔드 모두 사용자 입력값 검사 프론트엔드에서는 사용자에게 알려주기위한 UX 적인 측면으로 입력값 검사를 하기 때문에 백엔드에서 유효성 검사를 해줘야만 한다. 나는 테스트할때 브라우저에서 <input>에 값을 넣어가며 테스트하지만, 생각해보면 Postman 과 같은 API 플랫폼에서 요청을 날릴때는 자바스크립트에 작성해놓은 입력값 검사는 무용지물이다.

의존성 추가

/* build.gradle */

dependencies {
    ... 
    implementation 'org.springframework.boot:spring-boot-starter-validation:2.5.6'

}

막상 추가를 해놓고 보니 실제로 Java 파일에서 import 할때는 import javax.validation.constraints.*; 가 추가됐다. spring 이름의 패키지를 사용할 줄 알았는데.. 그래서 찾아봤으나 이해된 내용은 없었다😗

javax validation bean validation spring boot starter validation 관계를 정리할 수 있는 날이 오길 바라며 찾아봐야지🙏

DTO 수정

검증할 멤버변수에 어노테이션을 추가해준다.

@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
public class SignupRequestDto {
    @NotBlank(message = "아이디는 필수 입력값입니다.")
    @Pattern(regexp = "[a-zA-Z0-9]{2,9}",
             message = "아이디는 영문, 숫자만 가능하며 2 ~ 10자리까지 가능합니다.")
    private String username;

    @NotBlank(message = "비밀번호는 필수 입력값입니다.")
    @Pattern(regexp = "^(?=.*\\d)(?=.*[a-zA-Z])[0-9a-zA-Z]{8,16}",
             message = "비밀번호는 영문과 숫자 조합으로 8 ~ 16자리까지 가능합니다.")
    private String password;

    @NotBlank(message = "이메일은 필수 입력값입니다.")
    @Pattern(regexp = "^[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}",
             message = "올바르지 않은 이메일 형식입니다.")
    private String email;

    public SignupRequestDto(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
}

예외처리

유효성 검사를 통과하지 못하면 MethodArgumentNotValidException이 발생하고 디폴트로 400 오류를 리턴한다. 서버에서 생성한 오류 메시지를 전달하기 위해서는 예외처리 핸들러로 처리해줘야한다.

@RestControllerAdvice
public class ApiExceptionHandler {

    ...
    
    /* 유효성 검사 예외처리 핸들러 */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();

        StringBuilder builder = new StringBuilder();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            builder.append(fieldError.getDefaultMessage());
            builder.append("\n");
        }

        ApiException apiException = new ApiException(
                builder.toString(),
                HttpStatus.BAD_REQUEST
        );

        return new ResponseEntity<>(
                apiException,
                HttpStatus.BAD_REQUEST
        );
    }

}

컨트롤러 수정

요청 메시지에 담긴 데이터를 검증하기 위해 파라미터 앞에 @Valid 어노테이션을 붙여준다.

@PostMapping(value = "/signup")
public User createUser(@Valid @RequestBody SignupRequestDto signupRequestDto) {
    return userService.createUser(signupRequestDto);
}

문제점

이렇게만 설정할 경우 아래와 같이 오류 메시지가 응답으로 내려간다.

뭔가.. 기대했던 메시지가 아닌데..? 적어도 이메일은 필수 입력값입니다. 올바르지 않은 이메일 형식입니다. ... 처럼 하나의 필드에 대한 검사가 순서대로 출력되길 바랐으나 완전 중구난방😱 사실 @NotBlank @Pattern 중 하나만이라도 걸리면 유효성 검사는 통과를 못한 것이기 때문에 모든 유효성을 검사할 필요는 없다. 이런 처리를 할 수 있도록 도와주는 것이 ValidationGroups ValidationSequence 이다.

ValidationGroups 클래스 추가

public class ValidationGroups {
    public interface NotEmptyGroup {};
    public interface PatternCheckGroup {};
}

ValidationSequence 인터페이스 추가

Default ➡️ NotEmptyGroup ➡️ PatternCheckGroup 순으로 검사를 진행한다는 의미이다!

@GroupSequence({Default.class, NotEmptyGroup.class, PatternCheckGroup.class })
public interface ValidationSequence {
}

DTO 수정

각 필드의 유효성 검사 어노테이션에 어떤 ValidationGroups에 해당되는지 groups 속성을 추가한다.

@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
public class SignupRequestDto {
    @NotBlank(message = "아이디는 필수 입력값입니다.", groups = ValidationGroups.NotEmptyGroup.class)
    @Pattern(regexp = "[a-zA-Z0-9]{2,9}",
             message = "아이디는 영문, 숫자만 가능하며 2 ~ 10자리까지 가능합니다.", groups = ValidationGroups.PatternCheckGroup.class)
    private String username;

    @NotBlank(message = "비밀번호는 필수 입력값입니다.", groups = ValidationGroups.NotEmptyGroup.class)
    @Pattern(regexp = "^(?=.*\\d)(?=.*[a-zA-Z])[0-9a-zA-Z]{8,16}",
             message = "비밀번호는 영문과 숫자 조합으로 8 ~ 16자리까지 가능합니다.", groups = ValidationGroups.PatternCheckGroup.class)
    private String password;

    @NotBlank(message = "이메일은 필수 입력값입니다.", groups = ValidationGroups.NotEmptyGroup.class)
    @Pattern(regexp = "^[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}",
             message = "올바르지 않은 이메일 형식입니다.", groups = ValidationGroups.PatternCheckGroup.class)
    private String email;

    public SignupRequestDto(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
}

결과확인

NotEmptyGroup에 속하는 유효성 검사의 메시지만 출력되는 것을 확인할 수 있다.

생각정리

Spring을 이용하면 무엇이든 뚝딱뚝딱 알아서 해줘서 편한데 왜 되는지 알기 어려워서 답답하다. 오늘은 유독 코드를 작성하면서도 답답함을 많이 느꼈고 글로 정리하면서도 정확한 용어를 사용하는게 맞나 싶어서 혼란스러웠다. Validation을 이해하려면 빡집중 모드로 전환이 필요해보인다😡

참고문서

📌 소프. "Spring Boot Validation 순서 정하기 & 테스트 코드", 기회는 찬스, 15 May 2021

0개의 댓글