스프링 트랜잭션 전파
스프링 트랜잭션 전파 예제 - 커밋, 롤백
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final LogRepository logRepository;
public void joinV1(String username) {
Member member = new Member(username);
Log logMessage = new Log(username);
log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member);
log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
logRepository.save(logMessage);
log.info("== logRepository 호출 종료 ==");
}
public void joinV2(String username) {
Member member = new Member(username);
Log logMessage = new Log(username);
log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member);
log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
try {
logRepository.save(logMessage);
} catch (RuntimeException e) {
log.info("log 저장에 실패했습니다. logMessage={}", logMessage.getMessage());
log.info("정상 흐름 반환");
}
log.info("== logRepository 호출 종료 ==");
}
}
- 회원 등록/조회 예제
- joinV1()
- 회원 등록 시 회원과 DB로그를 함께 DB에 저장하는 로직
- 각 Repository에만 트랜잭션이 설정되어 있고 서비스 계층에는 별도의 트랜잭션이 설정되어 있지 않음
- joinV2()
- joinV1()과 동일하지만 DB로그 저장 시 예외가 발생하면 예외를 처리해서 정상 흐름으로 만든다.
- joinV1()
- 예제 상황
- 서비스 계층에는 트랜잭션이 없다.
- 회원, 로그 레포지토리에는 각각 트랜잭션을 가지고 있다.
- 회원, 로그 레포지토리 모두 커밋에 성공하는 경우
- 서로 각각의 트랜잭션으로 처리하기 때문에 영향을 미치지 않는다.
- 둘 다 각각의 커넥션을 생성하고 트랜잭션을 시작해서 커밋한다.
- 회원 레포지토리는 정상 동작하지만 로그 레포지토리는 예외가 발생하는 경우
- 위 예시와 마찬가지로 서로 각각의 트랜잭션으로 처리하기 때문에 영향을 미치지 않는다.
- 회원 레포지토리는 정상 동작으로 커밋에 성공
- 로그 레포지토리
- 커넥션 생성 후 트랜잭션 시작
- 레포지토리에서 런타임 예외 발생
- 레포지토리에서 발생한 예외를 밖으로 던짐으로 트랜잭션 AOP가 예외를 받게된다.
- 런타임 예외 발생으로 트랜잭션 AOP는 트랜잭션 매니저에 롤백을 호출
- 트랜잭션 매니저는 신규 트랜잭션이기에 물리 트랜잭션 롤백 호출
- 이 경우 회원은 저장되지만 이력 로그는 롤백되면서 데이터 정합성 문제가 발생할 수 있기에 둘을 하나의 트랜잭션으로 묶어서 처리하는 것이 좋다.
스프링 트랜잭션 전파 예제 - 단일 트랜잭션
- 회원 레포지토리와 로그 레포지토리를 하나의 트랜잭션으로 묶는 가장 간단한 방법은 둘을 호출하는 서비스 계층에만 트랜잭션을 사용하는 것
- MemberService에만 트랜잭션을 적용하고 각 레포지토리에는 트랜잭션을 사용하지 않는 경우
- 이 경우 논리,물리,외부,내부 트랜잭션, 트랜잭션 전파와 같은 개념을 생각할 필요가 없다.
- MemberService만 트랜잭션 AOP가 적용되고 각 레포지토리에는 AOP가 적용되지 않는다.
- 트랜잭션 동기화 매니저를 통해 MemberService가 생성한 동일한 커넥션을 각 레포지토리에서 사용하게 된다.
- 문제점
- 항상 클라이언트가 MemberService를 통해 호출하면 좋겠지만 각 레포지토리를 호출하고 트랜잭션을 해당 레포지토리에만 트랜잭션을 적용하고 싶은 경우에는 각 레포지토리에는 트랜잭션이 적용되고 있지 않기 때문에 문제가 발생한다.
- 이 문제를 해결하기 위한 것이 트랜잭션 전파이다.
스프링 트랜잭션 전파 예제 - 전파 커밋
- 서비스 계층과 각 레포지토리에서 트랜잭션을 사용하고 모두 커밋하는 경우
- 서비스, 각 레포지토리 트랜잭션은 논리 트랜잭션으로 하나의 물리 트랜잭션으로 묶인다.
- MemberService, MemberRepository, LogRepository 논리 트랜잭션 모두가 커밋되어야 물리 트랜잭션이 커밋되며 하나라도 롤백되면 물리 트랜잭션도 롤백된다.
- 레포지토리는 서비스를 통해 호출되므로 서비스는 외부 트랜잭션, 각 레포지토리는 내부 트랜잭션이다.
- 외부 신규 트랜잭션만 물리 트랜잭션을 시작하고 커밋한다.
- 내부 트랜잭션은 물리 트랜잭션을 시작, 커밋하지 않는다.
- 동작 흐름
- MemberService 호출 -> 트랜잭션 AOP 호출
- 커넥션 생성, 신규 트랜잭션 생성, 물리 트랜잭션 시작 ...
- MemberRepository 호출 -> 트랜잭션 AOP 호출
- 기존 트랜잭션이 있기에 기존 트랜잭션에 참여한다.
- MemberRepository 로직 수행 후 정상 응답 -> 트랜잭션 AOP가 트랜잭션 매니저에게 커밋 요청
- 트랜잭션 매니저는 신규 트랜잭션이 아니기에 물리 트랜잭션에 커밋하지 않는다.
- LogRepository 호출 -> 트랜잭션 AOP 호출
- 기존 트랜잭션이 있기에 기존 트랜잭션에 참여한다.
- MemberRepository 로직 수행 후 정상 응답 -> 트랜잭션 AOP가 트랜잭션 매니저에게 커밋 요청
- 트랜잭션 매니저는 신규 트랜잭션이 아니기에 물리 트랜잭션에 커밋하지 않는다.
- MemberService 로직 수행 후 정상 응답 -> 트랜잭션 AOP가 트랜잭션 매니저에게 커밋 요청
- 트랜잭션 매니저는 신규 트랜잭션이기에 물리 트랜잭션에 커밋한다.
- MemberService 호출 -> 트랜잭션 AOP 호출
- 서비스, 각 레포지토리 트랜잭션은 논리 트랜잭션으로 하나의 물리 트랜잭션으로 묶인다.
스프링 트랜잭션 전파 예제 - 전파 롤백
- 서비스 계층과 각 레포지토리에서 트랜잭션을 사용하고 로그 레포지토리에서 예외가 발생해서 롤백되는 경우
- MemberService 호출, MemberRepository 호출, 로직 수행, LogRepository 호출은 이전 예제와 동일
- LogRepository 로직 수행 중 런타임 예외가 발생해서 던짐 -> LogRepository의 트랜잭션 AOP가 예외를 받음
- LogRepository의 트랜잭션 AOP는 런타임 예외이기에 트랜잭션 매니저에게 롤백 요청
- 트랜잭션 매니저는 신규 트랜잭션이 아니기에 물리 트랜잭션 롤백을 하지 않고 rollbackOnly로 설정
- LogRepository의 트랜잭션 AOP도 런타임 예외를 밖으로 던짐 -> MemberService에서 런타임 예외를 받음
- MemberService에서도 런타임 예외를 처리하지 못하고 밖으로 던짐 -> MemberService의 트랜잭션 AOP가 예외를 받음
- MemberService의 트랜잭션 AOP는 런타임 예외이기에 트랜잭션 매니저에게 롤백 요청
- 트랜잭션 매니저는 신규 트랜잭션이기에 물리 트랜잭션 롤백한다.
- 결과적으로 외부 트랜잭션에서도 롤백을 했기에 rollbackOnly 설정은 사용되지 않는다.
- MemberService의 트랜잭션 AOP도 런타임 예외를 밖으로 던짐 -> 클라이언트가 예외를 받음
스프링 트랜잭션 전파 예제 - 복구 REQUIRED
- 요구사항 - 이전 예제에서는 로그에 문제가 발생하면 회원 등록도 실패했는데 로그에 문제가 발생하더라도 회원 등록은 가능하게 하려면 어떻게 할까?
- 모두 트랜잭션을 사용하고 로그 레포지토리에서 예외가 발생하고 서비스에서 예외를 처리하는 경우
- LogRepository에서 예외가 발생
- 런타임 예외이기에 LogRepository의 트랜잭션 AOP에게 던짐
- LogRepository의 트랜잭션 AOP는 런타임 예외이기에 트랜잭션 매니저에게 롤백 요청
- 트랜잭션 매니저는 신규 트랜잭션이 아니기에 물리 트랜잭션 롤백을 하지 않고 rollbackOnly로 설정
- LogRepository의 트랜잭션 AOP도 런타임 예외를 밖으로 던짐 -> MemberService에서 런타임 예외를 받음
- MemberService에서 예외를 처리 후 정상 흐름으로 바꿔서 트랜잭션 매니저에게 커밋 요청
- 트랜잭션 매니저는 신규 트랜잭션이기에 물리 트랜잭션 커밋을 하기전에 rollbackOnly 설정 확인
- 트랜잭션 매니저는 rollbackOnly 설정이 있기 때문에 UnexpectedRollbackException 예외를 던지고 물리 트랜잭션 롤백
- MemberService의 트랜잭션 AOP는 전달받은 UnexpectedRollbackException 예외를 클라이언트에게 던짐
- LogRepository에서 예외가 발생
- 내부 트랜잭션에서 발생한 예외로 롤백을 한 상태에서 외부 트랜잭션에서 처리하고 커밋을 하더라도 논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다.
- => 외부 트랜잭션에서 예외를 처리해도 요구사항은 만족할 수 없다.
스프링 트랜잭션 전파 예제 - 복구 REQUIRES_NEW
public class LogRepository {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Log logMessage) {...}
...
}
- LogRepository에 REQUIRES_NEW 옵션 사용
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
@Transactional
public void joinV2(String username) {
Member member = new Member(username);
Log logMessage = new Log(username);
log.info("== memberRepository 호출 시작 ==");
memberRepository.save(member);
log.info("== memberRepository 호출 종료 ==");
log.info("== logRepository 호출 시작 ==");
try {
logRepository.save(logMessage);
} catch (RuntimeException e) {
log.info("log 저장에 실패했습니다. logMessage={}", logMessage.getMessage());
log.info("정상 흐름 반환");
}
log.info("== logRepository 호출 종료 ==");
}
...
}
- MemberService에서 런타임 예외를 처리하는 joinV2() 사용
- REQUIRES_NEW를 사용해서 로그와 물리 트랜잭션을 별도로 분리하는 경우
- MemberRepository는 기본 옵션 REQUIRED로 기존 트랜잭션에 참여한다.
- LogRepository는 REQUIRES_NEW 옵션을 사용해서 별도의 커넥션과 물리 트랜잭션을 만든다.
- LogRepository에서 런타임 예외 발생
- 런타임 예외이기에 LogRepository의 트랜잭션 AOP에게 던짐
- LogRepository의 트랜잭션 AOP는 런타임 예외이기에 트랜잭션 매니저에게 롤백 요청
- 트랜잭션 매니저는 신규 트랜잭션이기에 rollbackOnly 설정은 하지 않고 물리 트랜잭션을 롤백
- LogRepository의 트랜잭션 AOP도 런타임 예외를 밖으로 던짐 -> MemberService에서 런타임
- MemberService에서 예외를 처리 후 정상 흐름으로 바꿔서 트랜잭션 매니저에게 커밋 요청
- 트랜잭션 매니저는 신규 트랜잭션이기에 물리 트랜잭션 커밋을 하기전에 rollbackOnly 설정 확인
- 트랜잭션 매니저는 rollbackOnly 설정이 없기에 물리 트랜잭션을 커밋
- 결과적으로 LogRepository와 관련된 트랜잭션은 롤백, MemberService, MemberRepository와 관련된 트랜잭션은 커밋된다.
- 요구사항대로 회원 등록은 가능하게 되고 로그 문제로 인해 로그 데이터만 롤백된다.
- REQUIRES_NEW를 사용한 만큼 커넥션이 추가로 생성되서 사용되게 되므로 성능이 중요한 곳에서는 주의해야한다.
- REQUIRES_NEW를 사용하면 기존 커넥션이 살아있는 상태에서 잠시 멈추고 새로운 커넥션을 생성해서 사용하는 것
- 그림과 같이 REQUIRES_NEW를 사용하지 않고 문제를 해결할 수 있는 방법도 있다.
- 회원 등록과 로그 저장을 별도의 트랜잭션으로 구성해 묶지 않고 각각 호출하는 방법
- 이 경우 동시에 2개 커넥션을 사용하지 않기에 REQUIRES_NEW 옵션보다 성능적으로 좋다.
- 구조상 REQUIRES_NEW 옵션을 사용하는 것이 더 깔끔한 경우가 있기에 적절하게 사용할 것
- 그림과 첫 예제 (각 레포지토리에만 트랜잭션을 사용하는 경우)와의 차이점
- 트랜잭션 적용 범위가 다르다
- 첫 예제에서는 MemberService에 트랜잭션을 사용하지 않기에 MemberService에서 예외가 발생해도 트랜잭션이 롤백되지 않는다.
- 그림에서는 MemberService에서도 트랜잭션을 사용하기에 MemberService에서 예외가 발생하면 트랜잭션이 롤백된다.
- 트랜잭션 적용 범위가 다르다
정리
- 내부 트랜잭션에서 발생한 예외로 롤백을 한 상태에서 외부 트랜잭션에서 처리하고 커밋을 하더라도 논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다.
- 논리 트랜잭션 중 하나라도 롤백되면 물리 트랜잭션을 롤백된다.
- 논리 트랜잭션 모두가 커밋되어야 물리 트랜잭션도 커밋된다.
- REQUIRES_NEW 옵션을 사용해서 별도의 물리 트랜잭션으로 구분하면 서로 영향을 받지 않고 트랜잭션을 커밋 / 롤백할 수 있다.
- REQUIRES_NEW를 사용한 만큼 커넥션이 추가로 생성되서 사용되게 되므로 성능이 중요한 곳에서는 주의해야한다.
출처 : [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
스프링 DB 2편 - 데이터 접근 활용 기술 강의 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드
www.inflearn.com
'Spring > [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 스프링 트랜잭션 전파 - 기본 (0) | 2024.11.29 |
---|---|
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 스프링 트랜잭션 이해 (1) | 2024.11.29 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - 활용 방안 (2) | 2024.11.27 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - Querydsl (0) | 2024.11.26 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2024.11.26 |