어노테이션 만들기기 / 검증

ys·2024년 5월 24일

Spring공부

목록 보기
8/14

1. 어노테이션이란?

  • java 5(1.5)부터 등장한 기능
  • 프로그램에 추가적인 정보를 제공하는 메타 데이터
  • 여기서 메타 데이터란 어플리케이션이 처리해야 할 데이터가 아니라 컴파일 과정과 런타임에서 코드를 어떻게 컴파일하고 처리할 것인지에 대한 정보를 말한다
  • 비즈니스 로직과 분리하여 대상의 벨리데이션 체크, 값 주입, 역할 부여(기능 주입) 등을 수행할 수 있어 체계가 잡혀있는 깔끔한 코드를 작성할 수 있게 된다
  • 또한, 검증 로직을, 데이터를 전달하는 DTO에 두는게 아니어서 SRP 원칙또한 잘 지킬 수 있다
  • Java의 리플렉션(실행중인 자바 클래스의 정보를 가져오는 기능)을 사용하여 런타임 시기에 어노테이션의 정보를 바탕으로 다양한 기능을 수행할 수 있으므로
  • 어노테이션은 AOP(관점지향 프로그래밍)을 구성하는 데에 많은 도움을 줄 수 있다.

어노테이션의 종류

built - in 어노테이션

  • Java 코드에 적용되는 어노테이션
  • @Overrie, @Deprecated, @SuppressWarnings 등이 존재

meta 어노테이션

  • 다른 어노테이션에 적용되기 위한 어노테이션
  • @Retention, @Documneted, @Target, @Inherited, @Repeatable 등이 존재

Meta 어노테이션

  • 우리는 이번에 Meta 어노테이션을 알아 볼 것이다

Retention

  • 해당 어노테이션의 정보를 어느 범위까지 유지할 것인지를 설정함
  • RetentionPolicy.SOURCE: 컴파일 전까지만 유효하며 컴파일 이후에는 사라짐
  • RetentionPolicy.CLASS: 컴파일러가 클래스를 참조할 때까지 유효함
  • RetentionPolicy.RUNTIME: Reflection을 사용하여 컴파일 이후에도 JVM에 의해 계속 참조가 가능함

Documented

  • JavaDoc 생성 시 Document에 포함되도록 함

Traget

  • 해당 어노테이션이 사용되는 위치를 결정

ElementType.PACKAGE : 패키지 선언시
ElementType.TYPE : 타입 선언시
ElementType.CONSTRUCTOR : 생성자 선언시
ElementType.FIELD : 맴버 변수 선언시
ElementType.METHOD : 메소드 선언시
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언시
ElementType.LOCAL_VARIABLE : 지역 변수 선언시
ElementType.TYPE_PARAMETER : 매개 변수 타입 선언시


2. 어노테이션 생성

1. 어노테이션 생성

2. 메타 어노테이션 추가

@Target({ElementType.FIELD}) 
@Retention(RetentionPolicy.RUNTIME)  
public @interface PhoneNumber {...}
  • 컴파일 이후에도 JVM에 의해 계속 참조가 가능하도록 RetentionPolicy.RUNTIME 을 적용하였다
  • 또한 Field 선언시 사용하도록 ElementType.FIELD를 적용하였다
  • 여러 타겟을 원하는 경우에는 @Target({ElementType.TYPE, ElementType.Field}) 와 같이 사용하면 된다.

3. 변수 추가

@Target({ElementType.FIELD}) 
@Retention(RetentionPolicy.RUNTIME)  
public @interface PhoneNumber {
	String name() default "010-0000-0000"
    }
  • 이렇게 name()을 변수로 지정해줄 수 있다
  • 그리고 기본값은 010-0000-0000"으로 지정했다

4. 적용하기

  • 우리는 Field 타입으로 지정했기 때문에, 아무 field에 @PhoneNumber로 어노테이션을 붙여주고
  • 그변수명.name()을 해주면, default로 지정한 "010-0000-0000"이 반환된다

