[Spring] 회원가입 폼 검증하기 − ① (BeanValidation)

dondonee·2024년 5월 21일
0
post-thumbnail

회원가입 폼 검증하기 (BeanValidation)

OKKY를 참고한 방송대 커뮤니티 게시판을 만들어보고 있다. 이번에는 회원가입 폼 데이터를 검증해보았다.



사용자 입력 검증하기

사용자가 폼 데이터를 입력하고 POST 요청을 보내면 컨트롤러에서 그 데이터를 받아 검사해야 한다. 예를 들어 사용자의 실명을 받는 name 값이 null은 아닌지, 숫자나 특수문자 데이터가 들어오지는 않았는지 검사할 필요가 있다.

기존에는 if 조건문을 통해 검증을 한 뒤, 문제가 있다면 BindingResultrejectValue() 또는 reject()를 통해 오류 내역을 추가했다. 이것은 상당히 반복되는 작업이기 때문에 이것을 상당 부분 자동화해주는 Java Bean Validation이라는 기술이 등장했다.


Java Bean Validation

BeanValidation은 구현체가 아니라 Bean Validation 2.0(JSR-380) 이라는 기술 표준이다. JPA는 표준 기술이고 Hibernate가 구현체인 것처럼 BeanValidation도 구현체를 바꿔 낄 수 있는데 일반적으로 사용되는 구현체는 Hibernate Validator이다. (이름에 하이버네이트가 붙지만 ORM과는 관련이 없다.)


implementation 'org.springframework.boot:spring-boot-starter-validation'

build.gradle에 BeanValidationn 의존성을 추가해준다. 스프링 부트에서 관리되기 때문에 버전을 명시하지 않아도 된다.



폼 DTO 객체 만들기

도메인 객체를 폼 데이터를 받을 때 재사용하지 말고, 폼 데이터를 받는 전용 DTO 객체를 만드는 것이 좋다고 한다. 프론트에서 컨트롤러로 넘어올 때만 사용되는 검증 로직을 분리해두고, 도메인 객체는 순수하게 유지함으로써 유지보수가 용이해지는 것이다.

나는 회원의 프로필 정보를 받는 도메인 객체 Member가 있었지만 회원가입용 폼 객체인 MemberSignUpForm을 별도로 만들었다.


@Data
public class MemberSignUpForm {

    @NotBlank  // null, 공백문자 허용 X (String 타입 필드 검증)
    @Size(min = 4, message = "아이디는 최소 4자 이상 입력하세요.")
    @Size(max = 15, message = "아이디는 15자 이하로 입력하세요.")
    @Pattern(regexp = "^[a-z0-9]*$", message = "아이디는 영문 소문자와 숫자만 가능합니다.")
    private String loginName;

    @NotBlank
    @Size(min = 6, message = "비밀번호는 최소 6자 이상 입력하세요.")
    @Size(max = 25, message = "비밀번호는 25자 이하로 입력하세요.")
    @Pattern(regexp = "^[a-zA-Z0-9!@#$%^&*]*$", message = "비밀번호는 알파벳, 숫자, 특수문자(!@#$%^&*)만 가능합니다.")
    @Pattern(regexp = "^(?=.*[a-zA-Z0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]*$", message = "비밀번호는 알파벳, 숫자, 특수문자(!@#$%^&*)가 각 1개 이상 포함되어야 합니다.")
    private String password;

    @NotBlank  // 비밀번호 확인이 유효하지 않으면 컨트롤러에서 BindingResult에 에러 추가
    private String passwordCheck;

    @NotBlank
    @Size(min = 1, max = 20, message = "닉네임은 20자 이하로 입력하세요.")
    @Pattern(regexp = "^[a-zA-Z가-힣0-9]*$", message = "닉네임은 한글(음절), 알파벳, 숫자만 가능합니다.")
    private String nickname;

    @NotNull(message = "학년을 선택하세요.")  // Enum 필드 유효성 검사는 setter 바인딩 시 수행됨
    private Grade grade;

    @NotNull(message = "지역을 선택하세요.")
    private Region region;

    @NotNull(message = "편입여부를 선택하세요.")
    private Boolean transferred;
}

사용한 검증 애노테이션

  • @NotNull : null을 허용하지 않는다.
  • @NotBlank : @NotNull + 공백문자를 허용하지 않는다. String 타입 검증에 사용된다.
  • @Size : 문자열 길이를 제한한다. @Length는 기능은 동일하지만 Hibernate 전용이다.
  • @Pattern : 문자열 검증시 정규식 패턴 조건을 줄 수 있다.

Boolean 검증

편입여부를 입력하는 필드인 transferredBoolean 타입이다. (방송대에서는 편입이 아주 흔하고, 편입 여부에 따라 공유할 수 있는 주제가 다를 수 있어 필드를 추가했다.)

