본문 바로가기
language/java

JDK Proxy vs CGLIB

by 죄니안죄니 2026. 6. 16.
반응형

이번 글은 Spring 면접에서 가장 많이 나오는 주제 중 하나이며,
Dynamic Proxy를 이해했다면 반드시 이어서 알아야 하는 내용이다.

많은 사람들이

"JDK Proxy는 인터페이스용이고 CGLIB는 클래스용"

정도만 외우고 끝난다.

하지만 왜 그렇게 만들어졌는지 이해하면 절대 잊지 않는다.


JDK Proxy vs CGLIB

둘 다 Proxy를 만드는 기술이지만, 만드는 방법이 완전히 다르다.


먼저 Proxy의 목적을 다시 생각해보자.

우리가 원하는 것은

Client

↓

Proxy

↓

Real Object
 

이다.

그리고 Proxy에서

로그 출력

트랜잭션 시작

캐시 확인

권한 검사

실제 메서드 실행

commit
 

같은 공통 기능을 수행한다.


문제는 Proxy를 어떻게 만들 것인가?

Java에는 크게 두 가지 방법이 있다.

1. 인터페이스를 구현하는 방법

(JDK Dynamic Proxy)

----------------------------

2. 클래스를 상속하는 방법

(CGLIB)
 

1. JDK Dynamic Proxy

인터페이스 기반

예를 들어

 
public interface UserService {

    void save();

}
 

구현체

 
public class UserServiceImpl
        implements UserService {

    @Override
    public void save() {

        System.out.println("save");

    }

}
 

Proxy는

 
public class UserServiceProxy
        implements UserService {

    private UserService target;

}
 

처럼 만들어진다.


그림으로 보면

               UserService (interface)
                      ▲
                      │
      ┌───────────────┴───────────────┐
      │                               │
UserServiceImpl              Dynamic Proxy
 

둘 다 같은 인터페이스를 구현한다.


Client는

 
UserService service = ...
 

만 알고 있다.

실제로는

UserService

↓

Dynamic Proxy

↓

Real Object
 

가 된다.


왜 인터페이스가 꼭 필요할까?

Proxy도

 
UserService
 

타입이어야 하기 때문이다.


예를 들어

 
UserService service =
        new DynamicProxy();
 

가 가능해야 한다.

그래서

Dynamic Proxy도 같은 인터페이스를 구현한다.


장점

JDK 기본 기능

추가 라이브러리 없음

구현이 단순

메모리 사용 적음
 

단점

인터페이스가 반드시 있어야 한다.

UserService (interface)

↓

UserServiceImpl
 

이 구조가 아니면 사용할 수 없다.


인터페이스가 없는 경우

 
public class UserService {

    public void save() {

    }

}
 

여기서

implements ?
 

할 것이 없다.


그래서 JDK Proxy는 사용할 수 없다.


2. CGLIB

이번에는 다른 방식을 사용한다.

상속(Inheritance)


기존 클래스

 
public class UserService {

    public void save() {

        System.out.println("save");

    }

}
 

Proxy는 내부적으로

이런 클래스를 만든다.

 
public class UserServiceProxy
        extends UserService {

    @Override
    public void save() {

        before();

        super.save();

        after();

    }

}
 

그림

UserService

      ▲

      │ extends

      │

UserServiceProxy
 

Client는

 
UserService service =
        new UserServiceProxy();
 

라고 생각한다.

하지만 실제 객체는 Proxy이다.


왜 CGLIB는 인터페이스가 필요 없을까?

상속을 사용하기 때문이다.

extends UserService
 

만 가능하면 된다.


실행 흐름

Client

↓

Proxy.save()

↓

before()

↓

super.save()

↓

after()
 

그런데 문제가 있다.

상속에는 제한이 있다.


final 클래스

 
public final class UserService {

}
 

Proxy는

 
class Proxy
        extends UserService
 

를 만들어야 한다.


하지만

final

↓

상속 금지
 

이다.


그래서

final class

↓

CGLIB Proxy 생성 불가
 

이다.


final 메서드도 마찬가지

 
public class UserService {

    public final void save() {

    }

}
 

