2025년 4월 23일
카테고리 : DB이론
조회 : 77|2분 읽기
@Transactional, 잘 사용하고 있나??
@Transactional, 정말 잘 알고 쓰고 있나요?
@Transactional은 스프링에서 가장 많이 쓰이지만, 가장 많이 오해되는 기능 중 하나입니다. 이 글에서는 단순한 사용법을 넘어서, 실무에서 겪는 다양한 오작동 사례와 그 원인을 정리하고, 안전하게 사용하는 방법까지 친절히 설명합니다.
1. @Transactional은 무엇인가?
Spring의 @Transactional은 선언형 트랜잭션 관리 방식입니다. 해당 어노테이션이 붙은 메서드는 AOP 프록시를 통해 트랜잭션 시작 → 커밋/롤백 흐름이 자동으로 관리됩니다.
📌 기본 동작 흐름
1. 메서드 호출 시 트랜잭션 시작
2. 메서드가 정상 종료되면 커밋
3. 런타임 예외 발생 시 롤백
java1@Transactional 2public void serviceMethod() { 3 // 트랜잭션 시작 4 repository.save(); 5 // 트랜잭션 커밋 or 롤백 6}
하지만 이 동작이 항상 바람직한 것은 아닙니다. 트랜잭션은 커넥션을 장시간 점유하고, 불필요한 lock이나 set autocommit=0 같은 제어 쿼리를 발생시킬 수 있어 오히려 성능을 저하시킬 수 있습니다.
특히 @Transactional(readOnly = true)를 사용하더라도 트랜잭션 세션은 유지되며, DB에 따라서는 커밋을 요구하거나 리소스를 점유하는 문제가 생길 수 있습니다. 따라서 단순 조회에는 트랜잭션을 아예 사용하지 않는 것도 고려해볼 수 있습니다.
2. AOP와 프록시, 그리고 @Transactional의 동작 원리
Spring에서 @Transactional은 프록시 기반 AOP에 의해 작동합니다.
간단히 말하면, @Transactional이 붙은 메서드는 진짜 메서드가 아니라 프록시 객체가 대신 호출하는 구조로 되어 있습니다.
간단히 말하면, @Transactional이 붙은 메서드는 진짜 메서드가 아니라 프록시 객체가 대신 호출하는 구조로 되어 있습니다.
🔧 예시 흐름
- UserService 클래스에 @Transactional이 붙어 있다면
- Spring은 이 클래스의 프록시(UserService$$EnhancerBySpringCGLIB)를 만들어 등록함
- 메서드 호출은 항상 이 프록시 객체를 통해 이루어짐
- 프록시는 트랜잭션을 시작하고, 메서드가 끝나면 커밋 or 롤백을 수행함
📎 중요: 프록시를 우회하면 트랜잭션이 작동하지 않습니다.
즉, 같은 클래스 내부 메서드 호출처럼 프록시를 거치지 않으면 @Transactional은 무시됩니다.
3. 프록시의 한계: 내부 호출 무시
java1@Service 2public class MyService { 3 4 @Transactional 5 public void outer() { 6 inner(); // 트랜잭션 안 잡힘 7 } 8 9 @Transactional 10 public void inner() { 11 // 트랜잭션이 적용되지 않음 12 } 13}
💡 해결 방법
- inner() 메서드를 다른 Bean으로 분리
- 혹은 self-injection 방식으로 자기 자신을 주입받아 호출
java1@Autowired 2private MyService self; 3 4public void outer() { 5 self.inner(); // 프록시 경유 → 트랜잭션 작동 6}
4. 예외에 따른 rollback 여부
Spring은 기본적으로 RuntimeException이 발생할 경우에만 자동으로 rollback합니다. checked exception은 rollback 대상이 아니기 때문에, 예외가 발생해도 커밋될 수 있습니다.
java1@Transactional 2public void process() throws IOException { 3 throw new IOException(); // rollback 안 됨 4}
💡 해결 방법
java1@Transactional(rollbackFor = Exception.class)
- 명시적으로 롤백할 예외 타입을 지정
5. 외부 API와 함께 쓸 때 주의사항
트랜잭션 안에서 외부 시스템을 호출하면, 트랜잭션 롤백 여부와 무관하게 외부 요청은 이미 처리돼버립니다.
java1@Transactional 2public void registerUser(User user) { 3 userRepository.save(user); 4 externalApi.sendNotification(user); // 이건 롤백되지 않음 5 throw new RuntimeException(); 6}
💡 해결 방법
- 외부 호출은 트랜잭션 외부로 분리 (이벤트 기반 설계 또는 Outbox Pattern 사용)
6. 전파 속성(Propagation) 제대로 쓰기
Spring의 트랜잭션은 전파 수준에 따라 다른 트랜잭션을 생성하거나 기존 것을 공유할 수 있습니다.
| 속성 | 설명 |
|---|---|
| REQUIRED | 기본값. 기존 트랜잭션이 있으면 참여, 없으면 새로 생성 |
| REQUIRES_NEW | 무조건 새로운 트랜잭션 생성 |
| NESTED | 부모 트랜잭션 내 savepoint 사용 |
java1@Transactional(propagation = Propagation.REQUIRES_NEW) 2public void writeLog() { 3 logRepository.save(...); 4}
7. flush 타이밍도 기억하자 (JPA)
JPA의 save()는 바로 DB에 반영되지 않습니다. 트랜잭션 커밋 직전 flush()가 일어나야 실제 쿼리가 실행됩니다.
java1userRepository.save(user); 2externalService.call(); // 여기서 예외 발생 시 DB엔 아직 반영되지 않음
🧭 마무리하며
@Transactional은 단순한 선언 하나로 끝나는 게 아닙니다.
적용 방식, 호출 경로, 예외 타입, 외부 연동, 전파 설정 등 실무에서 고려해야 할 포인트가 많습니다.
적용 방식, 호출 경로, 예외 타입, 외부 연동, 전파 설정 등 실무에서 고려해야 할 포인트가 많습니다.
그리고 무엇보다 중요한 건 트랜잭션을 정말 필요로 하는 구간에만 적용하는 것입니다.
불필요하게 트랜잭션을 남용하면 DB 리소스를 낭비하고, 오히려 성능 저하를 유발할 수 있습니다.
불필요하게 트랜잭션을 남용하면 DB 리소스를 낭비하고, 오히려 성능 저하를 유발할 수 있습니다.
정말로 트랜잭션을 믿고 쓰고 싶다면, 어떻게 작동하는지를 정확히 이해하고,
어디서부터 어디까지 트랜잭션이 적용되는지 명확히 선을 긋는 것이 필요합니다.
어디서부터 어디까지 트랜잭션이 적용되는지 명확히 선을 긋는 것이 필요합니다.