이 필드에서 true 또는 false 값이 들어오는지 검사를 해야한다고 생각해서 애노테이션을 찾아봤는데, 값이 true인지 검사하는 @AssertTruefalse인지 검사하는 @AssertFalse 애노테이션 밖에 없었다.

결론적으로, Boolean 필드의 검증은 @NotNull 하나면 충분하다. 어차피 Boolean 필드는 true 또는 false 둘 중 하나만 가질 수 있기 때문이다. 스프링은 POST로 들어온 문자열 값을 Boolean 타입에 바인딩할 때 True, true, TRUE, 1 값 모두를 참 값으로 처리해준다. 하지만 만약 aaa와 같이 의미없는 값이 들어온다면 BindingResult 객체에 typeMismatch 필드 오류로 추가한다.


String 패턴 검증 - @Pattern

@Pattern 애노테이션을 사용하면 정규식 패턴 조건을 걸어줄 수 있다. 이번에 정규식을 처음 사용해보았는데 정규식이 생긴 것은 외계어 같지만 차근차근 배워보니 간단한 것은 어렵지 않게 작성할 수 있었다. 도움이 많이 된 두 글의 링크를 아래 첨부한다.


1) 아이디 검증

^[a-z0-9]*$  // 아이디 검증 정규식

아이디는 영어 소문자와 숫자만 허용했다. 위의 정규식은 문자열의 처음(^)과 끝($)의 모든 문자(*)에 앞의 조건([a-z0-9])을 적용한다는 뜻이다. -는 아스키 코드에서 연속적으로 존재하는 코드들을 묶어서 지정하는 기호이다.

정규식에 not null 조건이나 길이 조건도 추가할 수 있지만, 나는 오류 메세지를 세분화하기 위해 @NotBlank, @Size 애노테이션을 통해 따로따로 검증을 했으므로 @Pattern에서는 허용 문자에 대해서만 검증을 했다.


2) 비밀번호 검증

^[a-zA-Z0-9!@#$%^&*]*$  // 비밀번호 허용 문자 검증
^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]*$  // 혼합 문자 검증

비밀번호 검증에는 두 개의 @Pattern을 사용했다. 이것 또한 합쳐서 검증할 수 있지만 오류 메세지를 사용자에게 자세하게 전달하기 위해 분리한 것이다.

첫 번째 정규식은 영어 소문자(a-z), 영어 대문자(A-Z), 숫자(0-9), 특수문자(!@#$%^&*)만을 허용한다. 아스키 코드에서 영어 소문자와 영어 대문자가 연속적으로 위치하지 않고 중간에 특수문자가 존재하기 때문에 A-z와 같은 방식으로 사용하면 안 된다.

두 번째 정규식은 영문자, 숫자, 특수문자를 최소 1개씩 포함하고 있는지 검사하는 혼합 검증용이다. (?=.)는 현재 검사 위치에서 조건을 걸어주는 방식인데, 조건을 충족하면 해당 조건이 소거된다. 만약 같은 위치에 여러 조건이 걸리는 경우, 조건이 충족되면 다음 문자로 검사가 이동되는 것이 아니라 충족된 조건이 소거되고 동일한 위치에서 다음 조건을 검사한다. 비밀번호의 경우에는 조건 위치를 모두 *로 걸어주었기 때문에 조건을 1번 이상(.) 충족해주면 검사가 통과된다. 또한 모든 문자(*)는 영문자, 숫자, 일부 특수문자만 가능하다. 정리하면 비밀번호는 영문자, 숫자, 일부 특수문자만 올 수 있으며 각각 최소 1개씩 가져야 한다는 조건이 된다.

비밀번호 확인은 바인딩 시점에는 @NotBlank만 적용하고 컨트롤러에서 비밀번호와 동일한지 검사하도록 했다.


3) 닉네임 검증

^[a-zA-Z가-힣0-9]*$

닉네임은 영문자, 한글, 숫자만 허용했다.



Enum 바인딩 및 검증

Enum이 값 제한을 걸어주는 기능은 좋지만 초보자로서는 사용하기가 까다롭다. 이번에도 어김없이 진행에 브레이크를 걸어주는 것은 Enum 이었다 ^^

Enum 필드

회원가입용 폼 객체 MemberSignUpForm에는 Enum 타입 필드가 두 개 있다.

Grade는 학년을 표시하는 필드로 1~4학년과 졸업생 총 5개의 값 중 하나를 가질 수 있다. Region은 서울, 부산, 대구 등 지역을 표시한다. (방송대는 온라인 기반 대학교이고 전국에 지역대학이 있기 때문에 지역이 의미가 커서 프로필에도 지역 항목을 넣었다.) 참고로 두 Enum 클래스는 모두 Member 안에 선언된 내부 클래스이다.