Proxy는

 
@Override
public void save()
 

를 해야 한다.


그런데

final

↓

Override 금지
 

이다.


그래서

final method

↓

AOP 적용 불가
 

이다.


이것이 실무에서 매우 중요하다.

예를 들어

 
@Transactional
public final void save() {

}
 

라고 작성했다.


많은 사람들이

@Transactional

↓

트랜잭션 적용
 

이라고 생각한다.


실제로는

CGLIB

↓

Override 불가능

↓

Proxy 생성 불가능

↓

@Transactional 동작 안 함
 

이다.


Spring은 무엇을 사용할까?

Spring은 상황에 따라 다르다.


Spring 4.x 이전

기본

인터페이스 있음

↓

JDK Proxy

----------------

인터페이스 없음

↓

CGLIB
 

Spring Boot 2.x

기본값

CGLIB
 

이다.

설정

 
spring.aop.proxy-target-class=true
 

기본값이기 때문이다.


인터페이스가 있어도

CGLIB

↓

클래스 상속 방식
 

을 사용한다.


왜 CGLIB를 기본으로 바꿨을까?

실무에서는

 
@Service
public class UserService {

}
 

처럼

인터페이스 없이 작성하는 경우가 매우 많아졌다.


그리고

JDK Proxy

↓

인터페이스 필수
 

라는 제약이 있었다.


그래서

Spring Boot

↓

CGLIB 기본

↓

인터페이스 없어도 AOP 가능
 

으로 변경되었다.


둘을 그림으로 비교

JDK Proxy

          UserService(interface)

                  ▲

      ┌───────────┴───────────┐

      │                       │

Real Object             Dynamic Proxy
 

CGLIB

UserService

      ▲

      │ extends

      │

Proxy
 

성능 차이는?

예전에는

JDK Proxy가 조금 더 빠르다.
 

라는 이야기가 많았다.


하지만 현대 JVM에서는

JIT 최적화

MethodHandle

Reflection 개선
 

등으로 인해

실무에서 체감할 수준의 차이는 거의 없다.


그래서 지금은

유지보수성

적용 가능 여부

Spring 기본 전략
 

이 더 중요하다.


면접 포인트

JDK Dynamic Proxy

  • 인터페이스 기반
  • 인터페이스를 구현하여 Proxy 생성
  • 인터페이스가 반드시 필요

CGLIB

  • 클래스 상속 기반
  • 인터페이스 없이 사용 가능
  • final 클래스, final 메서드는 Proxy 생성 불가

Spring Boot에서 기본 Proxy는?

Spring Boot 2.x 이후 대부분의 환경에서는 CGLIB 기반 Proxy가 기본이며, 인터페이스가 없어도 AOP를 적용할 수 있다.


여기서 꼭 알고 넘어가야 할 한 가지

많은 사람들이

"AOP가 적용 안 된다 = CGLIB 문제"

라고 생각하는데, 실제 실무에서 더 많이 만나는 문제는 Self Invocation(자기 자신 호출) 이다.

 
@Service
public class UserService {

    public void outer() {
        inner();   // @Transactional이 있어도 적용되지 않음
    }

    @Transactional
    public void inner() {
        ...
    }
}
 

왜냐하면 this.inner() 호출은 Proxy를 거치지 않고 실제 객체를 직접 호출하기 때문이다.


다음 글 추천

가장 자연스럽게 이어지는 주제는 Serialization / Deserialization이다.

Reflection, Annotation, Proxy를 이해한 상태에서 보면,

  • Jackson은 왜 기본 생성자가 필요한지
  • JSON이 어떻게 객체가 되는지
  • Spring MVC가 RequestBody를 어떻게 객체로 만드는지

까지 하나의 흐름으로 연결해서 이해할 수 있다.

반응형

'language > java' 카테고리의 다른 글

AutoBoxing / Unboxing  (0) 2026.06.16
Serialization / Deserialization  (0) 2026.06.16
Dynamic Proxy 원리  (0) 2026.06.16
Meta Annotation  (0) 2026.06.16
Annotation 동작 원리  (0) 2026.06.16

댓글