Spring이 제공하는 트랜잭션의 기능
- 트랜잭션 동기화: 컨텍스트 내에서 일관된 처리
- 트랜잭션 추상화: jdbc, jpa 등의 다양한 인터페이스를 동일한 방식으로 제공
- AOP를 이용한 트랜잭션 분리: 트랜잭션이라는 관심사(Aspect)를 분리해서 비즈니스에 집중시킴
AOP(Aspect Oriented Programming)은 흩어진 관심사를 별도의 클래스로 모듈화하는 방식을 말한다.
- Aspect: 애플리케이션 내에서 중복으로 존재하는 부가 기능(advice)을 모듈화
- Advice: Aspect에 구현된 실제의 부가 기능 코드
- Pointcut: Aspect를 어디에 적용할지 지정 (언제)
- EX) @GetMapping시에만, UserService만 등의 언제 실행할지 정하는 조건문
- JointPoint: Advice가 실행될 수 있는 지점 (어디에)
- EX) 메서드 실행 직전, 실행 직후 등의 코드에서 갈 수 있는 위치
Spring AOP는 프록시 객체를 자동으로 생성해서 Aspect/Advice에 직접적으로 의존하지 않도록 해준다.
그럼 프록시 패턴은 어떤 의미인가?
객체를 대리(proxy)하는 객체를 통해 대상 객체에 접근하는 방식
상위 인터페이스를 상속하는 프록시 클래스 생성, 여기서 부가 기능 관련된 기능을 처리한다.
→ target에서 aspect를 알 필요 없이 순수한 비즈니스 로직에 집중할 수 있다
트랜잭션 처리를 위한 @Transactional 애노테이션은 Spring AOP의 대표적인 예이다.
@Transactional 역시 Proxy 형태로 동작한다.
Spring은 JDK Proxy, Spring Boot는 CGLIb Proxy를 기본으로 하기 때문에, 사용하는 것에 따라 생성된 프록시 객체 형태는 다를 수 있다.
JDK Proxy, CGLib Proxy: Target의 어떤 부분을 상속 받아서 프록시를 구현하느냐?
- target에 대한 호출이 들어오면 AOP 프록시가 intercept
- 프록시에서 Transaction Advisor가 Commit, Rollback 등의 트랜잭션 처리
- 각 Advisor에서 부가 기능을 처리하면 Target Method를 수행한다.
다이나믹 프록시(Dynamic Proxy)
프록시의 문제점: 클라이언트가 매번 프록시를 작성해야한다.
그러니까, Target 수만큼 프록시 클래스가 많아져야한다.
JDK Reflect를 이용해서 매번 프록시를 작성하지 않고, 런타임에서 프록시를 동적(dynamic) 생성
- 필요한 부가 기능은 InvocationHandler를 통해 직접 구현
- reflect.Proxy가 생성하는 Proxy 클래스를 사용
스프링 ProxyFactoryBean은 Advice와 Pointcut으로 구성된다.
1개 이상의 어드바이스와 포인트컷이 만나면, 하나의 Advisor가 되어서 ProxyFactoryBean을 통해 다이나믹 프록시를 생성한다.
프록시를 이용한 AOP 구현
근데 일반적으로는 프록시 기반 AOP 정도면 충분하고, 아주 드물게 AspectJ를 사용할 수 있다.
바이트코드 생성, 조작으로 AOP 구현 (feat. AspectJ)
AspectJ가 유연하고 강력한 특징이 있지만.. 스프링 컨테이너나 DI 도움 없이 가능해서 좋다.
선언적 트랜잭션 - AOP를 통한 트랜잭션 제어
핵심 로직과 별개의 관심사(트랜잭션 처리, 로깅)을 분리해서, 자동으로 끼워넣는 AOP
Spring은 AOP를 이용해서 트랜잭션 제어를 런타임시 감싸는 프록시 객체를 생성한다.
@Transactional
public void doSomething() {
userRepository.save(...);
}
이 메소드를 실행하면 직접 실행되지 않고, 스프링이 만든 프록시 객체가 먼저 호출된다.
YourClassProxy (프록시 객체)
└── TransactionAspect (before advice: 트랜잭션 시작을 수행)
└── YourClass.doSomething()
└── TransactionAspect (after advice: 커밋 or 롤백을 수행)

