Skip to content

Latest commit

 

History

History
110 lines (79 loc) · 9.32 KB

File metadata and controls

110 lines (79 loc) · 9.32 KB

06. 바이트코드 재패키징 — ASM · CGLIB · Objenesis

무엇을 / 왜

spring-core 안에는 Spring이 직접 작성하지 않은 코드가 상당량 들어 있다. org.springframework.asm, org.springframework.cglib, org.springframework.objenesis, org.springframework.javapoet은 각각 외부 라이브러리(ASM, CGLIB, Objenesis, JavaPoet)를 org.springframework.* 네임스페이스로 재패키징(shade) 한 것이다.

왜 굳이? 각 패키지의 package-info.java가 같은 이유를 반복해 말한다(직접 인용).

"This repackaging technique avoids any potential conflicts with dependencies on [그 라이브러리] at the application level or from third-party libraries."

버전 충돌 회피다. 애플리케이션이 자기 용도로 ASM 9.4를, Spring은 내부적으로 ASM 9.x를 쓴다면 클래스패스에서 충돌한다. 패키지 이름을 바꿔 재패키징하면 둘은 완전히 다른 클래스가 되어 공존한다. 그리고 이 라이브러리들은 "for internal use only" — 애플리케이션 코드가 직접 의존할 API가 아니라, Spring 내부 메커니즘의 부품이다.

소스: spring-core/src/main/java/org/springframework/{asm,cglib,objenesis,javapoet}/

세 라이브러리의 역할

   ┌──────────┬─────────────────────────────┬────────────────────────────────┐
   │ 라이브러리 │ 본업                         │ Spring 안에서의 쓰임             │
   ├──────────┼─────────────────────────────┼────────────────────────────────┤
   │ ASM 9.x  │ .class 바이트코드 읽기/쓰기   │ ① 클래스 로딩 없는 메타데이터    │
   │          │  (ClassReader/ClassWriter)   │    읽기 (5장 MetadataReader)     │
   │          │                              │ ② CGLIB 가 클래스 생성에 ASM 사용 │
   ├──────────┼─────────────────────────────┼────────────────────────────────┤
   │ CGLIB 3.3│ 런타임 서브클래스 생성        │ 인터페이스 없는 빈의 AOP 프록시,  │
   │          │  (Enhancer/MethodInterceptor)│ @Configuration 클래스 향상       │
   │          │                              │ (CGLIB → ASM 으로 바이트코드 출력)│
   ├──────────┼─────────────────────────────┼────────────────────────────────┤
   │Objenesis │ 생성자 호출 없이 인스턴스화    │ 프록시/모의 객체를 기본 생성자    │
   │ 3.5      │                              │ 없이 만들 때 (SpringObjenesis)    │
   ├──────────┼─────────────────────────────┼────────────────────────────────┤
   │JavaPoet  │ 자바 소스 코드 생성           │ AOT 코드 생성(aot.generate)에서   │
   │          │                              │ 참조 (현재 패키지는 package-info  │
   │          │                              │ 만 존재 — 위치 예약/문서화)        │
   └──────────┴─────────────────────────────┴────────────────────────────────┘

세 라이브러리는 사슬로 협력한다. CGLIB이 프록시 클래스를 만들 때 내부적으로 ASM의 ClassWriter로 바이트코드를 출력하고, 그렇게 만든 클래스를 인스턴스화할 때 생성자를 우회해야 하면 Objenesis를 쓴다.

ASM — 두 방향의 바이트코드

ASM은 방문자(Visitor) 패턴 기반의 바이트코드 라이브러리다. 두 방향이 모두 Spring에서 쓰인다.

   읽기:  .class bytes ──▶ ClassReader.accept(ClassVisitor)
                          │  visit/visitAnnotation/visitMethod … 콜백
                          ▼
                    (5장) SimpleAnnotationMetadataReadingVisitor
                          → AnnotationMetadata 구성  (클래스 로드 X)

   쓰기:  ClassWriter ──▶ visitMethod/visitField … 호출로 새 클래스 조립
                          ▼
                    byte[] ──▶ ClassLoader.defineClass (CGLIB 경유)

재패키징의 흔적이 SpringAsmInfo다. 이 클래스는 Spring이 사용하는 ASM API 레벨 상수를 한 곳에 모아, 내부 코드가 일관된 버전으로 ASM을 호출하게 한다.

CGLIB — 런타임 서브클래스 프록시

JDK 동적 프록시는 인터페이스만 프록시할 수 있다. 인터페이스 없는 구체 클래스(예: @Service class OrderService)에 AOP를 걸려면 런타임에 그 클래스의 서브클래스를 생성해 메서드를 가로채야 한다. 이것이 CGLIB Enhancer의 일이다.

   Enhancer
     .setSuperclass(OrderService.class)
     .setCallback(MethodInterceptor)        // 메서드 호출 가로채기
     .create()
        │  ASM ClassWriter 로 OrderService$$SpringCGLIB$$0 바이트코드 생성
        ▼
   OrderService 의 서브클래스 인스턴스 (메서드 호출이 인터셉터로 라우팅)

Spring은 두 가지 재패키징 흔적을 남긴다.

  • SpringNamingPolicy — 생성되는 프록시 클래스의 이름 규칙(...$$SpringCGLIB$$...)을 정해, 표준 CGLIB 산출물과 구분하고 디버깅을 돕는다.
  • SpringCglibInfo — Spring 재패키징 버전임을 표시하는 마커.

@Configuration 클래스도 CGLIB으로 향상된다. @Bean 메서드 간 호출이 싱글톤을 보장하도록 메서드를 가로채야 하기 때문이다.

Objenesis — 생성자 없는 인스턴스화

프록시 서브클래스를 만들었는데 원본 클래스에 적당한 기본 생성자가 없거나, 생성자 부작용을 피하고 싶을 때가 있다. Objenesis는 JVM별 트릭으로 생성자를 전혀 호출하지 않고 인스턴스를 만든다. Spring은 이를 SpringObjenesis로 감싼다.

// SpringObjenesis — 내부 진입점
public class SpringObjenesis implements Objenesis {
    public static final String IGNORE_OBJENESIS_PROPERTY_NAME = "spring.objenesis.ignore";
    public boolean isWorthTrying() { ... }   // 환경에서 동작 가능한지 사전 판단
    public <T> T newInstance(Class<T> clazz) { ... }
}

isWorthTrying()이 인상적이다 — GraalVM 네이티브 이미지처럼 Objenesis 트릭이 통하지 않는 환경에서는 시도 자체를 건너뛰도록 미리 판단한다. spring.objenesis.ignore 프로퍼티로 강제 비활성화도 가능하다. 즉 재패키징에 더해 Spring 환경에 맞춘 안전 가드를 입힌 것이다.

이들과 연결되는 core의 보조 타입

재패키징 라이브러리는 core 루트의 몇몇 인터페이스와 맞물린다.

  • DecoratingProxy — 프록시가 "내가 실제로 감싸는 원본 클래스"(getDecoratedClass())를 알리는 인터페이스. 2장에서 컨버터 시그니처를 추출할 때, 프록시면 원본 클래스로 다시 시도하던 그 메커니즘이다.
  • SmartClassLoader / OverridingClassLoader / DecoratingClassLoader — 생성한 프록시 클래스를 적절한 로더에 정의하고, 같은 이름 클래스를 재정의 가능한지 판단하는 클래스로딩 보조물.

설계 포인트 / 확장점

  • 재패키징 = 의존성 격리: 패키지 이름을 바꾸는 것만으로 외부 라이브러리 버전 충돌을 원천 차단한다. 클래스 파일 레벨 변환이라 원본 소스/문서를 그대로 참고할 수 있다(package-info가 원본 링크 제공).
  • "internal use only": 애플리케이션이 직접 org.springframework.asm.*를 import하면 안 된다 — 언제든 버전이 바뀔 수 있는 내부 부품이다.
  • 환경 적응 가드: SpringObjenesis.isWorthTrying(), spring.objenesis.ignore처럼 단순 재패키징을 넘어 네이티브/제한 환경을 고려한 래퍼를 둔다.
  • 협력 사슬: 메타데이터 읽기(ASM) → 프록시 생성(CGLIB이 ASM으로 출력) → 인스턴스화(Objenesis)로 이어지는 저수준 바이트코드 인프라가 한 모듈에 모여 상위 AOP/설정 기능을 떠받친다.

정리

asm/cglib/objenesis/javapoet은 외부 라이브러리를 org.springframework.*로 재패키징해 버전 충돌을 피하고 내부 전용으로 쓰는 부품들이다. ASM은 바이트코드를 읽고(메타데이터) 쓰며(클래스 생성), CGLIB은 그 위에서 런타임 서브클래스 프록시를 만들고, Objenesis는 생성자 없이 인스턴스화한다. Spring은 여기에 SpringNamingPolicy, SpringObjenesis 같은 얇은 래퍼로 명명 규칙과 환경 적응 로직을 덧입혀, AOP와 @Configuration 향상 같은 상위 기능의 바이트코드 토대를 제공한다.