@Valid란?
@Valid는 JSR-303 표준 스펙(자바 진영 스펙)으로써 Bean Validator를 이용하여 객체의 제약 조건을 검증하도록 지시하는 어노테이션이다. JSR 표준의 빈 검증 기술의 특징은 객체의 필드에 달린 어노테이션으로 편리하게 검증을 한다는 것이다.
Spring에서는 LocalValidatorFactoryBean을 Bean으로 등록하면 LocalValidatorFactoryBean이제약 조건 검증을 처리한다. Spring Boot에서는 아래와 같이 의존성을 추가해주면 해당 기능들을 사용할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
동작 원리
Spring에서 모든 요청은 DispatcherServlet을 통해 컨트롤러로 전달된다. 전달 과정에서 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데 @Valid도 ArgumentResolver가 처리한다.
대표적으로 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 @RequestBody의 Json 메세지를 객체로 변환해주는 작업이 있다. 이 내부에 @Valid로 시작하는 어노테이션이 있다면 유효성 검사를 진행한다.
그리고 여기서 검증에 오류가 있다면 발생하는 오류가 이전에 작성했던 MethodArgumentNotValidException이다. DispatcherServlet에 기본으로 등록된 Exception Resolver인 DefaultHandlerExceptionResolver에 의해 400에러가 발생한다.
이러한 이유로 @Valid는 컨트롤러에서만 동작하며 다른 계층에서는 검증이 되지 않는다. 다른 계층에서 파라미터를 검증하려면 @Validated와 결합되어야 한다.
@Validated
유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋지만 불가피하게 다른 곳에서 파라미터 검증을 해야하는 경우 이 @Validated를 사용할 수 있다. @Valid와 다르게 Spring에서 제공해주는 기능이며 AOP기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해준다.
아래와 같이 클래스에 @Validated 어노테이션을 붙여주고 검증이 필요한 메소드의 파라미터에 @Valid 어노테이션을 붙여주면 된다.
@Service
@Validated
public calss ValidTestService{
public void validTest(@Valid ValidRequest validRequest){
...
}
}
유효성 검증에 실패하면 ConstraintViolationException이 발생한다. 이는 @Valid와 @Validated가 제공해주는 곳도 다르고 동작 원리도 다르기 때문이다.
동작 원리
AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. 이후 해당 클래스의 메소드가 호출되면 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.
위와 같이 동작하기 때문에 컨트롤러, 서비스, 레포지토리 등의 계층과는 상관없이 Spring Bean이라면 유효성 검증을 진행할 수 있다.
@Valid 적용
위에서 언급한 의존성을 build.gradle에 추가해준다
build.gradle(member 모듈)
bootJar{
enabled = true
}
jar{
enabled = false
}
dependencies {
implementation project(':common-response')
runtimeOnly 'mysql:mysql-connector-java' //MySQL
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //JPA
implementation 'org.springframework.boot:spring-boot-starter-validation' //@Valid
}
다음으로 검증할 객체와 해당 객체의 제약사항을 추가해준다.
dto/MemberDto
package com.seungh1024.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.*;
import lombok.Builder;
import lombok.Getter;
public class MemberDto {
@Getter
@Builder
public static class MemberJoinRequest{
@Email
@NotBlank
@Size(max = 50, message = "50자 이하만 가능합니다.")
private String memberEmail;
@NotBlank
@Size(min = 8,max = 20, message = "8자 이상, 20자 이하만 가능합니다.")
private String memberPassword;
@NotBlank
@Size(min = 2,max = 20, message = "2자 이상, 20자 이하만 가능합니다.")
private String memberName;
}
@Getter
@Builder
public static class MemberUpdateRequest{
private int memberId;
@NotBlank
@Size(min = 8,max = 20, message = "8자 이상, 20자 이하만 가능합니다.")
private String memberPassword;
}
...
}
기존의 DTO에 유효성 검증을 위한 어노테이션들만 붙었다. 다양한 어노테이션들 중 대표적인 것들과 위에 사용한 어노테이션들을 설명하자면
- @Email : 이메일 형식인지 검증한다
- @NotNull : 해당 값이 null인지 아닌지 검증한다.
- @NotEmpty : 해당 값이 null이 아니고 비어있는 문자열(””)인지 아닌지 검증한다. 단, (” “)과 같은 공백 문자열은 허용된다.
- @NotBlank : 해당 값이 null이 아니고 비어있는 문자열(””)인지, 공백 문자열(” “)인지 검증한다.
- @AssertTrue : 해당 값이 true인지 검증한다.
- @Size : 해당 값이 주어진 값 사이에 해당하는지 검증한다. 아래에 설명할 @Min, @Max의 경우 숫자를 사용하는 필드 검증하는데 사용되지만 @Size는 문자열, 배열 등의 크기를 검증하는데 사용된다. 옵션으로 min, max를 설정할 수 있다.
- @Min, @Max : 각각 주어진 값보다 작지 않은지, 크지 않은지 검증한다. 옵션으로 value, message를 설정할 수 있다. @Min(value = 2 , message = “message”) 이렇게 사용하면 최소 값이 2이상이어야 한다.
- @Pattern : 해당 값이 주어진 패턴과 일치하는지 검증한다.
이제 제약조건을 추가했으니 해당 객체를 파라미터로 받는 API에 @Valid만 추가해주면 된다.
controller/MemberController
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody @Valid MemberDto.MemberJoinRequest memberDto){
Member member = memberService.createMember(memberDto);
MemberDto.MemberAllResponse response = MemberDto.MemberAllResponse.builder()
.memberId(member.getMemberId())
.memberEmail(member.getMemberEmail())
.memberName(member.getMemberName())
.build();
return new ResponseEntity(success(response),HttpStatus.OK);
}
이렇게 간단하게 @Valid만 추가하면 잘 동작한다.
테스트
이메일 형식에 맞지 않으니 해당 에러 메세지가 나왔다.
이전에 MethodArgumentNotValidException을 만들었었다. 해당 에러 핸들링에서 bindingResult객체를 넘겨주면 해당 객체의 FieldError에서 에러 목록들을 읽어서 field,value,message를 넣어준 각각의 객체로 만든 것이 기억이 날 것이다.
그렇다면 필드의 여러 부분들을 조건에 맞지않게 보내면 해당 내용들이 모두 리스트로 실려서 응답으로 올 것이다.
테스트해보니 위와 같이 잘 나오는 것을 볼 수 있었다.
'Java > Spring boot 프로젝트' 카테고리의 다른 글
Spring Security + JWT 적용하기(1) - 게시판 만들기(10) (0) | 2023.04.06 |
---|---|
Entity 모듈 분리(멀티 모듈 프로젝트) - 게시판 만들기(9) (0) | 2023.03.22 |
Spring Entity와 DTO 구분하기 -게시판 만들기(7) (0) | 2023.03.16 |
@ControllerAdvice를 사용한 예외 처리,에러 핸들링 - 게시판 만들기 (6) (0) | 2023.03.16 |
Spring 공통 응답 만들기(Enum, 제네릭 타입) - 게시판 만들기(5) (0) | 2023.02.20 |