- 외부 호출 (External Call): MyService의 outerMethod()가 외부(예: 컨트롤러나 다른 서비스)에서 호출된다. 이 호출은 Spring이 생성한 MyService의 프록시 객체를 통과한다.
- 프록시의 역할: 프록시는 outerMethod()에 붙은 @Transactional 어노테이션을 감지하고, 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 등의 AOP 로직을 수행한다.
- 내부 호출 (Internal Call): outerMethod()의 실제 구현체 내부에서 this.innerMethod()를 호출한다. 이 호출은 프록시를 거치지 않고, MyService의 원본 객체에 있는 innerMethod()를 직접 호출한다.
- 트랜잭션 미적용: innerMethod()에도 @Transactional이 붙어 있지만, 프록시를 통하지 않았기 때문에 innerMethod()를 위한 별도의 트랜잭션 AOP 로직(새 트랜잭션 시작, 전파 규칙 적용 등)이 실행되지 않는다. innerMethod()는 단순히 outerMethod()가 이미 시작한 트랜잭션 컨텍스트 안에서 실행된다.
@Transactional 사용시 실수할 수 있는 부분
1. private 메소드는 트랜잭션 처리할 수 없다: 당연히 AOP 원리를 이해한다면 납득할 수 있다!
해당 메서드를 외부에서 호출할 수 있어야 프록시 객체도 메서드를 가로챌 수 있다.
private 메서드는 클래스 내부에서만 접근 가능하기에 프록시 객체를 생성할 수 없다.
2. 같은 클래스 내의 메서드 호출은 트랜잭션 처리되지 않음:
생성된 프록시 객체는 클래스 내부의 다른 메서드를 직접 호출하기에 가로챌 수 없다.
예시를 들어보자.
@Service
public class MyService {
@Transactional
public void outerMethod() {
...
innerMethod();
}
@Transactional
public void innerMethod() {
...
}
}
다음과 같은 경우에는 outerMethod()를 호출하기 위해서 프록시 객체가 생성된다.
하지만 내부에서 호출되는 innerMethod()는 같은 클래스라 프록시 객체가 새로이 가로챌 수 없고 기존 메서드를 호출한다.
outerMethod()에 대한 트랜잭션은 정상적으로 시작되지만, innerMethod()는 프록시 객체를 거치지 않고 실제 인스턴스의 메서드를 직접 호출하기에 프록시 우회가 발생한다.
이는 @Transactional의 옵션을 Propagation.REQUIRES_NEW로 설정해도 innerMethod()의 트랜잭션은 똑같이 무시된다.
프록시를 통하지 않는 내부 호출로 인해서 @Transactional 자체가 활성화되지 않기에 propagation 옵션이 뭐던간에 무시된다.
트랜잭션의 범위와 전파(Propagation)
하위 메서드에서 @Transactional 어노테이션이 존재해도, 같은 트랜잭션 범위에서 작업이 일어난다. (default= Propagation.REQUIRED)
물리 트랜잭션: 실제 데이터베이스에 적용되는 실질 트랜잭션이 커밋되거나 롤백되는 단위
논리 트랜잭션: 스프링에서 트랜잭션 매니저를 통해 관리되는 프로그래밍 수준의 트랜잭션
Propagation.REQUIRED : 기존 트랜잭션이 있으면 사용하되, 없으면 새로 만든다.
Propagation.REQUIRES_NEW: 항상 새로운 트랜잭션이 필요하다.
- AOP를 통해서 프록시 객체를 생성하고, 거기서 트랜잭션을 자동으로 처리한다.
- 따라서 Bean 클래스에서 중첩되어 작성된 Transactional을 인지하지 못하기에, 클래스를 분리해야함
Propagation.MANDATORY
Propagation.SUPPORTS: 기존 트랜잭션이 있으면 사용하되, 없으면 없이 진행한다.
Propagation.NOT_SUPPORTED: 기존 트랜잭션이 있어도 없이 진행한다.
'backend' 카테고리의 다른 글
| Hack-Playground: 부산대-부경대 해킹캠프 회고 (0) | 2025.11.10 |
|---|---|
| 토스가 겪은 Reactor Netty의 Memory Leak 이슈를 알아보자 (4) | 2025.08.18 |
| JVM 튜닝 목적으로 Heap 덤프 분석은 처음 해봐요 (feat. Eclipse MAT) (0) | 2025.06.13 |
| What Color is Your Function? (feat. 함수의 색 전염성) (0) | 2025.03.12 |
| Java도 한다 경량 쓰레드 (Virtual Thread) (0) | 2025.02.26 |