본문 바로가기
language/java

Java Proxy 기반 트랜잭션 처리

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

이 글은 Spring 전체 챕터에서 가장 중요한 글이라고 생각한다.

왜냐하면 지금까지 배운 모든 개념이 여기서 하나로 연결되기 때문이다.

Reflection
    ↓
Bean 생성
    ↓
DI
    ↓
BeanPostProcessor
    ↓
Dynamic Proxy
    ↓
JDK Proxy / CGLIB
    ↓
★★★★★ @Transactional ★★★★★
    ↓
ThreadLocal
    ↓
Commit / Rollback
 

많은 사람들이 @Transactional을

"트랜잭션 걸어주는 Annotation"

정도로만 알고 있다.

하지만 실제로는 Spring + Java의 여러 기술이 함께 동작하는 결과물이다.


Java Proxy 기반 트랜잭션 처리

@Transactional 한 줄 뒤에서는 무슨 일이 일어날까?

다음 코드를 보자.

 
@Service
public class OrderService {

    @Transactional
    public void order() {

        orderRepository.save();

        paymentRepository.save();

    }

}
 

우리는 단지

 
orderService.order();
 

를 호출했을 뿐이다.

그런데

Transaction Begin

↓

Order 저장

↓

Payment 저장

↓

Commit
 

이 자동으로 수행된다.


질문을 바꿔보자.

begin()은 누가 호출했을까?

commit()은 누가 호출했을까?

Connection은 어디에 저장되어 있을까?


만약 Spring이 없다면

개발자가 직접 작성해야 한다.

 
public void order() {

    Connection connection = dataSource.getConnection();

    try {

        connection.setAutoCommit(false);

        orderRepository.save(connection);

        paymentRepository.save(connection);

        connection.commit();

    } catch (Exception e) {

        connection.rollback();

    } finally {

        connection.close();

    }

}
 

모든 Service마다

이 코드를 반복해야 한다.


Spring은 어떻게 해결했을까?

Spring은

비즈니스 코드와

트랜잭션 코드를 분리했다.


비즈니스 코드

 
@Transactional
public void order() {

    orderRepository.save();

    paymentRepository.save();

}
 

트랜잭션은

Proxy가 담당한다.


전체 흐름

가장 먼저 전체 그림을 보자.

Controller

↓

Transaction Proxy

↓

TransactionManager.begin()

↓

Connection 생성

↓

ThreadLocal 저장

↓

Real Service

↓

Repository

↓

ThreadLocal에서 Connection 조회

↓

Commit

↓

ThreadLocal 제거
 

1단계

Controller 호출

 
orderService.order();
 

많은 사람들이

Controller

↓

OrderService
 

라고 생각한다.


실제로는

Controller

↓

Transaction Proxy

↓

Real OrderService
 

이다.


2단계

Proxy가 먼저 실행된다.

Proxy는

메서드 호출을 가로챈다.

Proxy.invoke()

↓

Transaction Begin

↓

Real Method 실행

↓

Commit
 

여기까지가

이전 글에서 배운

Dynamic Proxy이다.


3단계

TransactionManager가 등장한다.

Proxy는

PlatformTransactionManager에게 요청한다.

 
transactionManager.getTransaction(...);
 

TransactionManager는

Connection 생성

↓

AutoCommit(false)

↓

Transaction 시작
 

을 수행한다.


4단계

Connection을 어디에 보관할까?

가장 중요한 부분이다.


Service

Repository

DAO


모두 같은 Connection을 사용해야 한다.


그렇다면

매번

 
save(connection);
 

처럼 전달해야 할까?


Spring은 그렇게 하지 않는다.


ThreadLocal 등장

TransactionManager는

Connection을

ThreadLocal에 저장한다.

Thread-1

↓

ThreadLocalMap

↓

Connection
 

이제

현재 Thread는

Connection을 하나 공유한다.


Repository에서는

 
orderRepository.save();
 

만 호출한다.


하지만 내부에서는

 
DataSourceUtils.getConnection();
 

이 실행된다.


그리고

현재 Thread

↓

ThreadLocal

↓

Connection 반환
 

을 수행한다.


Repository는

Connection을 전달받지 않았는데도

같은 Connection을 사용하게 된다.


그림으로 이해

Controller

↓

Transaction Proxy

↓

ThreadLocal(Connection 저장)

↓

OrderService

↓

OrderRepository

↓

ThreadLocal(Connection 조회)

↓

PaymentRepository

↓

ThreadLocal(Connection 조회)
 

두 Repository 모두

같은 Connection을 사용한다.


Commit은 언제 할까?

Real Service가

정상 종료되면

Proxy가 다시 실행된다.


