• @Valid를 이용해 타임리프에서 유효성 검증하기

    2022. 8. 19.

    by. 내이름은 킹햄찌

    Bean Validation

    • Bean Validation 2.0(JSR - 380) 기술 표준으로 특정 구현체가 아님
    • 대표적으로 Hibernate validator를 사용함

     

     

    환경구성

    Spring Boot 2.3버전 이후 부터는 프로젝트 생성시에 javax.validation이 자동 추가 되지 않기 때문에 build.gradle에 의존성을 추가해야 한다. 자세한건 여기를 통해 확인 할 수 있다.

    Spring에서는 LocalValidatorFactoryBean이 조건 검증을 처리하기 때문에 이를 빈에 등록해야하지만 SpringBoot에서는 위의 방법으로 의존성 추가만으로 설정이 된다.

     

     

    배경

    Bean Validaton이 나오게 된 배경은 어플리케이션 전체에 분산되어 있는 유효성 검사 코드가 중복이 발생하고, 검사 로직을 찾기 힘들어 지며 자연스럽게 코드가 복잡하게 변했다. 그래서 JAVA에서는 유효성 검사기 필요한 도메인 모델에 어노테이션을 이용해 직접 정의하여 사용할 수 있는 Bean Validation 프레임 워크를 만들게 된 것이다.

     

     

    @Valid 동작 원리

    모든요청은 프론트 컨트롤러인 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작한다. @Valid 역시 같은 방법으로 처리된다. 컨트롤러에 사용된 어노테이션이 @ModelAttribute 또는 @RequestBody를 사용하면 ArgumentResolver의 구현체인 ModelAttributeMethodProcessor 또는 RequestResponseBodyMethodProcessor가 받은 요청을 객체로 변환하면서 @Valid가 있을때 유효성 검증을 같이 진행하게 된다. 검증 오류가 발생한다면 MethodArgumentNotValidException 예외가 발생하게되고 디스패처 서블릿에 기본으로 등록된 예외 리졸버인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다. @Valid를 사용하여 조건검증이 이루어지는 곳이 컨트롤러 이기 때문에 기본적으로 컨트롤러에서만 동작하고 다른 계층에서 사용하기 위해서는 @Validated와 결합되어야 한다.

     

     

    @Valid 예시

    package spring.postproject.Member.dto;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import javax.validation.constraints.NotBlank;
    
    @Data
    @NoArgsConstructor
    public class MemberCreateDto {
    
        @NotBlank(message = "닉네임은 필수입니다.")
        private String nickname;
        @NotBlank(message = "아이디는 필수입니다.")
        private String userId;
        @NotBlank(message = "비밀번호는 필수입니다.")
        private String password;
    
        @Builder
        public MemberCreateDto(String nickname, String userId, String password) {
            this.nickname = nickname;
            this.userId = userId;
            this.password = password;
        }
    }

    위와같이 유효성 검사를 진행할 변수 상단에 어노테이션을 사용하여 도메인 레벨에서 어노테이션을 이용하여 유효성 검사를 할 수 있다.

     

     

    Controller에서 사용

    @PostMapping("/member/new")
        public String create(@Valid @ModelAttribute MemberCreateDto memberCreateDto, BindingResult result, Model model){
    
            if (result.hasErrors()) {
                return "member/memberCreate";
            }
    
            Member member = Member.builder()
                    .userId(memberCreateDto.getUserId())
                    .password(memberCreateDto.getPassword())
                    .nickName(memberCreateDto.getNickname())
                    .build();
    
            member.updateRole(MemberRoll.ROLE_NORMAL);
            memberService.signUp(member);    
            return "/member/loginForm";
        }

    Controller에서는 요청을 받았을때 검증할 파라미터 앞에 @Valid를 사용함으로써 해당 파라미터의 유효성을 검증할 수 있게된다.

    검증 오류 발생시 BindingResult에 FieldError, ObjectError를 추가하여 반환한다. BindingResult는 Error가 발생것으로 예상되는 파라미터 뒤에 위치시켜야 한다. 에러가 추가된 BindingResult는 자동으로 Model에 담겨지기 때문에 아래와 같이 타임리프에서도 사용할 수 있다.

     

     

     

    thymeleaf

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/header :: header" />
    <style>
        .fieldError {
            border-color: red;
            color : red;
        }
    </style>
    <body>
    <div class="container">
        <div th:replace="fragments/bodyHeader :: bodyHeader"/>
        <form role="form" action="/member/new" th:object="${memberCreateDto}" method="post">
            <div class="form-group">
                <label th:for="nickname">닉네임</label>
                <input type="text" th:field="*{nickname}" class="form-control" placeholder="닉네임를 입력하세요" th:class="${#fields.hasErrors('nickname')}? 'form-control fieldError' : 'form-control'">
                <p th:if = "${#fields.hasErrors('nickname')}" th:errors="*{nickname}">Incorrect data</p>
            </div>
    
            <div class="form-group">
                <label th:for="userId">아이디</label>
                <input type="text" th:field="*{userId}" class="form-control" placeholder="아이디를 입력하세요" th:class="${#fields.hasErrors('userId')}? 'form-control fieldError' : 'form-control'">
                <p th:if = "${#fields.hasErrors('userId')}" th:errors="*{userId}">Incorrect data</p>
            </div>
    
            <div class="form-group">
                <label th:for="password">비밀번호</label>
                <input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호를 입력하세요" th:class="${#fields.hasErrors('password')}? 'form-control fieldError' : 'form-control'">
                <p th:if = "${#fields.hasErrors('password')}" th:errors="*{password}">Incorrect data</p>
            </div>
            <button type="submit" class="btn btn-primary">확인</button>
        </form>
        <br/>
        <div th:replace="fragments/footer :: footer" />
    </div> <!-- /container -->
    </body>
    </html>

    post 요청으로 들어온 데이터를 컨트롤러에서 조건 검증하여 예외 발생시 결과를 Model에 담아서 위의 폼으로 다시 랜더링 한다. 에러 발생시 해당 클래스를 에러 필드로 변경하고(th:class="${#fields.hasErrors('password')}? 'form-control fieldError' : 'form-control'")

    에러의 내용을 출력(th:errors="*{nickname}") 한다. 자세한건 thymeleaf 공식문서에서 확인할 수 있다.

     

    아래는 아무것도 입력하지 않고 확인버튼을 눌렀을때의 결과이다.

     

     

     

     

     

    ref

    https://jaimemin.tistory.com/1884

    https://meetup.toast.com/posts/223

    https://kapentaz.github.io/java/Java-Bean-Validation-제대로-알고-쓰자/#

    https://mangkyu.tistory.com/174

    댓글