Spring의 컴포넌트 스캔은 묘한 요구를 가진다. @Component가 붙은 클래스를 찾아야 하는데, 그 클래스를 JVM에 로드하기 전에 판단해야 한다. 후보 클래스를 다 로드하면 (1) 메모리/시간 낭비, (2) 스캔만 하고 버릴 클래스의 정적 초기화 부작용, (3) 클래스패스에 없는 타입을 참조하는 클래스에서 NoClassDefFoundError가 터진다. 그래서 Spring은 .class 파일의 바이트코드를 직접 파싱해 어노테이션 메타데이터를 얻는다. 이때 쓰는 도구가 재패키징된 ASM이다.
또 하나의 문제는 어노테이션의 합성(merge) 이다. @PostMapping은 메타 어노테이션 @RequestMapping을 품고, @AliasFor로 속성 이름이 별칭으로 연결된다. 사용자는 "@RequestMapping의 path 속성"을 묻는데 실제 값은 @PostMapping("/home")에 있다. core.annotation의 MergedAnnotations가 이 합성된 뷰를 제공한다.
소스: 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) │
└──────────────────────────────────────────────────────────────┘
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.ClassFileAPI를 쓸 수 있다(MetadataReader주석이 명시). 어느 쪽이든 클래스를 로드하지 않고 메타데이터만 얻는다는 점이 핵심이다.
AnnotationMetadata가 두 구현을 갖는다는 게 설계의 묘미다. 스캔 단계에서는 ASM 기반 SimpleAnnotationMetadata(로드 안 함), 이미 로드된 클래스를 다룰 때는 리플렉션 기반 StandardAnnotationMetadata. 둘 다 같은 인터페이스라 상위 로직은 출처를 신경 쓸 필요가 없다.
@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 어노테이션처럼 다룰 수 있다.
MergedAnnotations.search(strategy).from(element)의 전략이 탐색 범위를 정한다.
DIRECT 해당 요소에 직접 선언된 것만
INHERITED_ANNOTATIONS @Inherited 규칙까지 (Java 기본 상속)
SUPERCLASS 슈퍼클래스까지
TYPE_HIERARCHY 슈퍼클래스 + 구현 인터페이스 전체 (가장 넓음)
@Transactional이 인터페이스에 붙고 구현체에서 사용되는 식의 패턴을 잡으려면 TYPE_HIERARCHY가 필요하다.
MetadataReaderFactory.getMetadataReader(...): className 또는Resource를 받아MetadataReader를 만든다.CachingMetadataReaderFactory는DefaultResourceLoader의 리소스 캐시(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/리플렉션)에 독립적인 인터페이스로 노출되어, 컴포넌트 스캔과 조건부 설정 전체의 기반이 된다.