스프링 트랜잭션 전파(Propagation)는 트랜잭션이 중첩되거나 여러 트랜잭션이 동시에 발생할 때, 이 트랜잭션들이 어떻게 상호작용할지를 결정하는 중요한 개념이다.
HikariCP 커넥션 풀과 트랜잭션 관리
스프링에서 트랜잭션을 다루는 경우, Hikari 커넥션 풀을 대부분 사용한다. 이때 반환되는 커넥션은 프록시 객체로 감싸져 있다. 프록시 객체의 주소는 매번 다르지만, 실제 물리적인 커넥션은 동일하다. 예를 들어, 두 개의 논리 트랜잭션이 동일한 물리 커넥션을 공유할 수 있다.
중첩 트랜잭션
중첩 트랜잭션은 하나의 트랜잭션이 진행 중일 때 또 다른 트랜잭션이 시작되는 상황을 의미한다. 이때 내부 트랜잭션이 외부 트랜잭션에 참여하는 것처럼 보이는 것이 기본 동작이다. 스프링에서는 이러한 동작을 통해 논리 트랜잭션과 물리 트랜잭션의 구분을 명확히 한다.
- 물리 트랜잭션: 실제로 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 시작되고 커밋 또는 롤백된다.
- 논리 트랜잭션: 트랜잭션 매니저에 의해 관리되는 트랜잭션으로, 여러 논리 트랜잭션이 하나의 물리 트랜잭션으로 묶일 수 있다. (@Transactional)
논리 트랜잭션과 물리 트랜잭션의 원칙
스프링에서는 논리 트랜잭션과 물리 트랜잭션을 다음과 같은 원칙으로 관리한다:
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
이 원칙은 중첩된 트랜잭션에서 특히 중요하다. 예를 들어, 외부 트랜잭션이 성공적으로 커밋되기 위해서는 내부 트랜잭션도 반드시 성공적으로 커밋되어야 한다. 만약 내부 트랜잭션에서 오류가 발생해 롤백되면, 전체 물리 트랜잭션도 롤백된다.
트랜잭션 전파의 주요 시나리오
1. 외부 트랜잭션과 내부 트랜잭션
외부 트랜잭션이 진행 중인 상황에서 내부 트랜잭션이 추가로 시작되면, 스프링은 내부 트랜잭션을 외부 트랜잭션에 참여시키는 방식으로 동작한다. 이 경우, 내부 트랜잭션은 외부 트랜잭션에 속하게 되고, 두 트랜잭션은 하나의 물리 트랜잭션으로 묶인다.
2. 트랜잭션 커밋과 롤백의 흐름
- 외부 트랜잭션: 물리 트랜잭션을 시작하고 커밋하는 역할을 담당한다.
- 내부 트랜잭션: 외부 트랜잭션에 참여하며, 별도의 물리 커밋이나 롤백을 하지 않는다.
3. 트랜잭션 매니저의 역할
스프링의 트랜잭션 매니저는 다음과 같은 역할을 수행한다:
- 외부 트랜잭션이 시작되면 새로운 커넥션을 생성하고, 이를 트랜잭션 동기화 매니저에 보관한다.
- 내부 트랜잭션이 시작되면 기존의 외부 트랜잭션에 참여하도록 설정한다.
- 트랜잭션이 종료될 때, 각 논리 트랜잭션의 상태를 확인하여 물리 트랜잭션을 커밋하거나 롤백한다.
- 논리 트랜잭션에서 커밋, 롤백이 실행되더라도, 그 당시에 물리 트랜잭션에는 아무런 영향을 주지 않는다.
스프링 트랜잭션 - 외부 롤백
물리 트랜잭션 속 외부 트랜잭션과 내부 트랜잭션이 있는 상태이다.
트랜잭션을 시작한 첫 로직이 외부 트랜잭션, 두번째 시작한 트랜잭션을 내부 트랜잭션이라고 한다.
만약 먼저 실행된 외부 트랜잭션이 롤백되면 그 후에 실행된 내부 트랜잭션이 정상 실행되도, 전체 물리 트랜잭션이 롤백된다.
스프링 트랜잭션 - 내부 롤백
하지만 만약 외부 트랜잭션이 문제가 없고, 그 후에 내부 트랜잭션이 롤백된다면 어떻게 처리될까?
간단하다. 대원칙에 따라 논리 트랜잭션이 1개라도 예외가 발생하면 전체를 롤백한다.
하지만 어떻게 할까?
스프링 트랜잭션 전파 활용: 다양한 전파 옵션과 그 적용
스프링 트랜잭션 전파(Propagation)를 이해하는 것은 트랜잭션을 효과적으로 관리하고 애플리케이션의 데이터 일관성을 유지하는 데 매우 중요하다. 이번 글에서는 전파 옵션을 실제 예제와 함께 설명하며, 각 전파 옵션이 실무에서 어떻게 활용되는지 알아보겠다.
전파 옵션의 기본 개념
스프링에서는 트랜잭션 전파를 통해 트랜잭션의 경계를 정의하고, 서로 다른 트랜잭션 간의 관계를 설정할 수 있다. 스프링에서 제공하는 주요 전파 옵션은 다음과 같다:
- REQUIRED: 기본 옵션으로, 이미 트랜잭션이 존재하면 그 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작한다.
- REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션은 보류된다.
전파 옵션 활용 예제
1. REQUIRED: 기본 전파 옵션
REQUIRED는 가장 많이 사용되는 트랜잭션 전파 옵션이다. 예를 들어, 회원 가입과 로그 남기기 기능을 하나의 트랜잭션으로 묶을 때 유용하다.
@Service
public class MemberService {
@Transactional
public void joinMember(String username) {
memberRepository.save(new Member(username));
logRepository.save(new Log("회원가입: " + username));
}
}
위에서 전파에 대해 다뤘던 대로, @Transactional 애노테이션을 사용해 트랜잭션을 관리하며, 회원 등록과 로그 남기기 모두 하나의 트랜잭션으로 처리된다. 만약 로그 저장 중에 예외가 발생하면 전체 트랜잭션이 롤백된다.
2. REQUIRES_NEW: 별도의 트랜잭션 시작
어떤 경우에는 로그 남기기와 같은 보조적인 작업이 메인 트랜잭션과 분리되어야 할 필요가 있다. 이때 REQUIRES_NEW 옵션을 사용하여 두 작업을 별도의 트랜잭션으로 분리할 수 있다.
@Service
public class MemberService {
@Transactional
public void joinMemberWithSeparateLog(String username) {
memberRepository.save(new Member(username));
try {
logRepository.saveLogWithNewTransaction(new Log("회원가입: " + username));
} catch (RuntimeException e) {
log.warn("로그 저장에 실패했습니다: {}", e.getMessage());
}
}
}
@Repository
public class LogRepository {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLogWithNewTransaction(Log log) {
entityManager.persist(log);
}
}
회원가입에는 성공했지만 로그를 못남긴다고해서 전부다 롤백할 필요가 없다.
로그가 실패하더라도 회원가입은 그대로 두어야한다.
이럴 때 트랜잭션 분리를 생각할 수 있다.
로그를 남기는 트랜잭션을 완전히 분리하게된다면 물리 트랜잭션 또한 새로운 트랜잭션을 사용하는 것이며 커넥션 또한 기존의 것이 아닌 새로운 것을 사용한다.
그렇기 때문에 로그를 다루는 비즈니스에서 예외가 발생해도 MemberService까지 전달되지 않는다.
필자는 이것을 몰라 예전에 프로젝트하며 몸소 몸으로 느꼈는데, 트랜잭션 전파를 좀 더 학습했더라면 좀 더 편하지 않았을까? 하는 생각이 든다.
2024.04.20 - [대외활동] - 알림기능, 문제의 시작
알림기능, 문제의 시작
기존 서비스저희 게시판 서비스는 HoneyTip(꿀팁공유해요 게시판)과 HoneyTipComment(꿀팁공유해요 댓글)이 연관관계 매핑이 되어있습니다.HoneyTipComment는 Comment를 단일테이블 전략으로 상속받고 있습
toychip.tistory.com
하지만 위에서는 응답은 응답대로 빠르게 나가야하며, AOP를 사용하여 댓글이 생성될 때의 트랜잭션을 그대로 갖고왔기 때문에 예외가 발생하는 것을 알았다.
적절히 Requires_new 설정을 사용해도 됫겠지만, 트랜잭션 자체를 완전히 TransactionEventListener로 분리하고 비동기로 처리하여 응답에는 빠르게 나가도록 설정했었다.
'Spring > Spring Transaction' 카테고리의 다른 글
Spring Transaction 1. 스프링 트랜잭션 이해 (0) | 2024.07.31 |
---|