여기서부터는 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 |
댓글