• 이번 프로젝트 때, client에서 들어오는 신발 사이즈 값의 유효성 검증을 위해 custom annotation을 만들어 코드와 분리하고 가독성을 높여보았다 그 과정 기록하기
  • custom annotation의 간결하다는 장점이 있지만, 그만큼 로직 흐름이 응축되어 있기 때문에 그 흐름을 파악하기 어렵다는 양면성을 가진다는 생각이 들었다
    • 그렇지만, 관심사의 분리를 통해 비지니스 로직에 집중할 수 있게 해주는 장점에 초점을 맞추어 구현하였다
    • 굳이 코드 상으로 검증 로직을 넣고, 에러 처리를 해줄 필요가 없다고 생각했다
    • 의도와 목적을 명확히하여 팀원들과 공감대를 이룬 후 추가하였다
    • 다만, 무분별한 custom annotation 생성은 지양해야 한다고 생각한다
  • 추가로, annotation을 알아보면서 평소에 궁금했던 어떻게 @Component를 class에 붙이기만 하면 자동으로 Bean으로 등록될까?에 대해서도 알 수 있었다


Annotation

  • annotation의 본질적인 목적은 source code에 추가적인 정보인 meta data를 표현하는 것
  • 단순히 부가적인 표현 + reflection을 이용해 annotation 지정만으로 원하는 클래스 주입하기 등 가능


Built-in annotation

  • 자바에서는 제공하는 기본 annotation
    • @Override, @FunctionalInterface, @Deprecated 등


Meta annotation

  • 기본 annotation 외에 meta annotation
  • meta annotation을 이용해 custom annotation 만들 수 있음


1. @Retention
annotation의 지속시간 (어떤 시점까지 어노테이션이 영향을 미치는지 결정)
= anntation이 어떻게 저장될지 결정 (코드 or 클래스로 컴파일 or 런타임)

  • RetentionPolicy enum에서 참조 가능
    • RetentionPolicy.SOURCE : 컴파일 후에 정보들이 사라짐 (byte code에 기록되지 않음)
      • ex) @Override, @SuppressWarnings
    • RetentionPolicy.CLASS : 컴파일 타임 때만 .class 파일에 존재하고 런타임 때는 없어잠 (byte code에 기록됨)
      • default 값
      • byte code level에서 어떤 작업을 해야할 때 유용
      • Reflection 사용 불가능
    • RetentionPlicy.RUNTIME : 런타임시에도 .class 파일에 존재
      • custom annotation 만들 때 주로 사용
      • Reflection 사용 가능

    제목 없음


2. @Documented
문서에 annotation 정보 포함하겠다는 의미 (public contract에 나옴)


3. @Target
annotation 적용 가능한 위치 지정 (ElementType enum 사용)
default 값은 모든 대상

  • ElementType.FIELD: enum 상수를 포함한 field 정의에 사용
    = field에만 annotation 달 수 있음(다른부분에 어노테이션 사용하면 컴파일 에러)
  • ElementType.TYPE : class, interface, enum 정의에 사용
  • ElementType.METHOD: method 정의에 사용
  • ElementType.PARAMETER: 파라미터 정의에 사용
  • ElementType.CONSTRUCTOR: 생성자 정의에 사용
  • ElementType.TYPE_USE: 자바 8이상부터, 타입에 사용


4. @Inherited
하위 클래스가 이 annotation을 상속 받을 수 있게 하겠다는 의미


5. @Repeatable
이 annotion을 같은 곳에서 중복해 사용 가능하다는 의미 (value에 여러 값을 넣고 싶을 때)



Custom Annotation

annotation을 내가 만들어서도 쓸 수 있다

  • annotation 정보를 얻고 싶으면, reflection만 이용해서 얻을 수 있음


custom annotation 구현하기

ShoeSize annotation

@Documented   //문서에 표시하겠다
@Retention(RetentionPolicy.RUNTIME)   //런타임에서도 적용되게 하겠다    
@Target({ElementType.FIELD, ElementType.TYPE_USE})   //필드와 타입에만 적용하겠다    
@Constraint(validatedBy = {ShoeSizeValidator.class})   //유효성 검증을 위해 넣었다  
public @interface ShoeSize {

	String message() default "";

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

	Class<? extends Payload>[] payload() default {};
}
  • annotation type은 @interface로 정의해야함
    • 자동적으로 java.lang.Annotation interface 상속받음
      -> 다른 class 상속 받을 수 없음


  • JSR-303 표준 어노테이션들이 갖는 3가지 공통 속성들
    • message : 유효하지 않을 경우 반환할 메세지 정의
    • groups : 유효성 검증이 진행될 그룹 정의 = 상황별 validation 제어가능
      • ex) insert 할 때와 update할 때, validation을 구분해서 실행하고 싶을 때 사용
    • payload : 유효성 검증 시에 전달할 메타 정보 및 제약 사항 정의
      • ex) 해당 유효성 검증의 중요도



