본문 바로가기
language/java

Dynamic Proxy와 Spring AOP

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

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

댓글