이번 글은 Spring 면접에서 단골 질문이며, AOP를 제대로 이해했는지 확인하는 질문이기도 하다.
면접에서 자주 나오는 질문이 있다.
"Spring은 JDK Dynamic Proxy와 CGLIB 중 어떤 것을 사용하나요?"
많은 사람들이
"인터페이스 있으면 JDK Proxy, 없으면 CGLIB입니다."
라고만 답한다.
틀린 답은 아니지만, 왜 그렇게 설계되었는지까지 이해해야 Spring Proxy를 제대로 이해한 것이다.
JDK Proxy vs CGLIB in Spring
Spring은 어떻게 Proxy를 만들까?
이전 글에서 우리는
Client
↓
Proxy
↓
Real Object
구조를 배웠다.
그리고
@Transactional
public void save() {
}
가 실제로는
Transaction Proxy
↓
Real Service
를 통해 실행된다는 것도 살펴봤다.
그렇다면 새로운 질문이 생긴다.
Spring은 Proxy를 어떤 방식으로 만들까?
Proxy를 만드는 방법은 두 가지
Spring은 크게 두 가지 방법으로 Proxy를 생성한다.
JDK Dynamic Proxy
CGLIB Proxy
둘 다 Proxy를 만든다는 점은 같지만,
만드는 방식이 완전히 다르다.
JDK Dynamic Proxy
JDK가 기본으로 제공하는 기능이다.
다음 구조를 보자.
public interface UserService {
void save();
}
@Service
public class UserServiceImpl
implements UserService {
@Override
public void save() {
System.out.println("save");
}
}
여기서 JDK Proxy는
UserService (Interface)
▲
│
JDK Proxy
│
▼
UserServiceImpl
처럼 동작한다.
즉,
Proxy도
인터페이스를 구현한다.
호출 흐름
UserService service = proxy;
service.save();
실제로는
Client
↓
JDK Proxy
↓
InvocationHandler.invoke()
↓
Real Object.save()
순서로 실행된다.
핵심은 인터페이스
JDK Proxy는
인터페이스를 구현하는 방식으로 Proxy를 만든다.
그래서
인터페이스가 반드시 필요하다.
인터페이스가 없다면?
@Service
public class UserService {
public void save() {
}
}
구현할 Interface가 없다.
이 경우
JDK Proxy는 사용할 수 없다.
CGLIB 등장
CGLIB는
인터페이스가 아니라
상속(Inheritance) 을 이용한다.
원본 클래스
public class UserService {
public void save() {
System.out.println("save");
}
}
CGLIB
UserService
▲
│ extends
CGLIB Proxy
즉,
Proxy가
UserService를 상속받는다.
호출 흐름
userService.save();
↓
Client
↓
CGLIB Proxy.save()
↓
Transaction
↓
super.save()
상속을 이용해서
기존 메서드 앞뒤에 기능을 추가한다.
그림으로 비교
JDK Proxy
Interface
▲
│ implements
Proxy
│
▼
Real Object
CGLIB
Proxy
│ extends
▼
Real Class
왜 Spring은 두 가지를 모두 지원할까?
Spring은 오래전부터 존재했다.
초기 Java 개발은
interface UserService
class UserServiceImpl
구조가 매우 일반적이었다.
그래서
JDK Proxy만으로도 충분했다.
하지만
최근에는
@Service
public class UserService {
}
처럼
인터페이스 없이 클래스를 직접 사용하는 경우가 매우 많아졌다.
그래서
CGLIB 지원이 필수가 되었다.
Spring Boot의 기본값
Spring Framework는
원래
인터페이스 있음
↓
JDK Proxy
------------
인터페이스 없음
↓
CGLIB
전략을 사용했다.
하지만
Spring Boot 2.x 이후부터는
기본적으로
CGLIB Proxy
를 사용하는 방향으로 변경되었다.
이유는
프로그래밍 모델을 단순하게 만들기 위해서이다.
final이 왜 문제일까?
다음 코드를 보자.
public final class UserService {
}
CGLIB는
상속으로 Proxy를 만든다.
그런데
final
↓
상속 금지
이다.
즉,
CGLIB Proxy
↓
extends UserService
↓
컴파일 불가
그래서
final 클래스는
CGLIB Proxy를 만들 수 없다.
final 메서드도 문제
public class UserService {
public final void save() {
}
}
CGLIB는
메서드를 Override해서
기능을 추가한다.
그런데
final
↓
Override 금지
이다.
결국
Transaction
Log
Cache
를 추가할 수 없다.
그래서 @Transactional이 안 되는 경우가 있다.
@Transactional
public final void save() {
}
CGLIB
↓
Override 불가
↓
Proxy 적용 불가
↓
Transaction 동작 안 함
실무에서 의외로 자주 발생하는 문제이다.
성능 차이는?
예전에는
JDK Proxy
>
CGLIB
라고 알려져 있었다.
하지만
최근 JVM에서는
거의 의미 없는 수준이다.
실무에서는
성능
X
사용 가능 여부
O
가 더 중요하다.
Proxy인지 확인하는 방법
디버깅할 때
System.out.println(
userService.getClass()
);
출력
JDK Proxy
com.sun.proxy.$Proxy123
CGLIB
UserService$$SpringCGLIB$$0
이렇게
실제 Proxy 타입을 확인할 수 있다.
Spring이 내부에서 선택하는 과정
Bean 생성
↓
AOP 필요 확인
↓
Interface 존재?
│
YES NO
│ │
JDK Proxy CGLIB
│ │
Proxy 생성
↓
Container 저장
실무에서 인터페이스를 꼭 만들어야 할까?
예전에는
UserService
↓
UserServiceImpl
구조를 많이 사용했다.
하지만 최근에는
@Service
public class UserService {
}
처럼
구현 클래스만 사용하는 프로젝트도 매우 많다.
Spring Boot가 기본적으로 CGLIB를 사용하는 이유도
이러한 개발 스타일을 지원하기 위해서이다.
지금까지의 흐름
Reflection
↓
Bean 생성
↓
DI
↓
BeanPostProcessor
↓
Dynamic Proxy
↓
JDK Proxy 또는 CGLIB 선택
↓
Container 저장
↓
Client 호출
↓
Transaction / Log / Cache 수행
면접 포인트
JDK Dynamic Proxy란?
인터페이스를 구현하는 방식으로 Proxy를 생성하는 JDK 기본 기능이다.
CGLIB란?
상속(extends)을 이용하여 Proxy를 생성하는 라이브러리이다.
Spring은 언제 JDK Proxy를 사용하고 언제 CGLIB를 사용하는가?
전통적으로는
- 인터페이스가 있으면 JDK Proxy
- 인터페이스가 없으면 CGLIB
를 사용했다.
다만 Spring Boot에서는 클래스 기반 개발을 지원하기 위해 CGLIB를 기본 Proxy 방식으로 사용하는 경우가 많다.
왜 final 클래스와 final 메서드가 문제가 되는가?
CGLIB는 상속과 메서드 Override를 이용하여 Proxy를 생성하기 때문에,
- final 클래스는 상속할 수 없고
- final 메서드는 Override할 수 없어
Proxy 기반 기능(@Transactional, AOP 등)을 적용할 수 없다.
다음 글
다음 글은 Java Proxy 기반 트랜잭션 처리이다.
여기서는 지금까지 배운
Reflection
↓
Bean 생성
↓
BeanPostProcessor
↓
Proxy 생성
↓
ThreadLocal
↓
Transaction
을 하나의 요청 흐름으로 연결하여,
@Transactional 한 줄이 내부에서 어떤 과정을 거쳐 Commit/Rollback까지 수행되는지를 그림과 코드로 깊이 있게 설명하면 Spring 챕터의 이해도가 크게 올라간다.
'language > java' 카테고리의 다른 글
| ThreadLocal과 TransactionSynchronizationManager (0) | 2026.06.24 |
|---|---|
| Java Proxy 기반 트랜잭션 처리 (0) | 2026.06.17 |
| Dynamic Proxy와 Spring AOP (0) | 2026.06.17 |
| 객체 생명주기와 Spring Bean (0) | 2026.06.17 |
| Bean 생성 과정 (0) | 2026.06.17 |
댓글