Skip to content

Latest commit

 

History

History
120 lines (93 loc) · 9.55 KB

File metadata and controls

120 lines (93 loc) · 9.55 KB

05. 어노테이션 메타데이터와 ASM

무엇을 / 왜

Spring의 컴포넌트 스캔은 묘한 요구를 가진다. @Component가 붙은 클래스를 찾아야 하는데, 그 클래스를 JVM에 로드하기 전에 판단해야 한다. 후보 클래스를 다 로드하면 (1) 메모리/시간 낭비, (2) 스캔만 하고 버릴 클래스의 정적 초기화 부작용, (3) 클래스패스에 없는 타입을 참조하는 클래스에서 NoClassDefFoundError가 터진다. 그래서 Spring은 .class 파일의 바이트코드를 직접 파싱해 어노테이션 메타데이터를 얻는다. 이때 쓰는 도구가 재패키징된 ASM이다.

또 하나의 문제는 어노테이션의 합성(merge) 이다. @PostMapping은 메타 어노테이션 @RequestMapping을 품고, @AliasFor로 속성 이름이 별칭으로 연결된다. 사용자는 "@RequestMappingpath 속성"을 묻는데 실제 값은 @PostMapping("/home")에 있다. core.annotationMergedAnnotations가 이 합성된 뷰를 제공한다.

소스: spring-core/src/main/java/org/springframework/core/annotation/, core/type/, core/type/classreading/, org/springframework/asm/

핵심 타입

   합성 어노테이션 모델 (core.annotation)
   ┌──────────────────────────────────────────────────────────────┐
   │ MergedAnnotations  ── 한 요소의 모든 (메타)어노테이션 컬렉션    │
   │   .get(type) → MergedAnnotation  ── 합성된 단일 어노테이션 뷰   │
   │ @AliasFor          ── 속성 별칭 선언                            │
   │ AnnotationTypeMappings / AnnotationTypeMapping ── 메타 매핑 계산 │
   │ AnnotatedElementUtils / AnnotationUtils ── 고수준 헬퍼          │
   └──────────────────────────────────────────────────────────────┘

   타입 메타데이터 추상 (core.type)
   ┌──────────────────────────────────────────────────────────────┐
   │ ClassMetadata        클래스 이름/추상/인터페이스/슈퍼클래스 등  │
   │ AnnotatedTypeMetadata 어노테이션 접근(getAnnotations 등)        │
   │ AnnotationMetadata = ClassMetadata + AnnotatedTypeMetadata      │
   │   ├─ StandardAnnotationMetadata  (리플렉션 기반: 이미 로드됨)    │
   │   └─ SimpleAnnotationMetadata    (ASM 기반: 로드 안 함)         │
   │ MethodMetadata                  메서드 단위 메타데이터          │
   └──────────────────────────────────────────────────────────────┘

   바이트코드 읽기 (core.type.classreading)
   ┌──────────────────────────────────────────────────────────────┐
   │ MetadataReader        한 .class 의 메타데이터 facade           │
   │ MetadataReaderFactory  className/Resource → MetadataReader      │
   │   ├─ SimpleMetadataReaderFactory                               │
   │   └─ CachingMetadataReaderFactory  (Resource 캐시)             │
   │ TypeFilter            스캔 매칭 술어 (core.type.filter)         │
   └──────────────────────────────────────────────────────────────┘

동작 흐름 1 — 클래스 로딩 없는 메타데이터 읽기

MetadataReaderFactory.getMetadataReader("com.example.Foo")가 컴포넌트 스캔의 입구다.

   getMetadataReader("com.example.Foo")
        │  ① className → "classpath:com/example/Foo.class" Resource 로 변환
        ▼
   ResourcePatternResolver / ClassLoader 로 .class Resource 획득 (1장)
        │
        ▼
   ASM ClassReader (org.springframework.asm) 가 바이트코드 방문
        │  SimpleAnnotationMetadataReadingVisitor 가 ClassVisitor 로 동작
        │  - 클래스 이름/슈퍼/인터페이스/플래그 수집 → ClassMetadata
        │  - 어노테이션 방문 시 MergedAnnotationReadingVisitor 가
        │    속성 값을 모아 MergedAnnotation 으로 재구성
        ▼
   SimpleMetadataReader { ClassMetadata, AnnotationMetadata } 반환
        │  (CachingMetadataReaderFactory 라면 Resource 별로 캐싱)
        ▼
   TypeFilter.match(reader, factory) 로 "@Component 인가?" 판정

Java 24+에서는 ASM 대신 표준 java.lang.classfile.ClassFile API를 쓸 수 있다(MetadataReader 주석이 명시). 어느 쪽이든 클래스를 로드하지 않고 메타데이터만 얻는다는 점이 핵심이다.

