여기부터는 Spring을 이해하는 핵심 중의 핵심이다.
많은 개발자가 AOP를
"로그 찍는 기능"
또는
"@Transactional이 내부적으로 쓰는 기술"
정도로만 알고 있다.
하지만 실제로는
Spring은 Proxy를 이용해서 기존 코드를 수정하지 않고 기능을 추가한다.
라는 것이 핵심이다.
그리고 이번 글은 이전 글들과 자연스럽게 이어져야 한다.
Reflection
↓
Bean 생성
↓
BeanPostProcessor
↓
★★★★★ Proxy 생성 ★★★★★
↓
Container 저장
↓
Controller에서 사용
지금까지 배운 내용이 여기서 하나로 연결된다.
Dynamic Proxy와 Spring AOP
Spring은 어떻게 기존 코드를 수정하지 않고 기능을 추가할까?
지금까지 우리는
Reflection으로 Bean 생성
↓
DI 수행
↓
BeanPostProcessor 실행
↓
Bean Container 저장
이라는 과정을 배웠다.
그런데 아직 한 가지 의문이 남아 있다.
다음 코드를 보자.
@Service
public class UserService {
@Transactional
public void save() {
System.out.println("save");
}
}
우리는
userService.save();
만 호출했다.
그런데 실제로는
트랜잭션 시작
↓
save()
↓
Commit
이 자동으로 수행된다.
Spring은 도대체 어디에서 이 코드를 실행하는 걸까?
만약 Proxy가 없다면
직접 구현해야 한다.
public void save() {
beginTransaction();
try {
...
commit();
}
catch (Exception e) {
rollback();
}
}
모든 Service마다
이 코드를 반복해야 한다.
그리고
로그도 필요하다.
public void save() {
log();
beginTransaction();
...
}
캐시도 추가된다.
public void save() {
cache();
log();
beginTransaction();
...
}
결국
비즈니스 코드보다
부가 기능이 더 많아진다.
AOP가 해결하려는 문제
Spring은 생각했다.
비즈니스 로직과 부가 기능을 분리하자.
비즈니스 코드
@Service
public class UserService {
public void save() {
System.out.println("save");
}
}
트랜잭션
로그
캐시
보안
모두 밖에서 처리한다.
Proxy란?
Proxy는
실제 객체 대신 앞에서 요청을 가로채는 객체
이다.
기존 구조
Client
↓
UserService
Proxy 구조
Client
↓
Proxy
↓
UserService
Client는
Proxy인지
실제 객체인지 모른다.
실제 흐름
userService.save();
실행
Client
↓
Transaction Proxy
↓
begin()
↓
Real UserService.save()
↓
commit()
↓
return
비즈니스 코드는
전혀 수정되지 않았다.
Dynamic Proxy란?
Proxy를 직접 하나씩 만들면 어떨까?
class UserServiceProxy {
...
}
class OrderServiceProxy {
...
}
class ProductServiceProxy {
...
}
클래스가 무한히 늘어난다.
그래서
실행 중(Runtime)에
자동으로 Proxy를 생성하는 것이
Dynamic Proxy이다.
Spring은 언제 Proxy를 만들까?
이전 글을 떠올려 보자.
Reflection
↓
Bean 생성
↓
DI
↓
BeanPostProcessor
↓
Container 저장
바로
BeanPostProcessor 단계이다.
Bean 생성
UserService
↓
BeanPostProcessor
↓
@Transactional 발견
↓
Proxy 생성
↓
Container 저장
즉,
Container에는
원본 객체가 아니라
Proxy가 들어갈 수도 있다.
그림으로 보면
Application Start
│
▼
Reflection
│
▼
UserService 생성
│
▼
BeanPostProcessor
│
▼
@Transactional 발견
│
▼
Transaction Proxy 생성
│
▼
BeanContainer 저장
Client 입장에서는
@Autowired
UserService userService;
많은 사람들이
UserService
를 받는다고 생각한다.
실제로는
TransactionProxy
↓
Real UserService
이다.
호출 흐름
userService.save();
↓
Proxy.invoke()
↓
Transaction Begin
↓
Real.save()
↓
Commit
AOP가 가능한 이유
AOP는
Proxy가
메서드 호출을 가로채기 때문이다.
예를 들어
save()
↓
Proxy
↓
로그
↓
트랜잭션
↓
권한 체크
↓
실제 save()
비즈니스 코드는
변경되지 않는다.
Around Advice
가장 많이 사용하는 Advice이다.
Method 호출
↓
Before
↓
Real Method
↓
After
↓
Return
실제로
@Transactional
@Cacheable
@Retryable
모두
Around 방식이다.
Before Advice
Method 호출
↓
로그 출력
↓
Real Method
After Advice
Real Method
↓
로그 출력
↓
Return
Spring에서 Proxy를 사용하는 기능
대표적으로
@Transactional
@Async
@Cacheable
@Retryable
Spring Security
모두 Proxy 기반이다.
실제 @Transactional 흐름
Controller
↓
Service.save()
↓
Transaction Proxy
↓
Connection 생성
↓
ThreadLocal 저장
↓
Real Service
↓
Repository
↓
Commit
↓
ThreadLocal 제거
여기서
이전 글에서 배운
ThreadLocal
이 다시 등장한다.
이전 글들과 연결하면
지금까지의 흐름은
Reflection
↓
Bean 생성
↓
DI
↓
BeanPostProcessor
↓
★★★★★ Dynamic Proxy 생성 ★★★★★
↓
Container 저장
↓
Controller 호출
↓
Proxy가 메서드 가로채기
↓
Transaction
↓
ThreadLocal
↓
Real Method
이것이
Spring Framework 내부의 핵심 흐름이다.
왜 Dynamic Proxy가 중요한가?
만약 Proxy가 없다면
모든 Service는
begin();
try {
save();
commit();
}
catch (...) {
rollback();
}
를 반복해야 한다.
Proxy 덕분에
개발자는
@Transactional
한 줄만 작성하면 된다.
실무에서 절대 기억해야 하는 것
Spring이 관리하는 Bean은
항상
내가 만든 객체
라고 생각하면 안 된다.
실제로는
Proxy
↓
Real Object
인 경우가 매우 많다.
그래서
this.save();
가
@Transactional이 동작하지 않는 이유도 설명된다.
this.save()
↓
Proxy 거치지 않음
↓
Real Object 직접 호출
↓
Transaction 적용 안 됨
이것은 Spring 면접에서 가장 많이 나오는 질문 중 하나이다.
면접 포인트
Dynamic Proxy란?
실행 중(Runtime)에 실제 객체를 감싸는 Proxy 객체를 자동으로 생성하여 메서드 호출 전후에 부가 기능을 추가하는 기술이다.
Spring은 언제 Proxy를 생성하는가?
Bean 생성 이후 BeanPostProcessor 단계에서 @Transactional, @Async, @Cacheable 등의 Annotation을 확인하고 필요한 경우 Proxy를 생성한다.
AOP가 가능한 이유는?
Client가 실제 객체가 아니라 Proxy를 호출하기 때문에 메서드 호출 전후에 원하는 기능을 자유롭게 추가할 수 있다.
왜 this.method()에서는 @Transactional이 동작하지 않는가?
같은 객체 내부 호출은 Proxy를 거치지 않고 실제 객체를 직접 호출하기 때문이다.
다음 글
다음은 JDK Proxy vs CGLIB in Spring이다.
이 글에서는
- Spring은 언제 JDK Proxy를 쓰고 언제 CGLIB를 쓰는지
- 인터페이스가 있으면 JDK Proxy를 사용하는 이유
- Spring Boot 3.x에서 기본값이 CGLIB인 이유
- final 클래스와 final 메서드가 Proxy에서 문제가 되는 이유
- 실무에서 Proxy 타입을 확인하는 방법
까지 깊이 있게 다루면 Spring Proxy에 대한 이해가 완성된다.
'language > java' 카테고리의 다른 글
| Java Proxy 기반 트랜잭션 처리 (0) | 2026.06.17 |
|---|---|
| JDK Proxy vs CGLIB in Spring (0) | 2026.06.17 |
| 객체 생명주기와 Spring Bean (0) | 2026.06.17 |
| Bean 생성 과정 (0) | 2026.06.17 |
| Reflection과 Spring DI (0) | 2026.06.17 |
댓글