본문 바로가기
language/java

Dynamic Proxy 원리

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

여기서부터는 Spring을 이해하는 가장 중요한 분기점이다.

많은 개발자가 @Transactional, @Async, @Cacheable, @Secured를 사용하면서도

"Spring이 어떻게 내가 작성한 메서드 앞뒤에 코드를 끼워 넣는 거지?"

라는 의문을 가진다.

답은 Dynamic Proxy(동적 프록시) 이다.

이 글을 이해하면 Spring AOP와 트랜잭션의 70% 이상을 이해했다고 봐도 된다.


Dynamic Proxy 원리

"실제 객체 대신 대리 객체(Proxy)를 만들어, 메서드 호출을 가로채고 원하는 기능을 추가하는 기술"


먼저 Proxy가 무엇인가?

현실에서 프록시(proxy)는

대신 처리해주는 사람이다.

예를 들어

은행에 직접 가지 않고

나
 │
 ▼
대리인(Proxy)
 │
 ▼
은행
 

대리인이 대신 업무를 처리한다.


Java도 똑같다.

Client
   │
   ▼
Proxy
   │
   ▼
Real Object
 

Client는 실제 객체를 직접 호출하는 것이 아니라

Proxy를 호출한다.


일반적인 객체 호출

 
public class UserService {

    public void login() {
        System.out.println("로그인");
    }

}
 

호출

 
UserService service = new UserService();

service.login();
 

실행 흐름

Client

↓

UserService.login()

↓

로그인
 

매우 단순하다.


그런데 로그를 남기고 싶다면?

메서드 실행 전

↓

로그 출력

↓

실제 메서드 실행

↓

실행 후 로그 출력
 

가 필요하다.


가장 쉬운 방법은

 
public void login() {

    System.out.println("before");

    ...

    System.out.println("after");

}
 

하지만

모든 메서드마다 넣어야 한다.

login()

save()

delete()

update()

find()
 

전부 수정해야 한다.


Proxy를 사용하면

실제 객체는 수정하지 않는다.

Client

↓

Proxy

↓

Real Object
 

Proxy가 대신 처리한다.

 
before();

real.login();

after();
 

그림으로 이해하기

기존

Client

↓

UserService
 

Proxy 적용

Client

↓

UserServiceProxy

↓

UserService
 

Client는

UserService를 호출한다고 생각하지만

실제로는 Proxy를 호출한다.


직접 Proxy 만들어보기

인터페이스

 
public interface UserService {

    void login();

}
 

실제 객체

 
public class UserServiceImpl
        implements UserService {

    @Override
    public void login() {

        System.out.println("로그인");

    }

}
 

Proxy

 
public class UserServiceProxy
        implements UserService {

    private final UserService target;

    public UserServiceProxy(UserService target) {

        this.target = target;

    }

    @Override
    public void login() {

        System.out.println("before");

        target.login();

        System.out.println("after");

    }

}
 

실행

 
UserService service =
        new UserServiceProxy(
                new UserServiceImpl());

service.login();
 

출력

before

로그인

after
 

그런데 문제가 있다.

메서드가 하나가 아니다.

login()

save()

delete()

update()

find()

...
 

100개라면?

Proxy도 100개를 만들어야 한다.

엄청난 중복이다.


그래서 Dynamic Proxy가 등장했다.

Proxy를 실행 중(Runtime)에 자동으로 만들어 주는 기술

이다.

개발자가

 
class UserServiceProxy {

}
 

를 직접 만들 필요가 없다.

JVM이 만들어 준다.


Dynamic Proxy 구조

Client

↓

Dynamic Proxy

↓

InvocationHandler

↓

Real Object
 

InvocationHandler

모든 메서드 호출이

여기로 들어온다.

 
public Object invoke(
        Object proxy,
        Method method,
        Object[] args
)
 

여기서

method

↓

login()

save()

delete()

...

모든 메서드 정보
 

를 받을 수 있다.


실제 코드

인터페이스

 
public interface UserService {