AnnotationMetadata가 두 구현을 갖는다는 게 설계의 묘미다. 스캔 단계에서는 ASM 기반 SimpleAnnotationMetadata(로드 안 함), 이미 로드된 클래스를 다룰 때는 리플렉션 기반 StandardAnnotationMetadata. 둘 다 같은 인터페이스라 상위 로직은 출처를 신경 쓸 필요가 없다.

동작 흐름 2 — @AliasFor 와 메타 어노테이션 합성

   @PostMapping("/home")  →  내부에 @RequestMapping(method=POST)
                              @PostMapping.value @AliasFor @RequestMapping.path …

   MergedAnnotations.from(element).get(RequestMapping.class)
        │
        ▼
   AnnotationTypeMappings: @PostMapping → @RequestMapping 메타 거리 계산,
   AliasFor 그래프로 (value↔path) 별칭 연결
        ▼
   MergedAnnotation<RequestMapping> (합성 뷰)
        path   = "/home"   ← @PostMapping("/home") 에서 끌어옴
        value  = "/home"   ← @AliasFor 로 동기화
        method = POST       ← 메타 어노테이션 기본값
        .getString("path"), .synthesize() 로 진짜 @RequestMapping 인스턴스화 가능

MergedAnnotation.synthesize()SynthesizedMergedAnnotationInvocationHandler(JDK 동적 프록시)를 써서, 별칭이 반영된 진짜 어노테이션 타입의 프록시 인스턴스를 만들어 준다. 즉 코드가 표준 RequestMapping 어노테이션처럼 다룰 수 있다.

SearchStrategy — 어디까지 뒤질 것인가

MergedAnnotations.search(strategy).from(element)의 전략이 탐색 범위를 정한다.

   DIRECT                 해당 요소에 직접 선언된 것만
   INHERITED_ANNOTATIONS  @Inherited 규칙까지 (Java 기본 상속)
   SUPERCLASS             슈퍼클래스까지
   TYPE_HIERARCHY         슈퍼클래스 + 구현 인터페이스 전체 (가장 넓음)

@Transactional이 인터페이스에 붙고 구현체에서 사용되는 식의 패턴을 잡으려면 TYPE_HIERARCHY가 필요하다.

핵심 메서드

  • MetadataReaderFactory.getMetadataReader(...): className 또는 Resource를 받아 MetadataReader를 만든다. CachingMetadataReaderFactoryDefaultResourceLoader의 리소스 캐시(1장)와 결합해 같은 .class를 두 번 파싱하지 않는다.
  • AnnotationMetadata.getAnnotationTypes() / hasAnnotation(name) / getAnnotationAttributes(name): 로드 없이 어노테이션 존재/속성을 질의. 디폴트 메서드 다수가 getAnnotations()(=MergedAnnotations) 위에 구현되어, 두 세계(type 메타데이터 ↔ 합성 어노테이션)를 잇는다.
  • AnnotatedElementUtils.findMergedAnnotation(element, type): 리플렉션 요소에서 합성 어노테이션을 한 줄로 얻는 고수준 헬퍼. 내부적으로 MergedAnnotations + TYPE_HIERARCHY를 쓴다.
  • TypeFilter.match(reader, factory): 스캔 술어. AnnotationTypeFilter, AssignableTypeFilter 등이 이 인터페이스를 구현해 "어떤 클래스를 후보로 채택할지"를 결정한다.

설계 포인트 / 확장점

  • 로드 회피 = ASM: "스캔은 많고 로드는 비싸다"는 제약이 ASM 기반 메타데이터 읽기를 낳았다. 컴포넌트 스캔, 조건부 설정(@Conditional), AOT 처리의 토대.
  • 합성 모델의 분리: MergedAnnotations는 의도적으로 "Spring 합성 어노테이션 전용"이다. 단순 표준 어노테이션 조회는 AnnotationUtils나 자바 리플렉션을 쓰라고 주석이 명시한다 — 책임 경계가 분명하다.
  • 이중 백엔드, 단일 인터페이스: AnnotationMetadata/MergedAnnotations가 ASM·리플렉션 어느 출처든 동일 API를 노출해 상위 코드가 출처 독립적이다.
  • @AliasFor 그래프: 명시적 별칭, 메타 어노테이션 별칭, 암묵적 별칭을 AnnotationTypeMapping이 그래프로 계산해 합성 뷰를 만든다. 잘못된 별칭 선언은 AnnotationConfigurationException으로 즉시 드러난다.

정리

이 영역은 "클래스를 로드하지 않고 어노테이션을 읽는다"와 "메타 어노테이션·별칭을 하나의 합성 뷰로 본다"는 두 난제를 푼다. ASM 기반 MetadataReader.class 바이트코드를 파싱해 AnnotationMetadata를 만들고, MergedAnnotations/@AliasFor가 메타 어노테이션 계층을 평탄화해 사용자에게 일관된 속성 뷰를 제공한다. 두 메커니즘은 출처(ASM/리플렉션)에 독립적인 인터페이스로 노출되어, 컴포넌트 스캔과 조건부 설정 전체의 기반이 된다.