1. @Transactional의 우선순위
@Transactional은 적용되는 위치에 따라 다음과 같은 우선순위를 가진다. 구체적인 메소드에 가까울수록 우선순위가 높다.
- 클래스 메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스 순으로 적용된다.
2. @Transactional의 작동 모드 (Proxy vs. Aspect)
@Transactional의 모드에는 Proxy Mode와 Aspect Mode가 있으며, 기본값(Default)은 Proxy Mode다.
Proxy Mode (기본 모드)
이 모드는 Public 메소드에 적용해야 한다. Protected나 Private 메소드에 선언해도 트랜잭션이 작동하지 않는다.
- @Transactional은 프록시 객체 외부에서 접근해야만 AOP(Aspect-Oriented Programming)가 적용된다.
- Self-invocation (내부 호출) 상황에서는 트랜잭션이 동작하지 않는다. 즉, @Transactional이 적용되지 않은 Public 메소드가 같은 클래스 내의 @Transactional이 적용된 Public 메소드를 호출할 경우 트랜잭션이 동작하지 않는다. 이는 내부 메소드 호출 시 프록시 객체가 감싸지지 않기 때문이다.
CGLIB (Code Generator Library) 동작 방식
- Enhancer 클래스로 프록시를 구현했다. JDK Dynamic Proxy와 다르게 Reflection을 사용하지 않고, 상속(Extends)을 이용해 프록시화 할 메소드를 오버라이딩하는 방식이다.
- 기본적으로 바이트 코드를 조작하여 바이너리를 생성하기 때문에 JDK Dynamic Proxy보다 성능이 좋다고 평가된다.
- final 객체나 private 접근자로 된 메소드는 상속이 지원되지 않는다. 따라서 해당 메소드에서는 프록시가 작동하지 않는다.
- Spring Boot는 기본적으로 CGLIB 프록시를 사용하도록 설정되었다. 즉, 인터페이스 유무와 관계없이 CGLIB를 사용해 AOP가 가능하다.
- (참고) Spring은 프록시 객체를 사용하기 때문에 인터페이스가 필요할 때가 있었다.
Aspect Mode
Non-Public 메소드에 트랜잭션을 적용할 때 고려한다. 이 모드는 직접 바이트 코드를 주입하는 형식이기 때문에, 자신의 클래스 내부 메소드를 호출해도 트랜잭션 코드가 포함된 메소드를 호출하게 된다.
@EnableTransactionManagement(proxyTargetClass = true, mode = AdviceMode.ASPECTJ)
3. 예외 처리 범위
@Transactional은 기본적으로 **Unchecked Exception (Runtime Exception)**만 관리하며 롤백을 수행한다. **Checked Exception (IOException, SQLException 등)**은 관리 대상이 아니므로 기본적으로 롤백되지 않는다.
- Unchecked Exception 발생 시 롤백이 가능하다.
- Checked Exception 발생 시 롤백이 불가능하다.
@Transactional(rollbackFor = Exception.class)를 사용하면 Checked Exception에 대해서도 롤백을 수행할 수 있게 된다. 그러나 Checked Exception이 발생하는 상황이라면, 해당 명령어를 사용하기보다 try...catch 블록을 사용해 명시적으로 예외를 발생시키는 것이 더 좋은 방법이다.
4. JPA 단일 작업 시 트랜잭션 선언 여부
JPA를 사용할 때 단일 작업에 대해 @Transactional을 직접 선언할 필요가 없을 수도 있다. JPA 구현체(예: Hibernate)는 기본적으로 대부분의 CRUD 메소드에 @Transactional을 선언해두었기 때문이다.
5. 부모 메소드에 트랜잭션 선언
여러 작업이 필요한 경우, 전체 트랜잭션을 하나의 단위로 묶기 위해 부모 메소드에 @Transactional을 선언하면 된다. 이 메소드가 전체 트랜잭션의 부모(컨테이너)가 된다.
6. 다른 클래스 간의 트랜잭션 호출
@Transactional이 선언된 메소드를 다른 클래스의 @Transactional 메소드에서 호출할 때 프록시가 잘 작동한다. 이들은 각각 프록시로 감싸여 있으므로 같은 트랜잭션 내에서 사용이 가능하다.
7. 같은 클래스 내에서 @Transactional 메소드 호출 (내부 호출)
같은 클래스 내에서 여러 작업을 수행하며 메소드별로 @Transactional을 선언하고 내부적으로 호출하는 경우, 같은 트랜잭션을 바라보지 않는다.
classtestClass {
publictest1() {
save();
test2();
}
@Transactional
publictest2() {
save2();
}
}
위와 같은 경우, test1() 안에서 test2()를 호출하면 동일한 Bean(Class) 내에서 호출이 발생하기 때문에 트랜잭션이 적용되지 않는다.
이는 Spring의 AOP 프록시가 확장되지 않고, 서비스 인스턴스를 래핑하여 호출을 가로채기 때문이다. 즉, test2()는 프록시로 감싸여 있지 않은 상태로 내부 호출되었기 때문이다.
이 문제를 해결하고 같은 트랜잭션을 바라보게 하는 방법은 세 가지가 있다.
- 클래스 분리: 같은 트랜잭션에서 실행되어야 하는 메소드 중 부모가 아닌 메소드(위 예시의 test2())를 다른 클래스로 분리하여 외부에서 호출하도록 한다.
- Self Autowiring: 자기 자신을 @Autowired로 주입받아 사용한다. Spring 4부터 Self Autowired 기능을 사용할 수 있게 되었다. 이 경우 생성자 주입 대신 필드에 @Autowired를 사용하여 순수한 메소드가 아닌 프록시로 감싸진 메소드를 사용하도록 해야 한다.
- @Transactional 메소드는 클래스 내부적으로 사용하지 말고 밖에서 호출하거나, 내부적으로 사용하려면 자기 자신의 프록시를 주입받아 사용해야 한다.
- TransactionTemplate 사용: Spring이 제공하는 TransactionTemplate을 사용하여 트랜잭션 경계를 명시적으로 설정한다.
'Back-end > SpringBoot' 카테고리의 다른 글
| Oracle + Quartz Batch 환경에서 PK Duplicate가 발생하는 이유 (1) | 2025.12.23 |
|---|---|
| 배치가 밀릴 때 발생할 수 있는 락(Lock) 이슈와 해결 방안 (0) | 2025.12.01 |
| [Security] 내 security filter chain 구조 (0) | 2025.06.22 |
| [Security] refresh token rotation 방식을 쓰는 이유 (0) | 2025.06.03 |
| [Security] JJWT 적용.... (0) | 2025.05.24 |