[스프링(spring)]@Valid

allnight5·2022년 12월 15일
0

스프링

목록 보기
11/62

참조사이트1
추후에.. 붙여넣기만 하지 말고 다른곳에서나 자료찾고 읽어보면서 수정해보기..
필요한 것만 넣으려고 했는데 생각보다 내용이 알차다

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있는데 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수 있다고 합니다.

@Valid의 개념 및 사용법

@Valid는 JSR-303 표준 스펙(자바 진영 스펙)으로써 빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 지시하는 어노테이션이다.JSR 표준의 빈 검증 기술의 특징은 객체의 필드에 달린 어노테이션으로 편리하게 검증을 한다는 것이다.

의존성만 추가해주면 해당 기능들이 자동으로 설정된다.

//https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
//제약조건 검증을 사용하기 위해서는 다음의 의존성을 build.gradle에 추가해주어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

예를 들어 @NotNull 어노테이션은 필드의 값이 null이 아님을 확인하도록 하며 @Min은 해당 값의 최솟값을 지정할 수 있도록 한다.

@Getter
@RequiredArgsConstructor
public class AddUserRequest {

	@Email
	private final String email;

	@NotBlank
	private final String pw;

	@NotNull
	private final UserRole userRole;

	@Min(12)
	private final int age;

}

그리고 다음과 같이 컨트롤러의 메소드에 @Valid를 붙여주면 유효성 검증이 진행된다.

@PostMapping("/user/add") 
public ResponseEntity<Void> addUser(@RequestBody @Valid AddUserRequest addUserRequest) {
      ...
}

여기까지 진행하면서 어노테이션을 넣어주면 진행되는지 알았는데. Valid를 붙여줘서 유효성 검사를 해줘야 하는구나 라는것을 알게되었다.

@Valid의 동작 원리


모든 요청은 프론트 컨트롤러인 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid 역시 ArgumentResolver에 의해 처리가 된다.
대표적으로 @RequestBody는 Json 메세지를 객체로 변환해주는 작업이 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에서 @Valid로 시작하는 어노테이션이 있을 경우에 유효성 검사를 진행한다. (이러한 이유로 @Valid가 아니라 커스톰 어노테이션인 @ValidMangKyu여도 동작한다.) 만약 @ModelAttribute를 사용중이라면 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.
검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생하게 되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.
이러한 이유로 @Valid는 기본적으로 컨트롤러에서만 동작하며 기본적으로 다른 계층에서는 검증이 되지 않는다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 한다.

[ @Validated를 이용한 유효성 검증 ]
@Validated의 개념 및 사용법
입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 하지만 개발을 하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다. Spring에서는 이를 위해 AOP 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated를 제공하고 있다. @Validated는 JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.

@Service
@Validated
public class UserService {

	public void addUser(@Valid AddUserRequest addUserRequest) {
		...
	}
}

유효성 검증에 실패하면 에러가 발생하는데, 로그를 확인해보면 이전의 MethodArgumentNotValidException 예외가 아닌 ConstraintViolationException 예외가 발생했다.
validated는 값이 Null이여서는 안된다.

여기까지 보면 결국 Valid 어노테이션을 사용하는데 validated 혼자사용하는게 아니라 같이사용하면 valid를 사용하는것과 무엇이 다른가 생각이 든다.

@Validated의 동작 원리
특정 ArgumnetResolver에 의해 유효성 검사가 진행되었던 @Valid와 달리, @Validated는 AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. 그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.
이러한 이유로 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다. 대신 클래스에는 유효성 검증 AOP가 적용되도록 @Validated를, 검증을 진행할 메소드에는 @Valid를 선언해주어야 한다.
이러한 이유로 @Valid에 의한 예외는 MethodArgumentNotValidException이며, @Validated에 의한 예외는 ConstraintViolationException이다. 이를 알고 있으면 나중에 예외 처리를 할 때 도움이 된다.


어째서 Validated를 쓰느냐 계층 무관하게 스프링 빈의 유효성 검증을 하기 위해서 사용하는데 검증을 진행할 메소드를 선택에 주기 위해서 Valid를 넣어주는 것이었다.


@Validated의 또 다른 기능(유효성 검증 그룹의 지정)


동일한 클래스에 대해 제약조건이 요청에 따라 달라질 수 있다. 예를 들어 일반 사용자의 요청과 관리자의 요청이 1개의 클래스로 처리될 때, 다른 제약 조건이 적용되어야 할 수 있는 것이다. 이때는 검증될 제약 조건이 2가지로 나누어져야 하는데, Spring은 이를 위해 제약 조건이 적용될 검증 그룹을 지정할 수 있는 기능 역시 @Validated를 통해 제공하고 있다.

검증 그룹을 지정하기 위해서는 (내용이 없는) 마커 인터페이스를 간단히 정의해야 한다. 위의 예시의 경우에는 사용자인 경우와 관리자인 경우를 분리해야 하므로 다음과 같은 2개의 마커 인터페이스를 만들 수 있다.