@NotNull(message = "학년을 선택하세요.")  // Enum 필드 유효성 검사는 setter 바인딩 시 수행됨
private Grade grade;

@NotNull(message = "지역을 선택하세요.")
private Region region;

Enum 필드 바인딩

폼에서 POST로 값이 넘어올 때 Enum 필드의 값은 String으로 넘어오기 때문에, 컨트롤러 파라미터에 바인딩될 때 아래와 같은 MethodArgumentNotValidException 오류가 발생한다.

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String  ...

검색해보니 전용 타입 컨버터를 만드는 등 여러 가지 방법이 있었지만, 나는 그냥 단순하게 setter에 valueOf()를 넣어서 파라미터로 받은 String 값과 일치하는 Enum을 찾아 바인딩하는 방법을 사용했다. valueOf()를 사용하면 매칭되는 Enum이 없는 경우 400 Bad Request가 발생하니 바인딩 시점에 검증도 되는 셈이다.

public void setGrade(String grade) {
	this.grade = Member.Grade.valueOf(grade);
}

public void setRegion(String region) {
	this.region = Member.Region.valueOf(region);
}

Enum 필드 바인딩 오류 메세지

Postman을 사용하여 Enum 필드에 유효하지 않은 값을 전달해 봤더니 methodInvocation 오류가 발생한다.

codes [methodInvocation.memberSignUpForm.grade,methodInvocation.grade,methodInvocation.java.lang.Enum,methodInvocation]

Enum 바인딩 오류 메세지는 공통적으로 '올바르지 않은 값입니다.' 라는 메세지를 추가했다. errors.properties 등록 방법은 아래에서 정리하겠다.

methodInvocation.java.lang.Enum=올바르지 않은 값입니다.


오류메세지 추가하기

@PostMapping("/signup")
public String signUp(@Validated @ModelAttribute MemberSignUpForm form, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) {}

위와 같이 컨트롤러에서 @Validated를 붙이면 @ModelAttribute를 통해 폼 데이터가 객체에 바인딩될 때 지정한 검증 애노테이션에 따라 검증을 수행하고, 오류가 있으면 BindingResult에 오류 명칭과 오류 메세지를 찾아서 추가해준다.


오류메세지 우선순위

오류 메세지는 오류 코드나 애노테이션을 통해서 유연하게 설정 가능한데, 스프링이 BindingResult에 오류 메세지를 찾아서 추가해줄 때 탐색 우선순위가 있다.

  1. 애노테이션의 message 속성 값 사용 (예. @NotNull(message = "지역을 선택하세요."))
  2. MessageSource에서 레벨별로 탐색 (아래에서 정리)
  3. 라이브러리가 제공하는 기본 메세지 사용 (BeanValidation이 기본 메세지 제공)

(강의노트에서는 1, 2 순서가 반대였는데 직접 적용해보니 위의 순서였다.)


MessageSource에서 오류메세지 등록하기

먼저 오류 코드를 통해 오류 메세지를 작성할 errors.properties 파일을 만든다. 그 다음 애플리케이션 설정 파일 application.properties에서 MessageSource를 등록해준다.

spring:
  messages:
    basename: errors

바인딩 오류는 오류 코드에 매칭할 수 있는데, 세분화된 오류 코드를 사용할 수도 있고 간단하게 범용 오류 코드를 사용할 수도 있다. 예를 들어 회원가입 폼 nickname 필드의 @NotBlank 오류에 매칭할 수 있는 오류 코드는 아래와 같다.

  • NotBlank.memberSignUpForm.nickname
  • NotBlank.nickname
  • NotBlank.java.lang.String
  • NotBlank

스프링은 오류 메세지를 가장 상세한 위쪽에서부터 아래쪽으로 설정 파일에 지정된 오류 메세지가 있는지 탐색하고, 없으면 라이브러리가 제공하는 기본 메세지를 사용한다.


NotNull=필수 항목입니다.
NotBlank=필수 입력 항목입니다.
typeMismatch=올바르지 않은 값입니다.
methodInvocation.java.lang.Enum=올바르지 않은 값입니다.

영한님이 추천하는 방법은 범용 메세지를 먼저 작성하고 필요에 따라 상세한 메세지를 작성하는 것이 좋다고 하셨다.

나는 위와 같이 errors.properties 범용 메세지를 작성했고, 상세한 메세지는 회원가입 폼 애노테이션의 message 속성을 사용하여 작성했다. (검증 애노테이션의 message 속성을 사용하면 min, max 등 보다 상세하게 메세지를 작성할 수는 있지만 국제화 메세지 적용이 가능한지는 모르겠다. 이거는 지금 문제가 아니기 때문에 패스했다.)



JSP View에 BindingResult 오류 메세지를 출력하는 것은 🔗 회원가입 폼 검증하기 − ② (JSP)에 정리했다.

0개의 댓글