Spring

[SpringBoot] Request로 오는 객체 검증(Validation) 알아보기

별토끼. 2021. 8. 15. 21:23
반응형

객체 검증은 정말 자주 하는 작업이지만 정말 중요한 작업이기도 하다. Annotation만 붙일 것이 아니라, 어떻게 동작하는지 또 어떻게 깔끔한 유효성 검사를 할 수 있는지 알아보자.

Intro

일반적 애플리케이션의 Validation 검시 시, 아래와 같은 문제를 갖고 있다.

  1. 애플리케이션 전체에 분산되어 있다.
  2. 코드 중복이 심하다.
  3. 비즈니스 로직에 섞여있어 로직 추적이 어렵고, 복잡해진다.

Bean Validation

위와 같은 문제를 해결하기 위해 Java에서는 Bean Validation이라는 유효성 검사 프레임워크를 제공해준다. 도메인 모델에 어노테이션으로 정의할 수 있다.

dependency 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

사용하기

Controller 에서는 @Valid 어노테이션을 추가하면 된다.

    @PostMapping("/form")
    public void PostForm(@Valid CreateForm createForm) {
        // ..
    }

Service나 Bean에서 사용하려면 Class 위에 @Validated도 추가해줘야 한다.

@Validated
@Service
public class FormService {
    @PostMapping("/form")
    public void PostForm(@Valid CreateForm createForm) {
        // ..
    }
}

여기서 데이터 유효성 검사가 여러 번 실행될 경우, 성능에 영향을 줄 수 있기 때문에 주의해야 한다.

오류 처리

데이터 유효성 검사 실패 시, ConstraintViolationException을 발생시킨다. ConstraintViolationException은 실패 정보를 담고 있는 ConstraintViolation 객체들을 가지고 있다. 이를 이용해 ExceptionHandler로 적절한 오류 응답을 만들 수 있다.

// 참고 : https://meetup.toast.com/posts/223
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(value = ConstraintViolationException.class) // 유효성 검사 실패 시 발생하는 예외 처리
    @ResponseBody
    protected Response handleException(ConstraintViolationException exception) {
        return Response
            .builder()
            .header(Header
                .builder()
                .isSuccessful(false)
                .resultCode(-400)
                .resultMessage(getResultMessage(exception.getConstraintViolations().iterator())) // 오류 응답을 생성
                .build())
            .build();
    }

    protected String getResultMessage(final Iterator<ConstraintViolation<?>> violationIterator) {
        final StringBuilder resultMessageBuilder = new StringBuilder();
        while (violationIterator.hasNext() == true) {
            final ConstraintViolation<?> constraintViolation = violationIterator.next();
            resultMessageBuilder
                .append("['")
                .append(getPopertyName(constraintViolation.getPropertyPath().toString())) // 유효성 검사가 실패한 속성
                .append("' is '")
                .append(constraintViolation.getInvalidValue()) // 유효하지 않은 값
                .append("'. ")
                .append(constraintViolation.getMessage()) // 유효성 검사 실패 시 메시지
                .append("]");

            if (violationIterator.hasNext() == true) {
                resultMessageBuilder.append(", ");
            }
        }

        return resultMessageBuilder.toString();
    }

    protected String getPopertyName(String propertyPath) {
        return propertyPath.substring(propertyPath.lastIndexOf('.') + 1); // 전체 속성 경로에서 속성 이름만 가져온다.
    }
}

위와 같은 ExceptionHandler를 작성하면, 아래와 같은 응답을 받을 수 있다.

{
    "header" : {
        "isSuccessful" : false,
        "resultCode" : -400,
        "resultMessage" : "['title' is 'null'. must not be blank], ['body' is 'null'. must not be null]"
    }
}

@PathVariable, @RequestParam에 대한 유효성 검사

개인적으로 개발하면서 궁금증을 가졌던 부분이다. DTO안에 primitive type이나 String 외에 다른 클래스가 있다면 어떻게 해야할까? 해당 클래스를 Validation하려면 그 DTO에 @Valid를 적용하고 다시 어노테이션을 적용하면 된다 !

StudentDto

@Getter
@Setter
public class StudentDto {
    @NotBlank
    private String name;

    @Valid
    private TestInfo testInfo; //Dto내에 또다른 class를 가진 객체가 있다면 @Valid를 설정한다.
}

TestInfo

@Getter
@Setter
public class TestInfo {
    @NotNull
    @Max(value=100)
    @Min(value=0)
    private int score;
}

참고
Validation 어디까지 해봤니?
서버 개발한다면 꼭 해야하는 작업, Spring validation

반응형