JeongJin's Blog

10. 유효성 검사와 예외 처리 (2) 본문

Book Study/스프링 부트 핵심 가이드

10. 유효성 검사와 예외 처리 (2)

정진킴 2023. 11. 21. 08:41

10.3 스프링 부트에서 유효성 검사


10.3.4 @Validated 활용

  • 유효성 검사를 수행하기 위해 @Valid 어노테이션을 선언하였습니다. @Valid 어노테이션은 자바에서 지원하는 어노테이션이며, 스프링도 @Validated 라는 별도의 어노테이션으로 유효성 검사를 지원한다.
  • @Validated는 @Valid 어노테이션의 기능을 포함하고 있기 때문에 @Validated 로 변경이 가능하다
  • @Validated는 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.
public interface ValidationGroup1 {
}
  • ValidationGroup1 인터페이스
public interface ValidationGroup2 {
}
  • ValidationGroup2
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {

	...

    @Min(value = 20, groups = ValidationGroup1.class)
    @Max(value = 40, groups = ValidationGroup1.class)
    //@Min(value = 20)
    //@Max(value = 40)
    private int age;

    @Positive(groups = ValidationGroup2.class)
    //@Positive
    private int count;

	...
}
  • @Min, @Max 어노테이션의 groups 속성을 사용해 ValidationGroup1 그룹을 설정하고 @Positive 어노테이션에 ValidationGroup2를 설정을 통해 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정
  • 실제로 그룹을 어떻게 설정해서 유효성 검사를 실시할지 결정하는 것은 @Validated 어노테이션에서 한다.
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
        @Validated @RequestBody ValidatedRequestDto validatedRequestDto) {
    LOGGER.info(validatedRequestDto.toString());
    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
        @Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
    LOGGER.info(validatedRequestDto.toString());
    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
        @Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
    LOGGER.info(validatedRequestDto.toString());
    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
        @Validated({ValidationGroup1.class,
                ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
    LOGGER.info(validatedRequestDto.toString());
    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
  • 첫번째 PostMapping에서는 @Validated 어노테이션에 속성을 지정하지 않음
  • 두번째 PostMapping에서는 @Validated 어노테이션에 ValidationGroup1 속성 지정
  • 세번째 PostMapping에서는 @Validated 어노테이션에 ValidationGroup2 속성 지정
  • 네번째 PostMapping에서는 @Validated 어노테이션에 ValidationGroup1, ValidationGroup2 속성 지정

첫번째 PostMapping body
첫번째 Post 응답

  • 요청 데이터를 보면 age와 count는 변수에 대한 유효성 검사를 통과하지 못하는 데이터 입니다. 하지만 첫번째 메서드를 호출 했을 경우 정상적으로 통과하는 것을 확인할 수 있다.
  • @Validated 어노테이션에 특정 그룹을 지정하지 않는 경우에는 groups 속성을 설정하지 않은 필드에 대해서만 유효성 검사를 실시한다.

두번째 Post 응답

  • ValidationGroup1 으로 지정된 age에 대한 에러가 발생

세번째 Post 응답

    • ValidationGroup2 으로 지정된 count에 대한 에러가 발생

네번째 Post 응답

  • ValidationGroup1, ValidationGroup2 으로 지정된 age, count에 대한 에러 발생

booleanCheck를 false로 변경한 body
정상 응답

  • 위 데이터는 age와 count는 검사를 통과하고 booleanCheck 변수에서 검사를 실패하는 데이터 이지만 정상적으로 응답이 온다
  • 정리
    • @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사를 수행
    • @Validated 어노테이션에 특정 그룹을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사를 수행

10.3.5 커스텀 Validation 추가

  • 실무에서는 유효성 검사를 실시할 때 자바 또는 스프링의 유효성 검사 어노테이션에서 제공하지 않는 기능을 써야할 경우도 있다. 이 경우 ConstraintValidator 와 커스텀 어노테이션을 조합해서 별도의 유효성 검사 어노테이션을 생성할 수 있다.
    • 동일한 정규식을 계속 쓰는 @Pattern 어노테이션의 경우가 가장 흔한 사례

▶ 전화번호 형식이 일치하는 확인하는 유효성 검사 어노테이션

public class TelephoneValidator implements ConstraintValidator {
	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		if (value == null) {
			return false;
		}

		return value.matches("01(?:0|1|[6-9][,-]?(\\d{4})[.-]?(\\d{4})$");
	}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validateBy=TelephoneValidator.class)
public @interface Telephone {
	String message() default "전화번호 형식이 일치하지 않습니다.";
	Class[] groups() default {};
	Class[] payload() default {};
}
  • @Target : 어디서선언할 수 있는 정의
  • @Retention : 해당 어노테이션이 실제로 적용되고 유지되는 범위를 의미
    • Retention 적용 범위는 RetentionPolicy를 통해 지정가능
      • RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조합니다. 리플렉션이나로깅에 많이 사용하는 정책
      • RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유지합니다.
      • RetentionPolicy.SOURCE : 컴파일 전까지만 유지됩니다. 컴파일 이후에는 사라집니다.
  • @Constraint :  TelephoneValidator와 매핑하는 작업 수행
  • Telephone 인터페이스 내부 
    • message() : 유효성 검사를 실패할 경우 반환되는 메세지
    • groups() : 유효성 검사를 사용하는 그룹으로 설정
    • payload() : 사용자가 추가 정보를 위해 전달하는 값
// @Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
@Telephone
private String phoneNumber;
  • @Pattern 에서 @Telephone 으로 변경 후 테스트를 하면 아래와 같이 출력된다.