-
Spring Validation백엔드 2023. 9. 13. 11:45728x90반응형SMALL
Validation
Java에서는 null값에 대한 접근을 할 때 null pointer exception이 발생합니다. 이러한 부분을 방지하기 위해 미리 검증을 하는 과정이 필요한데 이를 validation이라고 합니다.
예시
public void service(String account, String password, int age){ if(account==null || password==null){ return ... } if (age == 0){ return ... } // 정상 로직 시작 }
Validation은 검증해야 할 값이 많은 경우 코드의 길이가 많이 길어지게 됩니다.
구현에 따라서 달라질 수 있지만 Service 로직과 분리가 필요합니다.
흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있습니다.
구현에 따라 달라질 수 있지만, 검증 로직이 변경되는 경우 테스트 코드 등 참조하는 클래스에서 로직이 변경되어야 하는 부분이 발생할 수 있습니다.
Validation 관련 annotation
유효성 검증을 위해 Spring에서는 일관된 어노테이션을 많이 활용하게 됩니다.
원하는 변수에 어노테이션을 추가해 설정을 해주면 해당 유효성 검사를 할 수 있습니다.
@Size 문자 길이 측정 (Int Type 불가) @NotNull null 불가 @NotEmpty null, "" 불가 @NotBlank null, "", " " 불가 @Past 과거 날짜 @PastOrPresent 오늘이거나 과거 날짜 @Future 미래 날짜 @FutureOrPresent 오늘이거나 미래 날짜 @Pattern 정규식 적용 @Max 최대값 @Min 최소값 @AssertTrue / False 별도 Logic 적용 @Valid 해당 object의 validation을 실행 Validation dependency 추가
dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' }
Validation 실험 객체
@Data public class user{ private String name; private int age; private String email; private String phoneNumber; private String reqYearMonth; // yyyyMM }
@Email 어노테이션
email 형식에 맞지 않으면 Validation 검사에서 에러가 발생하게 됩니다.
public class User{ ... @Email private String email; }
Validation 검증을 받고 싶은 객체의 경우 @Valid 어노테이션을 앞에 붙여주어야 합니다.
@PostMapping("/user") public ResponseEntity user(@Valid @RequestBody User user){ return ResponseEntity.ok(user); }
만약 현재 controller 메서드에 User 매개변수의 email이 email 형식에 맞지 않은 상태로 도착을 하게 되면 에러가 발생하게 되고 400에러를 반환한게 됩니다.
@Pattern 어노테이션
public class User{ ... @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$") private String phoneNumber; }
@Pattern 어노테이션을 붙여주게 되고 해당 정규식을 항상 검증하게 됩니다.
이처럼 어노테이션을 통해 유효성을 검증하게 되면, 맨 위의 if문을 나열하는 것과 같은 로직이 필요없게 됩니다.
BindingResult
controller 메서드에 BindingResult 인자를 추가해주면, 만약 유효성 검증에서 에러가 발생하는 경우 바로 에러를 반환하지 않고 예외 처리에 대한 결과가 bindingResult에 들어오게 됩니다.
bindingResult 내에 유효성 검증에 실패한 속성에 대한 설명이 모두 포함되어 있으므로 이 객체를 통해 따로 에러에 대한 핸들링을 할 수 있도록 해줍니다.
@PostMapping("/user") public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){ if(bindingResult.hasErrors()){ bindingResult.getAllErrors().forEach(objectError -> { FieldError field = (FieldError) objectError; String message = objectErrorl.getDefaultMessage(); System.out.println(field.getField()); System.out.println(message); }) } }
위와 같은 상황에서 phoneNumber의 유효성 검사가 실패할 경우 다음과 같이 출력됩니다.
phoneNumber "^\\d{2,3}-\\d{3,4}-\\d{4}$"와 일치해야 합니다.
message와 같은 경우 @Pattern 어노테이션의 default message가 출력됩니다. 이를 커스텀해서 출력할 수도 있습니다.
message
public class User{ ... @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message="핸드폰 번호 양식은 xxx-xxxx-xxxx입니다.") private String phoneNumber; }
각 어노테이션 속성 중 message를 통해 오류가 발생했을 때 나타낼 메시지를 지정해줄 수 있습니다.
이와 같이 BindingResult 객체를 통해 실제 발생하는 Validation 관련 오류를 직접 핸들링해서 반환할 수 있습니다.
AssertTrue / False
User 클래스의 reqYearMonth와 같은 경우 최소 6자리 최대 6자리이므로 다음과 같은 validation을 추가해주도록 하겠습니다.
public class User{ ... @Size(min = 6, max = 6) private String reqYearMonth; // yyyyMM }
하지만 위와 같은 경우 6자리의 어떤 문자열이든 입력이 가능한 상태이므로 다른 형식으로 유효성 검사를 해주어야 합니다.
public class User{ ... @Size(min = 6, max = 6) private String reqYearMonth; @AssertTrue public boolean isReqYearMonthValidation(){ try{ LocalDate localDate = LocalDate.parse(this.reqYearMonth+"01", DateTimeFormatter.ofPattern("yyyyMMdd")); } catch(Exception e){ return false; } return true; } }
AssertTrue 메서드는 true를 반환하면 Validation을 통과한다는 뜻입니다. 해당 어노테이션이 붙은 메서드 명은 is로 시작해야합니다.
위와 같은 경우 User 객체 내부에 선언되어 있으므로 재사용이 불가능합니다. 그러므로 이를 새로운 Validation 어노테이션으로 만들어서 사용해보도록 하겠습니다.
Validation annotation 추가
원래 있었던 @Email 어노테이션을 살펴보도록 하겠습니다.
@Documented @Constraint(validatedBy = { }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) public @interface Email { String message() default "{javax.validation.constraints.Email.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /** * @return an additional regular expression the annotated element must match. The default * is any string ('.*') */ String regexp() default ".*"; /** * @return used in combination with {@link #regexp()} in order to specify a regular * expression option */ Pattern.Flag[] flags() default { }; /** * Defines several {@code @Email} constraints on the same element. * * @see Email */ @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented public @interface List { Email[] value(); } }
@Target과 @Retention 이 필요할 것으로 보이고 @Constraint를 통해 지정된 validator를 통해 검사하는 어노테이션을 만들어보도록 하겠습니다.
@Constraint(validatedBy = { }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) public @interface YearMonth(){ String message() default "{javax.validation.constraints.Email.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; String pattern() default "yyyyMMdd"; }
위와 같이 Validation 어노테이션에 필요한 디폴트값들을 복사해줍니다.
가장 아래 pattern으로 해당 어노테이션을 통해 유효성 검사를 할 때 어떤 형태의 validation을 사용할 것인지 지정해줄 수 있도록 해주었습니다. (여기서는 default값을 "yyyyMMdd"을 넣어주었습니다.)
User에 어노테이션 추가
public class User{ ... @YearMonth private String reqYearMonth; }
Validator 추가
public class YearMonthValidator implements ConstraintValidator<YearMonth, String>{ private String pattern; @Override public void initialize(YearMonth constraintAnnotation) { this.pattern = constraintAnnotation.pattern(); } @Override public boolean isValid(String value, ConstraintValidatorContext context){ try{ LocalDate localDate = LocalDate.parse(value+"01", DateTimeFormatter.ofPattern(this.pattern)); } catch(Exception e){ return false; } return true; } }
ConstraintValidator의 첫번째 제너릭에 들어갈 것은 우리가 만든 Validation 어노테이션이고 두 번재 제너릭은 pattern의 타입을 정해주면 됩니다.
Validator 내에 pattern 변수를 선언해주고 override한 initialize 메서드 내에서 annotation의 pattern을 받을 수 있도록 해주었습니다.
이 후 isValid 메서드 내에 변수가 해당 pattern을 따르고 있는지 확인을 해주게 됩니다.
이제 이 Validator는 YearMonth 어노테이션을 붙인 변수의 유효성 검사를 하게됩니다.
AssertTrue / False 는 메서드를 정의해서 커스텀 로직으로 유효성 검사가 가능하지만 재사용이 불가능합니다.
ConstraintValidator를 상속받아 custom 어노테이션을 정의하게 되면 재사용이 가능합니다.
반응형LIST'백엔드' 카테고리의 다른 글
Rest Template으로 Server(Client) to Server 통신하기 (0) 2023.09.15 Spring filter, interceptor (0) 2023.09.14 Jpa Embedded (0) 2023.09.06 JPA @Query, Native Query, Converter (0) 2023.09.05 JPA Cascade, OrphanRemoval (0) 2023.09.04