스프링 Custom Bean Validation 만들어서 사용해보기(어노테이션 @Email)

스프링 Bean Validation 이란??

스프링에서는 JavaBean(getter와 setter를 가지고 있는 자바객체, 흔히 VO, DTO라고 부르는 것들)의 유효성 작업을 진행하기 위해 javax.validation 패키지 내에 있는 여러가지 유효성 관련 클래스들을 활용할 수 있습니다. 

* 참고로 스프링 부트 2.3 버전 이후부터는 Spring Web 유효성에서 Validation 의존성이 따로 분리되어 별도로 Validation 의존성을 추가해야 javax.validation 패키지를 활용하여 여러가지 유효성 검사를 어노테이션을 통해 활용할 수 있습니다.

 


 

대표적으로 오늘 살펴볼 ConstraintValidator 인터페이스를 구현하여 아래와 같이 Email의 유효성 검사를 할 수 있는 EmailValidator를 생성하여 어노테이션만으로 해당 필드의 유효성 검사를 진행할 수 있습니다.

 

예제를 함께하시고 정상적으로 Bean Validator를 생성했다면 아래와 같이 IDE의 Bean Validation 탭을 펼쳐서 열어보면 작성한 EmailValidator를 확인해보시면 될 것 같습니다.

 

모든 소스는 이곳에 있습니다.

 

 

예제 프로젝트 구조

 

- @Email 어노테이션을 통해 javax.validation의 어노테이션과 마찬가지로 Validation 작업을 진행해보려고 합니다.

- UserController을 만들어서 간단하게 api를 만들어볼 생각입니다. 

- UserParam은 Email을 request Body로 담을 건데 그 그릇입니다.

- TDD는 아니었지만 Test 코드를 작성하여  api를 호출할 UserControllerTests 클래스를 생성하였습니다.

 


 

예제 코드

 

UserController.java

package com.example.demo1;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {

    @PostMapping
    public ResponseEntity addUser(@RequestBody @Validated UserParam userParam) 
			throws Exception {
                                                            
        return new ResponseEntity(userParam, HttpStatus.OK);
    }
}

- 간단히 @PostMapping으로 Request Body를 UserParam으로 받고 유효성만 검사한 후, 그대로 리턴해줍니다. 

 

UserParam.java

package com.example.demo1;

import lombok.*;
import javax.validation.constraints.NotEmpty;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class UserParam {
    @NotEmpty(message = "이메일을 입력해시기 바랍니다.")
    @Email
    private String email;
}

- email이 null or 빈문자열이 아니면 일단 @Email 어노테이션의 로직을 검사하게 될 것입니다.

 

UserControllerTests.java

package com.example.demo1;

import com.fasterxml.jackson.databind.ObjectMapper;
import jdk.nashorn.internal.ir.annotations.Ignore;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    @Ignore
    public void addUser() throws Exception {
        UserParam param = new UserParam();
        param.setEmail("doqndnffogmail.com");

        mockMvc.perform(post("/user")
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .accept(MediaType.APPLICATION_JSON_VALUE)
                    .content(objectMapper.writeValueAsString(param)))
                .andDo(print())
                .andExpect(status().isOk());
    }

}

- param의 email을 @없이 이상하게 추가하였습니다.

 

@Email 어노테이션
package com.example.demo1;

import org.springframework.util.StringUtils;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.util.regex.Pattern;

import static java.lang.annotation.ElementType.FIELD;

@Constraint(validatedBy = Email.EmailValidator.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(FIELD)
public @interface Email {

    String message() default "이메일이 양식에 맞지 않습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default{};

    class EmailValidator implements ConstraintValidator<Email, String> {
        private final String REGEX_EMAIL = "[a-z0-9][a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])";
        public Pattern email = Pattern.compile(REGEX_EMAIL);

        @Override
        public boolean isValid(String s, 
                          ConstraintValidatorContext constraintValidatorContext) {
            System.out.println("email validation");
            if(StringUtils.isEmpty(s)) {
                return true;
            } else {
                return email.matcher(s).matches();
            }
        }
    }

}

- @Retention과 @Target에 대해서 알고싶으시다면 다음을 참조해주세요. 

https://sas-study.tistory.com/329 / https://sas-study.tistory.com/276

 

- @Constraint 어노테이션의 validatedBy 속성에 Email의 유효성을 검증할 Validator의 Class 타입을 지정해줍니다.

- Validator는 EmailValidator로 위에서 언급했던 ConstraintValidator를 구현합니다. 간단히 isValid 메소드만 구현하였습니다.

- isValid의 String에는 Request Body를 통해 넘어온 email 값이 넘어오게 됩니다. 이를 정규식으로 matches만 해주면 깔끔하게 유효성 검사를 마칠 수 있습니다.

 

만약 유효성검사가 제대로 진행되지 않으신다면 Controller를 확인해보셔서 @Validated 어노테이션을 UserParam앞에 잘 달아놨는지 확인해보시기 바랍니다. 또한, 아래처럼 Bean Constraints에 @Email 어노테이션이 잘 등록됐는지 확인해보시면 될 것 같습니다. 

 

 

결과 확인

response에 아무것도 나오지 않아서 조금 애매합니다. UserController를 수정하여 메시지만 출력해보도록 하겠습니다.

 


 

수정된 UserController의 addUser 메소드

@PostMapping
    public ResponseEntity addUser(@RequestBody @Validated UserParam userParam, 
                Errors errors) throws Exception {
    
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(errors.getFieldError().getDefaultMessage());
        }
        System.out.println(userParam);
        return new ResponseEntity(userParam, HttpStatus.OK);
    }

Errors 객체는 에러가 발생하면 에러를 담습니다. 이전에는 바로 튕겨내어 Controller에 진입조차 못했다면 Errors 객체를 통해 error에 대한 데이터를 가지고 Controller에서 판별할 수 있습니다. 

 

 

 

수정된 결과

 

댓글

Designed by JB FACTORY