@Transactional을 붙였는데 트랜잭션이 적용되지 않아요.
트랜잭션 메서드는 this를 통해 직접 호출하지 말고, 주입된(Injected) 의존성을 통해 호출하세요.
이러한 고민이나 경고를 들어보신 적이 있으신가요!
스프링 프레임워크에서 프로그래밍을 할 때 어쩌면 난처한 상황으로 진행될 수 있는 문제에 미리 글로 정리해봅니다.
스프링은 AOP를 이용하여 @Transactional 어노테이션이 붙은 메서드를 트랜잭션 처리합니다. 또한 AOP를 사용하기 위해 스프링 컨테이너는 프록시 객체를 만들어서 관리합니다.
사용자의 개발을 편리하게 해주는 이 기능들이 동작원리를 모르고 사용한다면 나중에 예상치 못한 문제에 직면할 수 있다고 생각합니다.
Spring은 @Transactional 어노테이션이 붙은 메서드를 호출할 때, 실제 객체를 직접 호출하는 것이 아니라 프록시 객체를 통해 호출합니다. 이 프록시 객체가 바로 트랜잭션 관리의 핵심입니다.
@Service public class UserService { @Transactional public void createUser(User user) { userRepository.save(user); } }
위와 같은 서비스 클래스가 있을 때, Spring은 다음과 같은 과정을 거칩니다.
UserService의 실제 객체 대신 UserService$$EnhancerBySpringCGLIB 같은 프록시 객체를 생성합니다.createUser() 메서드를 호출하면, 실제로는 프록시 객체의 메서드가 먼저 실행됩니다.클라이언트 코드 ↓ [UserService Proxy] ← Spring이 생성한 프록시 객체 ↓ 1. 트랜잭션 시작 2. 실제 메서드 호출 3. 트랜잭션 커밋/롤백 ↓ [실제 UserService 객체] ← 개발자가 작성한 실제 클래스 ↓ 비즈니스 로직 실행
@Service public class UserService { public void processUsers(List<User> users) { for (User user : users) { this.createUser(user); // ❌ 프록시를 거치지 않음! } } @Transactional public void createUser(User user) { userRepository.save(user); // 만약 여기서 예외가 발생하면? } }
위 코드에서 processUsers() 메서드는 프록시를 통해 호출되지만, this.createUser(user) 호출은 프록시를 거치지 않고 실제 객체의 메서드를 직접 호출합니다.
클라이언트 ↓ [UserService Proxy] ↓ processUsers() 호출 ↓ [실제 UserService 객체] ↓ this.createUser() 직접 호출 ← ❌ 프록시 우회! ↓ @Transactional 무시됨
이러한 이유로 Transaction이 적용되지 않습니다.
이는 @Async, @Cacheable 같은 다른 AOP 어노테이션도 같은 문제가 적용됩니다.
기본적으로 클래스를 분리하면 각각의 프록시 빈으로 주입되어 Self Invocation 문제가 해결됩니다.
@Service public class UserService { private final UserCreationService userCreationService; public void processUsers(List<User> users) { for (User user : users) { userCreationService.createUser(user); // 다른 빈을 통한 호출 } } } @Service public class UserCreationService { @Transactional public void createUser(User user) { userRepository.save(user); } }
다른 빈을 주입하듯이, 직접 자신의 클래스를 의존관계 주입하면 본인의 프록시 빈을 호출함으로서 문제를 해결할 수 있습니다. 하지만 순환 참조 문제가 발생하므로 @Lazy를 붙여주어야 한다는 단점이 있습니다.
@Service public class UserService { @Autowired @Lazy // 순환 참조 해결 private UserService self; public void processUsers(List<User> users) { for (User user : users) { self.createUser(user); // 프록시를 통한 호출 } } @Transactional public void createUser(User user) { userRepository.save(user); } }
근본적으로 @Transactional 어노테이션 대신 스프링 프레임워크에서 제공하는 TransactionTemplate를 사용하여 해결할 수 있습니다. 선언적 트랜잭션대신 프로그래밍 방식 트랜잭션을 사용합니다.
하지만 다른 AOP 기능은 해결하지 못하는 것이 단점입니다.
@Service public class UserService { @Autowired private TransactionTemplate transactionTemplate; public void processUsers(List<User> users) { for (User user : users) { // 트랜잭션 경계를 명시적으로 설정 transactionTemplate.executeWithoutResult(status -> { createUser(user); }); } } private void createUser(User user) { userRepository.save(user); } }
프록시 대신 바이트코드 위빙 방식을 사용하여 Self Invocation 문제를 근본적으로 해결할 수 있습니다.
4.1. 설정 클래스 생성
@Configuration @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) public class AspectJTransactionConfig { }
4.2. XML 설정파일 생성
<!-- pom.xml --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.14.0</version> <configuration> <complianceLevel>17</complianceLevel> <source>17</source> <target>17</target> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </dependency>
4.3. 코드에서 바이트코드 위빙 사용
@Service public class UserService { private final UserRepository userRepository; public void processUsers(List<User> users) { for (User user : users) { this.createUser(user); // ✅ AspectJ 위빙으로 트랜잭션 적용 } } @Transactional public void createUser(User user) { userRepository.save(user); // self-invocation도 문제없이 트랜잭션 적용 this.updateUserStats(user.getId()); } @Transactional(propagation = Propagation.REQUIRES_NEW) private void updateUserStats(Long userId) { // private 메서드도 트랜잭션 적용 가능 } }
이러한 동작 원리와 해결 방법들을 이해하면 Spring의 트랜잭션 관리를 더 효과적으로 활용할 수 있고, 예상치 못한 버그를 미연에 방지할 수 있습니다.
이러한 문제는 컴파일 타임에 발견하기 어려운 문제입니다. 여전히 정상적으로 애플리케이션은 실행됩니다. 하지만 조심해야하는 이유는 프로덕션 환경에서 심각한 문제로 이어질 수도 있기 때문입니다 (ex. 결제에서 트랜잭션이 적용안된다면…)
SonarQube와 같은 정적 코드 분석 도구를 사용하면 사전에 문제를 감지하고 수정하여 예방할 수 있습니다. 저 또한 이 문제를 Code Smell로 발견했다는 보고서를 통해 공부할 수 있었습니다.


TransactionTemplate 사용 (3번 방법)