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(...); // 프록시를 거침!
}
}
배운 것
- Spring
@Transactional은 프록시 기반이다. 같은 클래스 내부 호출에서는 작동하지 않는다. - 에러 없이 조용히 발생한다.
@Transactional이 붙어있으니 당연히 될 거라 생각하기 쉽다. - Repository 레벨
@Modifying은 self-invocation과 비동기 문제를 동시에 해결해주는 실용적인 패턴이다.