Real Method 종료

↓

Commit

↓

Connection Close

↓

ThreadLocal.remove()
 

예외가 발생하면?

 
@Transactional
public void order() {

    orderRepository.save();

    throw new RuntimeException();

}
 

실행

Begin

↓

save()

↓

Exception

↓

Rollback

↓

Connection Close

↓

ThreadLocal.remove()
 

비즈니스 코드에는

rollback 코드가 하나도 없다.


왜 ThreadLocal.remove()가 중요할까?

Tomcat은

ThreadPool을 사용한다.

Request1

↓

Thread-1

↓

Connection 저장

↓

Request 종료
 

remove()를 하지 않으면

Thread-1

↓

이전 Connection 유지
 

다음 요청

Request2

↓

Thread-1 재사용

↓

이전 Connection 사용
 

심각한 버그가 발생한다.


그래서

Spring 내부는 항상

 
try {

    begin();

    ...

} finally {

    ThreadLocal.remove();

}
 

패턴으로 작성되어 있다.


실제 내부 흐름

Client

↓

Proxy.invoke()

↓

TransactionManager.begin()

↓

Connection 생성

↓

ThreadLocal 저장

↓

Real Service

↓

Repository

↓

ThreadLocal에서 Connection 조회

↓

Commit 또는 Rollback

↓

Connection Close

↓

ThreadLocal.remove()
 

이전 글들과 연결

지금까지 공부한 내용을 연결하면

Reflection

↓

Bean 생성

↓

DI

↓

BeanPostProcessor

↓

Dynamic Proxy 생성

↓

@Transactional 발견

↓

Transaction Proxy 생성

↓

Container 저장

↓

Controller 호출

↓

Proxy.invoke()

↓

TransactionManager

↓

ThreadLocal(Connection)

↓

Repository

↓

Commit / Rollback

↓

ThreadLocal.remove()
 

이것이 Spring Transaction의 전체 구조이다.


왜 @Transactional 내부 호출은 동작하지 않을까?

 
@Service
public class OrderService {

    public void save() {

        order();

    }

    @Transactional
    public void order() {

    }

}
 

실행

save()

↓

this.order()

↓

Real Object 직접 호출

↓

Proxy 안 거침

↓

Transaction 시작 안 됨
 

이 질문은

Spring 면접에서 가장 많이 등장한다.


실무에서 많이 사용하는 구조

Controller

↓

@Transactional Service

↓

Repository

↓

JDBC / JPA

↓

Database
 

Transaction은

Service 계층에 두는 것이 일반적이다.


면접 포인트

@Transactional은 어떻게 동작하는가?

Bean 생성 이후 BeanPostProcessor가 @Transactional을 발견하면 Transaction Proxy를 생성한다. 실제 메서드 호출은 Proxy를 통해 이루어지며, Proxy는 TransactionManager를 이용해 트랜잭션을 시작하고 Connection을 ThreadLocal에 저장한 후 실제 Service를 실행한다. 실행이 끝나면 Commit 또는 Rollback을 수행하고 마지막에 ThreadLocal을 정리한다.


Repository는 Connection을 어떻게 공유하는가?

Connection을 파라미터로 전달하는 것이 아니라 ThreadLocal에 저장하고, DataSourceUtils.getConnection()을 통해 현재 Thread의 Connection을 조회한다.


왜 ThreadLocal.remove()가 중요한가?

ThreadPool 환경에서는 Thread가 재사용되므로 remove를 하지 않으면 이전 요청의 Connection이 남아 Memory Leak이나 잘못된 트랜잭션 공유 문제가 발생할 수 있다.


다음 글 추천

다음은 ThreadLocal과 Spring Security를 추천한다.

이 글은 지금까지 배운 ThreadLocal을 활용하여

로그인

↓

Authentication 생성

↓

SecurityContextHolder

↓

ThreadLocal 저장

↓

Controller 어디서든 로그인 사용자 조회

↓

응답 종료

↓

ThreadLocal.remove()
 

과정을 설명할 수 있다.

그리고 이후 TransactionSynchronizationManager 글과도 자연스럽게 이어져,

Spring 내부에서 ThreadLocal이 얼마나 핵심적인 기술인지 독자가 체감할 수 있는 흐름이 완성된다.

반응형

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

ThreadLocal과 Spring Security  (0) 2026.06.24
ThreadLocal과 TransactionSynchronizationManager  (0) 2026.06.24
JDK Proxy vs CGLIB in Spring  (0) 2026.06.17
Dynamic Proxy와 Spring AOP  (0) 2026.06.17
객체 생명주기와 Spring Bean  (0) 2026.06.17

댓글