프로젝트를 진행하며 입력값을 받을 때 프론트에서도 검증을 하고 보내주겠지만, 비지니스 로직을 보호하기 위해 서버단에서도 검증을 해주는게 안전하다고 배웠다. 그걸 할 수 있게 해주는게 @Valid라고 한다.

 

@Valid란

요청 데이터 유효성 검증을 위해 사용하는 어노테이션

DTO에 선언된 제약 조건을 기반으로 입력값을 검증해줌

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRegisterRequest {
    @NotBlank
    private String username;

    private String name;

    @Size(min = 1, max = 12)
    @NotBlank
    private String password;

    @NotNull
    @Min(1)
    @Max(100)
    private Integer age;

    @Email
    private String email;

    @PhoneNumber
    //@Pattern(regexp = "^\\\\d{2,3}-\\\\d{3,4}-\\\\d{4}$", message = "휴대폰 번호 양식에 맞지 않습니다.")
    private String phoneNumber;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    @FutureOrPresent
    private LocalDateTime registerAt;

    //해당 메서드가 true 여야 검증이 통과
    @AssertTrue(message = "name or username은 존재해야 합니다.")
    public boolean isNameCheck(){
        if(Objects.nonNull(this.name) && !this.name.isBlank()){
            return true;
        }
        if(Objects.nonNull(this.username) && !this.username.isBlank()){
            return true;
        }
        return false;
    }

}

해당 코드에서

@Blank, @NotNull, @Pattern 등 필드값에 제약 조건을 건다.

그 후 입력값을 받아오는 Controller에서 검증이 필요한 입력값 앞에 @Valid를 사용한다.

@PostMapping("")
    public Api<UserRegisterRequest> register(
            @Valid // 입력값 검증 들어감
            @RequestBody
            Api<UserRegisterRequest> userRegisterRequest
    ) {
        log.info("init : {}", userRegisterRequest);

        var body = userRegisterRequest.getData();
        Api<UserRegisterRequest> response = Api.<UserRegisterRequest>builder()
                .data(body)
                .resultCode(String.valueOf(HttpStatus.OK.value()))
                .resultMessage(HttpStatus.OK.name())
                .build();
        return response;
    }

자주 사용되는 검증 어노테이션 정리

  • @NotNull: 널이 아닌 값
  • @NotBlank: NotNull + 빈문자열, 공백문자열 아닌 값(String)
  • @NotEmpty: String 이외에도 Collection, Array 와 같이 빈 리스트가 아닌 값
  • @Size(min=, max=): 길이 제한
  • @Pattern(regexp=): 정규식 패턴 만족
  • @Email: 이메일 형식
  • @Min/Max(value=): 숫자 타입 값 검증
  • @AssertTrue/False: 해당 메서드가 True 혹은 False여야 됨, 검증로직을 만들 수 있음
  • @Past/Future: LocalDate의 시간이 현재보다 과거/미래 검증

여기서 이 어노테이션은 검증 트리거일 뿐

@Constraint(validatedBy = PhoneNumberValidator.class) //검증 클래스 추가
@Target({ElementType.FIELD})//어디에 적용할거냐
@Retention(RetentionPolicy.RUNTIME) //언제 실행시킬거냐
public @interface PhoneNumber {
    String message() default "핸드 번호 양식에 맞지 않습니다. ex) 000-0000-0000";
    String regexp() default "^\\\\d{2,3}-\\\\d{3,4}-\\\\d{4}$";

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

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

이렇게 커스텀으로 만들 수도 있다.

어노테이션을 만들고, 그 어노테이션을 검증할 클래스를 만들면 해당 어노테이션이 붙은 필드를 검사하는 것이다.

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    private String regexp;

    @Override //정규식 패턴 초기화
    public void initialize(PhoneNumber constraintAnnotation) {
        this.regexp = constraintAnnotation.regexp();
    }

    @Override //입력받은 값과 정규식이 일치하는지 검사하는 메서드
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return Pattern.matches(regexp, value);
    }
}

다른 어노테이션도 동일하게 동작한다.

 

마무리

오늘은 필드 검증을 위한 자주 사용하는 어노테이션을 알아봤고, 어떻게 커스텀 어노테이션을 만드는지도 알아봤다.

검증을 철저히해 잘못된 입력을 받아 로직을 돌다가 에러가 나는 걸 막고,

빠르게 검증으로 막을 수 있어 서버에 부담을 줄일 수 있는 좋은 방식인 것 같다.

+ Recent posts