3. 🤔@Constraint로 검증하기

  • @Constraint 어노테이션을 활용하면 사용자가 원하는 Constraint와 Validation을 만들어 이를 적용할 수 있다
  • 프로젝트 진행 중, 핸드폰 번호를 정규식으로 검증하고 있다!
  • 다른 DTO에서도 해당 정규식을 사용할려면, 정규식을 알아야 하는 불편함과 코드를 복사 붙여넣기 해야하는 문제가 있다...
  • 그래서 이 검증을 @Constraint 를 활용한 어노테이션으로 만들어 검증하겠다!

A. PhoneNumber

@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 { };
}
  • 먼저, meta 어노테이션인 @Target과 @Retention을 정의해 준다
  • 그 다음 기본 message() 를 지정해준다
  • 그 다음, 우리가 사용할 정규식을 정해준다. 여기서는 @Pattern 메서드의 파라미터인 regexp와 동일하게 지정해 뜻을 명확하게 표현하였다
  • 이렇게만 지정하면 오류가 나므로 groups(),payload()도 지정해주었다...
  • 🤔 이제 @Constraint를 봐보자!!

해석

  • 제약 조건을 구현하는 ConstraintValidator 클래스입니다.
  • 지정된 클래스는 지정된 ValidationTarget에 대한 고유한 대상 유형을 참조해야 합니다.
  • 두 ConstraintValidator가 동일한 유형을 참조하는 경우 예외가 발생합니다.
  • 메소드 또는 생성자의 매개변수 배열(교차 매개변수라고도 함)을 대상으로 하는 - ConstraintValidator는 최대 하나가 허용됩니다. 둘 이상이면 예외가 발생합니다.
  • 반환:
  • 제약 조건을 구현하는 ConstraintValidator 클래스 배열
  • @Constraint의 validatedBy 값으로 우리가 아래에서 정의할 validator를 전달해주면 Spring Boot는 우리가 API를 호출할 때 전달한 값을 가져오면서 validation을 수행한다!

B. PhoneNumberValidator

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) {
        // value : phoneNumber 이 들어옴
        boolean result = Pattern.matches(regexp,value);
        return result;
    }
}
  • ConstraintValidator를 상속받는 PhoneNumberValidator를 만들자
  • 제네릭으론, <어노테이션, 타입>을 받는다
  • initialize로, 어노테에션으로 우리가 필요한 정규식 조건인 regexp을 저장해주자
  • isValid의 value값으로 우리가 어노테이션을 달아준 field의 값이 들어온다
  • isValid로 bool값을 반환하는데, 여기선 우리의 입력받은 값과, 정규식 조건과 같은지를 반환한다!
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
  • 다음 식과 같은 의미이다
  • 마지막으로, ✅ @Constraint(validatedBy = {PhoneNumberValidator.class})의 매개변수에 우리가 어떤 클래스로 검증할건지 명시해주면 끝이다!

C. 실행 예시

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRegisterRequest {

    private String name;

    private String nickName;

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

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

    @Email
    private String email;

    @PhoneNumber
    private String phoneNumber;

    @FutureOrPresent // 현재 or 미래
    private LocalDateTime registerAt;
    @AssertTrue(message = "name or nickName 중 반듯이 1개가 존재해야 합니다") // 해당 리턴값이 true일 때 실행하는 어노테이션, 반듯이 is라는 메서드에 붙여줘야 한다
    public boolean isNameCheck(){
        if (Objects.nonNull(name) && !name.isBlank()){
            return true;
        }
        if (Objects.nonNull(nickName) && !nickName.isBlank()){
            return true;
        }
        return false;
    }
}
  • 다음과 같이 Api 요청을 보낸다
{
  "result_code" : "",
  "result_message" : "",
  "data" : {
    "name" : "",
    "nick_name" : "홍길동",
    "password" : "qwer",
    "age" : 20,
    "email" : "hong@gmail.com",
    "phone_number" : "010-11112222",
    "register_at" : "2024-06-17T09:09:09"
  }  ,
  "error" : {
    "error_message" : [
    ]
  }
}
  • 딱봐도 phone-number의 타입이 틀린 것을 볼 수 있다
  • 원하는데로, 🤔어노테이션을 가지고 검증이 잘 이뤄진 것을 볼 수 있다!!!

참고!

profile
개발 공부,정리

0개의 댓글