그리고 해당 제약 조건이 적용될 그룹을 groups로 지정해줄 수 있다. 제약 조건이 적용될 그룹이 여러 개라면 {}를 이용해 그룹의 이름을 모두 넣어주면 된다. 예를 들어 다음과 같이 DTO에 그룹 속성을 지정해줄 수 있다.

@NotEmpty(groups = {UserValidationGroup.class, AdminValidationGroup.class} ) 
private String name; 

@NotEmpty(groups = UserValidationGroup.class) 
private String userId; 

@NotEmpty(groups = AdminValidationGroup.class) 
private String adminId;

그리고 컨트롤러에서도 다음과 같이 제약조건 검증을 적용할 클래스를 지정해주면 된다.

@PostMapping("/users") 
public ResponseEntity<Void> addUser(
    @RequestBody @Validated(UserValidationGroup.class) AddUserRequest addUserRequest) {
    
      ...
}

@Validated에 특정 클래스를 지정하지 않는 경우: groups가 없는 속성들만 처리
@Valid or @Validated에 특정 클래스를 지정한 경우: 지정된 클래스를 groups로 가진 제약사항만 처리
@Validated의 그룹 지정 기능은 코드가 복잡해져서 거의 사용되지 않으므로 이러한 기능이 있음을 참고하고 넘어가도록 하자.

예를 들어 새로운 사용자를 추가하는 API를 개발중이라고 하자. 그리고 이에 대한 입력값의 요구사항 및 제약사항은 다음과 같다.

이메일: 이메일 형식으로 입력을 받아야 한다.
비밀번호: 빈 값을 받으면 안된다.
역할: 사용자의 역할은 null이면 안된다.
나이: 사용자의 나이는 12세 이상이여야 한다.

@Getter
@RequiredArgsConstructor
public class AddUserRequest {

	@Email
	private final String email;

	@NotBlank
	private final String pw;

	@NotNull
	private final UserRole userRole;

	@Min(12)
	private final int age;

}

그리고 이러한 DTO를 요청으로 받는 UserController를 다음과 같이 작성해줄 수 있다.

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/users")
    public ResponseEntity<Void> addUser(final @RequestBody @Valid AddUserRequest addUserRequest) {
        userService.registerUser(addUserRequest);

        return ResponseEntity.noContent().build();
    }

}

예시로 어떤때 써야되는지 알수있었다.

[ @Valid와 @Validated 유효성 검증 차이 ]


위에서 살펴보았듯 @Validated의 기능으로 유효성 검증 그룹의 지정도 있지만 거의 사용되지 않으므로 유효성 검증 진행을 기준으로 차이를 살펴보도록 하자.

@Valid
JSR-303 자바 표준 스펙
특정 ArgumentResolver를 통해 진행되어 컨트롤러 메소드의 유효성 검증만 가능하다.
유효성 검증에 실패할 경우 MethodArgumentNotValidException이 발생한다.
@Validated
자바 표준 스펙이 아닌 스프링 프레임워크가 제공하는 기능
AOP를 기반으로 스프링 빈의 유효성 검증을 위해 사용되며 클래스에는 @Validated를, 메소드에는 @Valid를 붙여주어야 한다.
유효성 검증에 실패할 경우 ConstraintViolationException이 발생한다.

검증을 위한 커스텀 어노테이션 생성 ]
먼저 우리는 javax.validation에서 제공해주는 어노테이션들과 비슷한 어노테이션을 만들어주어야 한다. 우리는 연락처가 올바른 포맷인지 검증할 계획이므로 다음과 같은 어노테이션을 만들어줄 수 있다.

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {

    String message() default "Invalid Phone";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}

오.. 어노테이션도 만들어 쓸수 있구나 라는 것을 알았는데 어노테이션을 만드는데도 어노테이션을 쓰는구나.. 라는 생각도 들었다. 메소드 만드는데.. 다양한 로직이 들어다니까 쓰는게 맞는것 같기도 하다.?

Validator를 주입받아 프로그래밍적으로 유효성 검증을 진행할 수도 있다. 하지만 Spring이 제공해주는 핵심 가치 중 하나는 관심사의 분리를 통해 비지니스 로직에 집중할 수 있게 해주는 것이라고 생각한다. 굳이 코드 상으로 검증 로직을 넣고, 에러 처리를 해줄 필요는 거의 없어 보인다. 어노테이션을 통해 비지니스 로직이 아닌 유효성 검증과 같은 것들은 분리해내도록 하자.

위의 말이 아직 이해가 안가는데.. 굳이 검증로직을 넣어 에러처리를 하지말자까지는 이해가는데 어노테이션을 통해 유효성 검증과 비지니스 로직을 분리하자는 건 아직 이해가 안간다. Validator이 어노테이션으로 유효성 검증을 하는거 아닌가 싶은데 로직에 Vaild가 들어가니 사용하지 말고 분리하는것이 좋다는것일까?

profile
공부기록하기

0개의 댓글