목록으로

Spring @Transactional이 무시되는 순간 — Self-Invocation 함정

6

Spring @Transactional이 무시되는 순간 — Self-Invocation 함정

2026-02-03


발단

대량 푸시 발송 서비스에서 진행률 업데이트가 안 되는 버그가 있었다.

@Transactional
protected void updateProgress(Long requestId, int sentCount, int failCount) {
    Entity entity = repository.findById(requestId).orElseThrow();
    entity.setSentCount(sentCount);
    entity.setFailCount(failCount);
    // JPA dirty checking(엔티티 값이 바뀌었는지 자동 감지해서 UPDATE 쿼리를 날려주는 JPA 기능)으로 자동 UPDATE... 될 줄 알았다
}

@Transactional도 붙어있고 코드도 맞다. 근데 DB가 안 바뀐다.


Spring AOP 프록시가 뭔데?

→ AOP(Aspect-Oriented Programming)는 여러 곳에 반복되는 공통 기능(로깅, 트랜잭션 등)을 한 곳에서 관리하는 프로그래밍 기법이다. 프록시는 원래 객체를 감싸서 메서드 호출 전후에 추가 동작을 끼워 넣는 대리 객체다.

@Transactional은 마법이 아니다. Spring이 프록시 객체를 만들어서 트랜잭션을 감싸준다.

외부에서 호출:
client → [프록시] → MyService.method()
           ↑
     여기서 트랜잭션 시작/커밋/롤백

외부에서 호출하면 프록시를 거치니까 @Transactional이 작동한다.


문제: 같은 클래스 안에서 호출하면?

public class PushService {

    public void sendAll(...) {
        // ... 발송 로직 ...
        this.updateProgress(requestId, sent, failed);  // ← 여기!
    }

    @Transactional
    protected void updateProgress(...) { ... }
}

this.updateProgress()는 프록시를 거치지 않는다. 자기 자신을 직접 호출하는 거니까.

sendAll()
  └── this.updateProgress()  ← 프록시 안 거침
       └── @Transactional 무시됨
       └── 트랜잭션 없이 실행
       └── dirty checking 안 됨 → UPDATE 안 나감

이게 self-invocation(자기 호출) 문제다. 같은 클래스 안에서 this로 자기 메서드를 호출하면 프록시를 우회하기 때문에 AOP가 적용되지 않는 현상이다.


해결

서비스 메서드 대신 Repository에서 직접 JPQL UPDATE를 날리도록 바꿨다.

// Repository — @Modifying + @Query는 프록시와 무관하게 작동
@Modifying
@Transactional
@Query("UPDATE PushRequest e SET e.sentCount = :sent, e.failCount = :failed WHERE e.id = :id")
int updateProgress(@Param("id") Long id, @Param("sent") int sent, @Param("failed") int failed);

@Modifying(이 쿼리가 데이터를 변경하는 쿼리임을 Spring Data JPA에 알려주는 어노테이션) + @Query는 SQL을 직접 실행하니까 dirty checking에 의존하지 않는다.


Self-Invocation 해결법 3가지

방법장점단점
다른 빈으로 분리가장 정석클래스가 늘어남
Repository @Modifying간단, 직관적JPQL 직접 작성 필요
자기 자신을 주입최소 변경순환 참조(A가 B를 필요로 하고 B가 A를 필요로 하는 무한 루프) 위험
// 방법 3 참고: 자기 자신 주입
@Service
public class PushService {
    @Lazy @Autowired
    private PushService self;

    public void process() {
        self.updateProgress(...);  // 프록시를 거침!
    }
}

배운 것

  1. Spring @Transactional은 프록시 기반이다. 같은 클래스 내부 호출에서는 작동하지 않는다.
  2. 에러 없이 조용히 발생한다. @Transactional이 붙어있으니 당연히 될 거라 생각하기 쉽다.
  3. Repository 레벨 @Modifying은 self-invocation과 비동기 문제를 동시에 해결해주는 실용적인 패턴이다.