    void login();

}
 

구현체

 
public class UserServiceImpl
        implements UserService {

    @Override
    public void login() {

        System.out.println("로그인");

    }

}
 

InvocationHandler

 
public class LogHandler
        implements InvocationHandler {

    private final Object target;

    public LogHandler(Object target) {

        this.target = target;

    }

    @Override
    public Object invoke(
            Object proxy,
            Method method,
            Object[] args
    ) throws Throwable {

        System.out.println("before");

        Object result =
                method.invoke(target, args);

        System.out.println("after");

        return result;
    }

}
 

Proxy 생성

 
UserService service =
    (UserService) Proxy.newProxyInstance(

        UserService.class.getClassLoader(),

        new Class[]{
                UserService.class
        },

        new LogHandler(
                new UserServiceImpl()
        )

    );
 

호출

 
service.login();
 

출력

before

로그인

after
 

중요한 부분

우리는

 
service.login();
 

만 호출했다.

하지만 실제 흐름은

Client

↓

Dynamic Proxy

↓

invoke()

↓

before

↓

Real Object.login()

↓

after
 

이다.


Spring은 이것을 어떻게 사용할까?

예를 들어

 
@Transactional
public void save() {

}
 

를 보자.

많은 사람들이

@Transactional

↓

트랜잭션 시작
 

이라고 생각한다.

실제로는

Client

↓

Transaction Proxy

↓

begin()

↓

Real Object.save()

↓

commit()

↓

return
 

이다.


실제 흐름

Client

↓

save()

↓

Proxy

↓

Transaction 시작

↓

Real Object.save()

↓

정상

↓

commit()

↓

return
 

예외 발생

Client

↓

Proxy

↓

Transaction 시작

↓

Real Object.save()

↓

Exception

↓

rollback()

↓

throw Exception
 

Spring에서 Proxy가 사용되는 기능

거의 대부분이 Proxy 기반이다.

@Transactional

↓

Proxy
 

@Async

↓

Proxy
 

@Cacheable

↓

Proxy
 

@Retryable

↓

Proxy
 

AOP

↓

Proxy
 

Reflection과의 관계

앞에서 Reflection을 공부했다.

Reflection은

메서드 찾기

↓

Method 객체

↓

invoke()
 

를 제공한다.

Dynamic Proxy는

메서드 호출 가로채기

↓

Reflection Method.invoke()

↓

실제 메서드 실행
 

을 수행한다.

즉,

Client

↓

Dynamic Proxy

↓

Reflection(Method.invoke)

↓

Real Object
 

구조이다.


면접 포인트

Dynamic Proxy란?

실행 중(Runtime)에 실제 객체를 대신하는 Proxy 객체를 생성하여 메서드 호출을 가로채고 공통 기능을 추가하는 기술이다.

왜 사용하는가?

  • 공통 로직 중복 제거
  • 기존 코드 수정 없이 기능 추가
  • AOP 구현
  • 트랜잭션, 캐시, 비동기 처리 지원

Spring에서 어디에 사용되는가?

  • @Transactional
  • @Async
  • @Cacheable
  • Spring AOP
  • 다양한 부가 기능 처리

다음 글 예고

다음은 JDK Proxy vs CGLIB이다.

여기서 많은 면접 질문이 나온다.

  • 왜 인터페이스가 있으면 JDK Proxy를 사용할까?
  • 인터페이스가 없으면 왜 CGLIB를 사용할까?
  • final 클래스나 final 메서드에는 왜 AOP가 적용되지 않을까?
  • Spring Boot 2.x와 3.x의 프록시 전략은 무엇이 다른가?

이 글까지 이해하면 Spring Proxy 메커니즘을 거의 완성할 수 있다.

반응형

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

Serialization / Deserialization  (0) 2026.06.16
JDK Proxy vs CGLIB  (0) 2026.06.16
Meta Annotation  (0) 2026.06.16
Annotation 동작 원리  (0) 2026.06.16
Class 객체란?  (0) 2026.06.16

댓글