2025년 4월 23일
이론
카테고리 : DB
조회 : 77|2분 읽기

@Transactional, 잘 사용하고 있나??

@Transactional, 정말 잘 알고 쓰고 있나요?

@Transactional은 스프링에서 가장 많이 쓰이지만, 가장 많이 오해되는 기능 중 하나입니다. 이 글에서는 단순한 사용법을 넘어서, 실무에서 겪는 다양한 오작동 사례와 그 원인을 정리하고, 안전하게 사용하는 방법까지 친절히 설명합니다.

1. @Transactional은 무엇인가?

Spring의 @Transactional은 선언형 트랜잭션 관리 방식입니다. 해당 어노테이션이 붙은 메서드는 AOP 프록시를 통해 트랜잭션 시작 → 커밋/롤백 흐름이 자동으로 관리됩니다.

📌 기본 동작 흐름

1. 메서드 호출 시 트랜잭션 시작
2. 메서드가 정상 종료되면 커밋
3. 런타임 예외 발생 시 롤백
java
1@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이 붙은 메서드는 진짜 메서드가 아니라 프록시 객체가 대신 호출하는 구조로 되어 있습니다.

🔧 예시 흐름

  • UserService 클래스에 @Transactional이 붙어 있다면
  • Spring은 이 클래스의 프록시(UserService$$EnhancerBySpringCGLIB)를 만들어 등록함
  • 메서드 호출은 항상 이 프록시 객체를 통해 이루어짐
  • 프록시는 트랜잭션을 시작하고, 메서드가 끝나면 커밋 or 롤백을 수행함
📎 중요: 프록시를 우회하면 트랜잭션이 작동하지 않습니다.
즉, 같은 클래스 내부 메서드 호출처럼 프록시를 거치지 않으면 @Transactional은 무시됩니다.

3. 프록시의 한계: 내부 호출 무시

java
1@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 방식으로 자기 자신을 주입받아 호출
java
1@Autowired
2private MyService self;
3
4public void outer() {
5    self.inner(); // 프록시 경유 → 트랜잭션 작동
6}

4. 예외에 따른 rollback 여부

Spring은 기본적으로 RuntimeException이 발생할 경우에만 자동으로 rollback합니다. checked exception은 rollback 대상이 아니기 때문에, 예외가 발생해도 커밋될 수 있습니다.
java
1@Transactional
2public void process() throws IOException {
3    throw new IOException(); // rollback 안 됨
4}

💡 해결 방법

java
1@Transactional(rollbackFor = Exception.class)
  • 명시적으로 롤백할 예외 타입을 지정

5. 외부 API와 함께 쓸 때 주의사항

트랜잭션 안에서 외부 시스템을 호출하면, 트랜잭션 롤백 여부와 무관하게 외부 요청은 이미 처리돼버립니다.
java
1@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 사용
java
1@Transactional(propagation = Propagation.REQUIRES_NEW)
2public void writeLog() {
3    logRepository.save(...);
4}

7. flush 타이밍도 기억하자 (JPA)

JPA의 save()는 바로 DB에 반영되지 않습니다. 트랜잭션 커밋 직전 flush()가 일어나야 실제 쿼리가 실행됩니다.
java
1userRepository.save(user);
2externalService.call(); // 여기서 예외 발생 시 DB엔 아직 반영되지 않음

🧭 마무리하며

@Transactional은 단순한 선언 하나로 끝나는 게 아닙니다.
적용 방식, 호출 경로, 예외 타입, 외부 연동, 전파 설정 등 실무에서 고려해야 할 포인트가 많습니다.
그리고 무엇보다 중요한 건 트랜잭션을 정말 필요로 하는 구간에만 적용하는 것입니다.
불필요하게 트랜잭션을 남용하면 DB 리소스를 낭비하고, 오히려 성능 저하를 유발할 수 있습니다.
정말로 트랜잭션을 믿고 쓰고 싶다면, 어떻게 작동하는지를 정확히 이해하고,
어디서부터 어디까지 트랜잭션이 적용되는지 명확히 선을 긋는 것이 필요합니다.