ShoeSizeValidator.java

  • ConstraintValidator 상속 + isValid() override해서 값이 유효한지 판단하는 로직 작성
    • 신발 사이즈에 대한 유효성 검증이기 때문에 입력값의 크기가 0초과 400이하인지와 5단위인지 체크했다
public class ShoeSizeValidator implements ConstraintValidator<ShoeSize, Integer> {

	@Override
	public boolean isValid(Integer value, ConstraintValidatorContext context) {
		return isValidSizeRange(value) && isValidSizeUnits(value);
	}

	private boolean isValidSizeRange(int value) {
		return value > 0 && value <= 400;
	}

	private boolean isValidSizeUnits(int value) {
		return value % 5 == 0;
	}
}



사용 : ProductRegisterRequest.java

public record ProductRegisterRequest(
  ...

  List<@ShoeSize(message = "잘못된 형식의 신발 사이즈가 입력되었습니다.") Integer> sizes) {
}



Spring의 Annotation

Spring에서 application 실행 시, @Component가 붙은 클래스들을 스캔해서 IoC 컨테이너에 등록해줌
어떻게? 어떤 과정으로 되는걸까?


@Component

Component annotation

@Target({ElementType.TYPE})    //TYPE : Class나 Interface에 적용하겠다 = 타겟으로 삼겠다 
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}


Service annotation

  • @Service를 붙여도 스캔됨
    • Component annotation 사용중이기 때문에
@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented
@Component  
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}


ClassPathBeanDefinitionScanner.java의 doScan()

  • 스캔해서 bean으로 등록해주는 Spring의 코드
    • ClassPath에 있는 package의 모든 class를 읽어, annotation이 붙은 class를 처리(ex) IoC 컨테이너에 클래스 등록)해줌

    doscan


  • doScan method

    //doScan()
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    
    for (String basePackage : basePackages) {
      //basePackage에 속한 모든 리소스 파일 읽어옴   
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);  //아래 코드블럭 참고   
    
    • findCandidateComponents method

      public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
          @Nullable   //어떤 메타 데이터가 있을 때 채워지는 변수  
          private CandidateComponentsIndex componentsIndex;
          ...
      
          public Set<BeanDefinition> findCandidateComponents(String basePackage) {
          if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
            return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);  //아래 코드블럭 참고 
          }
          else {
            return scanCandidateComponents(basePackage);  //아래 코드블럭 참고   
          }
        }
          ...
      }
      
    • addCandidateComponentsFromIndex, scanCandidateComponents 메서드에 존재하는 isCandidateComponent 메서드

      //Component annotation 인지 판별 - 맞으면 filter 통과해서 bean으로 등록   
      protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        for (TypeFilter tf : this.excludeFilters) {
          if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return false;
          }
        }
        for (TypeFilter tf : this.includeFilters) {
          if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return isConditionMatch(metadataReader);
          }
        }
        return false;
      }
      


  • doScan method 이어서

      for (BeanDefinition candidate : candidates) {   
        //bean의 Scope이 싱글톤인지 프로토타입인지 판별  
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);     
        candidate.setScope(scopeMetadata.getScopeName());
        //bean 이름 결정(beanNameGenerator 사용 - 아래 코드블럭 참고)
        String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
        if (candidate instanceof AbstractBeanDefinition) {
          postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
        }
        if (candidate instanceof AnnotatedBeanDefinition) {
          AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
        }
    
    • BeanNameGenerator

      private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;   
      


  • doScan method 이어서

    if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);   
        definitionHolder =
            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);  
        //BeanDefinitionHolder로 만든 후 BeanDefinition로 만듬   
        beanDefinitions.add(definitionHolder);  
        registerBeanDefinition(definitionHolder, this.registry); // 만든걸 빈팩토리에 bean 등록(registry에 등록)  
    }
    



Reference
https://jdm.kr/blog/216
https://en.wikipedia.org/wiki/Java_annotation
https://donghyeon.dev/spring/2020/08/18/Spring-Annotation%EC%9D%98-%EC%9B%90%EB%A6%AC%EC%99%80-Custom-Annotation-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0/
https://mangkyu.tistory.com/206
https://ittrue.tistory.com/158
https://tecoble.techcourse.co.kr/post/2021-06-21-custom-annotation/
https://techblog.woowahan.com/2684/
https://pamyferret.tistory.com/65
https://minkukjo.github.io/framework/2020/07/09/Spring-133/


업데이트: