Spring

JDK Dynamic Proxy vs CGLIB — Spring AOP가 프록시를 만드는 두 가지 방식에 대해서 설명해주세요

JDK Dynamic Proxy와 CGLIB의 동작 원리, 인터페이스 기반 vs 클래스 기반 차이, Spring Boot의 기본 설정(proxyTargetClass)을 인터랙티브 시각화로 비교합니다.

2026년 3월 21일 · 약 12분 읽기

Q. "Spring AOP에서 사용하는 JDK Dynamic Proxy와 CGLIB의 차이를 설명하고, Spring Boot에서 기본으로 어떤 방식을 사용하는지 말씀해주세요."

예상 꼬리질문

답변 가이드

"JDK Dynamic Proxy는 Java 표준 라이브러리의 java.lang.reflect.Proxy를 사용하여, 대상 클래스가 구현한 인터페이스를 기반으로 런타임에 프록시 클래스를 동적 생성합니다. 반면 CGLIB은 바이트코드 조작 라이브러리를 통해 대상 클래스를 상속한 서브클래스를 동적으로 생성하므로, 인터페이스 없이도 프록시를 만들 수 있습니다."

"두 방식의 핵심 차이는 프록시 생성 메커니즘에 있습니다. JDK Proxy는 리플렉션(Reflection)으로 메서드를 호출하고, CGLIB은 바이트코드로 생성된 서브클래스에서 메서드를 직접 오버라이드하여 호출합니다. 이 차이로 인해 프록시 생성 속도는 JDK Proxy가 빠르지만, 실제 메서드 호출 성능은 CGLIB이 우수합니다."

"Spring Boot 2.0부터 proxyTargetClass=true가 기본값으로 적용되어 항상 CGLIB을 사용합니다. 인터페이스 없이 @Service, @Component를 선언하는 실제 개발 관행을 반영한 결정입니다. 단, final 클래스나 final 메서드는 CGLIB이 상속·오버라이드할 수 없으므로 AOP가 적용되지 않는다는 점을 주의해야 합니다."

@Transactional을 붙였는데 트랜잭션이 적용되지 않거나, final 메서드인데 왜 AOP가 안 되는지 의아했던 적 있나요?

이 모든 현상의 근원은 Spring이 프록시를 어떻게 만드는지에 있습니다. JDK Dynamic Proxy와 CGLIB의 동작 원리를 이해하면, Spring AOP의 동작 방식과 한계가 한눈에 보입니다.


1. 프록시(Proxy)가 무엇인가

꼬리질문: "Spring AOP에서 프록시가 어떤 역할을 하나요?"

Spring AOP는 실제 객체를 직접 수정하지 않고, 그 앞에 프록시 객체를 배치하여 부가 기능(트랜잭션, 캐싱, 로깅)을 삽입합니다. 클라이언트는 프록시 객체를 실제 객체로 알고 호출하고, 프록시가 부가 로직을 실행한 뒤 실제 객체에 위임합니다.

Spring은 빈(Bean)을 컨테이너에 등록할 때 AOP 어드바이스가 적용되어야 하면 실제 객체 대신 프록시 객체를 등록합니다. 이 프록시를 만드는 방식이 바로 JDK Dynamic ProxyCGLIB입니다.

호출 시뮬레이션 버튼을 눌러 프록시를 통한 호출 흐름을 확인하세요.

프록시 호출 흐름

클라이언트는 프록시를 실제 객체로 알고 호출합니다. 시뮬레이션 버튼을 눌러 흐름을 확인하세요.

호출자
Client
프록시 객체
UserService$$Proxy
Before Advice
invoke()
After Advice
실제 객체
UserServiceImpl

2. JDK Dynamic Proxy — 인터페이스 기반 프록시

꼬리질문: "JDK Dynamic Proxy를 사용하려면 어떤 조건이 필요한가요?"

JDK Dynamic Proxy는 java.lang.reflect.Proxy를 사용하여 인터페이스를 구현한 프록시 클래스를 런타임에 동적 생성합니다. 생성된 프록시는 같은 인터페이스를 구현하므로 클라이언트는 동일한 인터페이스 타입으로 프록시를 사용합니다.

핵심 제약은 대상 클래스가 반드시 하나 이상의 인터페이스를 구현해야 한다는 점입니다. 인터페이스가 없으면 JDK Dynamic Proxy를 사용할 수 없습니다.

각 단계에 마우스를 올리면 코드 스니펫을 확인할 수 있습니다.

JDK Dynamic Proxy 생성 흐름

각 단계에 마우스를 올리면 코드 스니펫을 확인할 수 있습니다.

인터페이스 구현 여부?
Yes → 아래 단계 진행
1
인터페이스 추출
2
Proxy.newProxyInstance() 호출
3
JVM이 $Proxy0 클래스 런타임 생성
4
InvocationHandler 연결
5
프록시 빈 등록 (타입: UserService)
프록시 생성 완료
생성된 클래스명: $Proxy42

* 인터페이스가 없는 경우 토글 버튼으로 실패 분기를 확인할 수 있습니다


3. CGLIB — 클래스 상속 기반 프록시

꼬리질문: "CGLIB이 final 메서드에 AOP를 적용할 수 없는 이유는 무엇인가요?"

CGLIB(Code Generation Library)은 ASM 바이트코드 조작 프레임워크를 사용하여 대상 클래스를 상속한 서브클래스를 동적으로 생성합니다. 인터페이스가 없어도 동작하지만 두 가지 핵심 제약이 있습니다.

final 클래스는 상속이 불가능하여 프록시 자체를 만들 수 없고, final 메서드는 오버라이드가 불가능하여 해당 메서드에만 AOP가 적용되지 않습니다. Spring은 이 경우 경고 로그를 출력하지만 예외를 발생시키지 않으므로 조용히 놓치기 쉽습니다.

final 클래스/메서드 시나리오 버튼으로 제약 상황을 확인하세요.

CGLIB 생성 흐름

각 단계에 마우스를 올리면 코드 스니펫을 확인할 수 있습니다.

제약 시나리오:
1
클래스 분석 (인터페이스 없어도 가능)
2
Enhancer 설정
3
ASM이 서브클래스 바이트코드 생성
4
MethodInterceptor 연결
5
프록시 빈 등록 (타입: OrderService 서브타입)
프록시 생성 완료
생성된 클래스명: OrderService$$EnhancerBySpringCGLIB$$xxxx

* 위 버튼으로 final 클래스/메서드 제약 시나리오를 확인할 수 있습니다


4. 두 방식 인터랙티브 비교

꼬리질문: "두 방식의 성능 차이는 어떻게 나타나나요?"

두 방식의 차이를 직접 비교해 보겠습니다. 인터페이스 기반 vs 클래스 기반이라는 근본적 차이가 어떤 결과로 이어지는지 핵심 항목별로 확인할 수 있습니다.

성능 측면에서 프록시 생성 속도는 JDK Proxy가 빠르지만, 메서드 호출 성능은 CGLIB이 유리합니다. 애플리케이션 시작 시 1회만 프록시가 생성되므로 실제 운영에서는 호출 성능이 더 중요합니다.

행을 클릭하면 해당 항목의 상세 설명을 확인할 수 있습니다.

JDK Proxy vs CGLIB 비교

행을 클릭하면 상세 설명을 확인할 수 있습니다.

비교 항목
JDK Dynamic Proxy
CGLIB

* Spring Boot 기본 설정(proxyTargetClass=true)에서는 항상 CGLIB이 사용됩니다


5. Spring Boot의 proxyTargetClass 설정

꼬리질문: "Spring Boot 2.0에서 왜 기본 프록시 방식을 CGLIB으로 변경했나요?"

Spring Boot 2.0부터 spring.aop.proxy-target-class=true가 기본값으로, 인터페이스를 구현한 클래스에도 CGLIB을 강제 사용합니다. 이 결정은 @Autowired 주입 시 구체 클래스 타입으로 주입하는 관행과의 충돌을 방지하기 위한 것입니다.

JDK Proxy 환경에서 구체 클래스 타입(UserServiceImpl)으로 @Autowired를 선언하면 ClassCastException이 발생합니다. CGLIB은 UserServiceImpl의 서브클래스를 생성하므로 이 문제가 없습니다.

주입 타입을 바꿔가며 두 방식의 호환성 차이를 확인하세요.

proxyTargetClass 설정 시뮬레이터

주입 타입을 바꿔가며 JDK Proxy와 CGLIB의 호환성 차이를 확인하세요.

JDK Dynamic Proxy
인터페이스 기반 프록시
설정
spring.aop.proxy-target-class=false
생성된 프록시
$Proxy42
주입 코드
@Autowired
private UserService userService;
// 인터페이스 타입으로 주입
성공
JDK Proxy 타입($Proxy42)은 UserService 인터페이스를 구현하므로 주입 가능합니다.
CGLIBSpring Boot 기본값
클래스 상속 기반 프록시
설정
spring.aop.proxy-target-class=true
생성된 프록시
UserServiceImpl$$EnhancerBySpringCGLIB$$xxxx
주입 코드
@Autowired
private UserService userService;
// 인터페이스 타입으로 주입
성공
CGLIB 프록시(UserServiceImpl$$CGLIB)는 UserServiceImpl의 서브타입이며 UserService도 구현하므로 주입 가능합니다.

* Spring Boot 2.0부터 CGLIB이 기본값입니다. 인터페이스 없는 서비스에도 AOP가 일관되게 동작합니다.

자주 발생하는 문제

퀴즈로 확인하기

개념을 제대로 이해했는지 확인해 보세요.

퀴즈 1

인터페이스 없는 클래스에 AOP 적용

@Service
public class OrderService { // 인터페이스 없음
    @Transactional
    public void placeOrder() { ... }
}

// Spring Boot 기본 설정에서
// 프록시 생성 결과는?
퀴즈 2

final 메서드의 함정

@Service
public class PaymentService {
    @Transactional
    public final void process(Payment payment) {
        paymentRepository.save(payment);
    }
}

// 위 코드를 실행하면 어떤 일이 발생하는가?
퀴즈 3

JDK Proxy 환경에서의 타입 주입

public interface UserService { void save(User user); }

@Service
public class UserServiceImpl implements UserService {
    public void save(User user) { ... }
}

// proxyTargetClass=false 설정
@Autowired
private UserServiceImpl userService; // 구체 클래스 타입

// 실행 결과는?
퀴즈 4

Self-Invocation 함정

@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        this.sendNotification(order); // 같은 클래스 내 호출
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(Order order) {
        notificationRepository.save(new Notification(order));
    }
}
// sendNotification()은 어떤 트랜잭션에서 실행되는가?
퀴즈 5

프록시 타입 확인

@Autowired
private UserService userService;
// (UserServiceImpl이 UserService를 구현)

@PostConstruct
public void check() {
    System.out.println(userService.getClass().getName());
}

// Spring Boot 기본 설정에서 출력되는 클래스명 형태는?

면접 체크리스트

이 항목들을 자신 있게 설명할 수 있다면 Spring 프록시 질문은 준비 완료입니다.

  • - JDK Dynamic Proxy: 인터페이스 필수, 리플렉션 기반, java.lang.reflect.Proxy 사용
  • - CGLIB: 인터페이스 불필요, ASM 바이트코드 조작, 클래스 상속 기반
  • - Spring Boot 기본값: proxyTargetClass=true → 항상 CGLIB (2.0부터)
  • - final 제약: CGLIB에서 final 클래스/메서드는 AOP 적용 불가
  • - Self-Invocation: 같은 클래스 내 this.method() 호출은 프록시 미경유 → AOP 무시
  • - 성능: 생성 속도는 JDK Proxy > CGLIB, 호출 성능은 CGLIB > JDK Proxy

참고 자료


의견을 들려주세요

서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.

0 / 1000