최근 기존 프로젝트에 새로운 요구사항이 생겨 기능을 확장하게 되었습니다. 코드를 살펴보던 중 서비스 레이어와 레포지토리 레이어에 똑같이 @Transactional 어노테이션이 붙어 있는 메소드를 발견했는데요,
처음엔 이렇게 생각했습니다.
“어? 이렇게 트랜잭션이 중첩되면 충돌이 나거나 오류가 생기는 거 아닌가?”
하지만 실제로는 아무 문제 없이 잘 동작하고 있었고, 심지어 롤백도 정확히 되더군요. 알고 보니 트랜잭션의 전파 속성때문이였습니다. 이마를 탁..🤦♀️
이번 글에서는 다음 세 가지를 중심으로 트랜잭션의 동작 원리를 정리해봅니다.
스프링에서 프록시 기반이란?
@Transactional이 어떻게 동작하는지 예제 코드
전파 속성
결론: 그래서 @Transactional 어디까지 붙이나요?
스프링은 AOP(관점 지향 프로그래밍)를 활용해 트랜잭션 기능을 제공합니다. 이 AOP는 바로 프록시(proxy)를 통해 구현되는데요.
프록시는 원본 객체를 감싸는 껍데기 객체로, 호출을 가로채 필요한 부가 로직을 추가할 수 있는 구조입니다.
예를 들어, MyService 클래스가 있으면, Spring은 실제 객체 대신 다음과 같이 프록시 객체를 생성합니다.
Client --> MyServiceProxy --> (Real) MyService
클라이언트는 실제 서비스가 아니라 프록시 객체를 호출하게 되며, MyServiceProxy는 트랜잭션 시작/커밋/롤백 처리를 합니다.
<예제 코드>
@Service
public class OrderService {
@Transactional
public void createOrder() {
orderRepository.save();
dealServcie.changeStatus(); // 여기서 예외 발생 시 전체 롤백
}
}
위 코드는 createOrder()가 호출되면 프록시가 트랜잭션을 시작합니다.
내부에서 예외가 발생하면 트랜잭션은 자동으로 롤백됩니다.
아무 문제 없이 끝나면 커밋됩니다.
트랜잭션 전파 속성은 현재 트랜잭션이 있는 상태에서 메서드가 호출될 때 어떤 방식으로 동작할지를 지정하는 설정입니다.
| 옵션 | 설명 |
|---|---|
REQUIRED (기본값) | 현재 트랜잭션 있으면 참여, 없으면 새로 시작 |
REQUIRES_NEW | 항상 새 트랜잭션 시작 (기존 트랜잭션은 일시 중단) |
NESTED | 트랜잭션 중첩 (savepoint 기반) |
보통 기본 @Transactional 어노테이션을 사용하면 기본값으로 동작하는데,
기본적으로 가장 외부에서 호출된 메서드 기준으로 트랜잭션이 시작됩니다.
<예제 코드>
@Service
@Transactional // 여기에 붙음
public class MyService {
public void doSomething() {
repository.save(); // Repository에도 @Transactional 있음
}
}
@Repository
@Transactional
public class MyRepository {
public void save() {
// DB 저장 로직
}
}
MyService.doSomething()이 호출되면서 트랜잭션 시작
내부적으로 MyRepository.save() 호출 → 이때는 이미 트랜잭션이 진행 중
따라서 레포지토리의 @Transactional은 새로운 트랜잭션을 만들지 않음
즉, 트랜잭션 중첩이 아니라 상속/전이되는 구조입니다!
어차피 트랜잭션은 전파되는데, 각 레이어에 @Transactional을 모두 붙이는 게 과연 의미가 있을까요?
서비스 레이어에만 붙여도 충분할 것 같은데 말이죠… 🤔
사실, 각 레이어에 @Transactional을 명시하는 것도 분명한 장점이 있습니다:
레포지토리를 단독으로 사용하는 경우에도 트랜잭션이 보장됩니다.
예: 테스트 코드, 배치 작업, 별도의 유틸성 컴포넌트 등 (레포지토리를 독립적으로 쓸 일이 있는가?”에 따른 전략)
예를 들어 readOnly = true를 설정하면, 해당 메서드는 조회 목적임을 명확히 드러낼 수 있어 성능 최적화나 의도 파악에 도움이 됩니다.
비즈니스 로직 단위로는 서비스 레이어에서 @Transactional을 사용하는 것이 기본입니다.
레포지토리에는 필요한 경우에만 최소한의 트랜잭션 설정(readOnly 등)을 적용하는 방식이 좋은것 같습니다.
다만, 무엇보다 중요한 건 팀 내에서의 일관된 기준과 명확한 의도 표현입니다. 상황에 맞게 유연하게, 그러나 일관성 있게 적용하는 것이 가장 이상적인 전략인것 같습니다~
좋은 글 읽고갑니다~!