Search
Duplicate
📒

[Database Study] 03-3. 트랜잭션 AOP 주의사항, 트랜잭션 옵션(전파 속성)

상태
수정중
수업
Database Study
주제
Transaction
4 more properties
참고

@Transactional AOP 주의 사항

NOTE
@Transactional을 사용하면 스프링의 트랜잭션 AOP가 적용됩니다. @Transactionl을 사용하면 프록시 객체가 요청을 먼저 받아서 처리하고 실제 객체를 호출합니다. 따라서 트랜잭션 적용을 위해서는 항상 프록시를 통해 대상 객체를 호출해야 합니다.
트랜잭션 적용 흐름도(프록시)
트랜잭션 적용 흐름도
static class CallService { // 외부 호출 public void external() { log.info("call external"); printTxInfo(); // 클래스 내부함수 직접 호출 internal(); // 클래스 내부함수 직접 호출(트랜잭션 적용 실패) } // 내부 호출 @Transactional public void internal() { log.info("call internal"); printTxInfo(); // 클래스 내부함수 직접 호출 } private void printTxInfo() { boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("tx active={}", txActive); } }
Java
복사
@Transactional이 적용되지 않은 external에서 inernal을 호출
@Test void externalCall() { callService.external(); }
Java
복사
test external()
exteranl()에서 internal()을 실행시키니 @Transactional을 적용했는데 트랜잭션이 적용되지 않았다.
internal()만 단독으로 실행시키면 트랜잭션이 적용이 되어있다.
클래스에서 메서드를 호출할 때 this를 사용하여 호출합니다 → this.internal(). 여기서 this는 클래스 자체를 가리키므로, 실제 대상 객체(Target)의 인스턴스를 의미합니다. 이 경우 내부 호출이 프록시를 거치지 않아 트랜잭션을 적용할 수 없습니다.

내부 호출 해결방법

내부호출로 인한 프록시를 거치지않고 호출해버림.
해결책: 클래스 외부 함수로 수정한다.
static class InternalService { @Transactional public void internal() { log.info("call internal"); printTxInfo(); } private void printTxInfo() { boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("tx active ={}", txActive); } }
Java
복사
public void externalV2() { log.info("call external"); printTxInfo(); internalService.internal(); }
Java
복사
internal() 메서드를 별도의 클래스로 분리하여, this의 문제점을 해결합니다.
스프링의 트랜잭션 AOP는 기본 설정으로 public 메서드에만 트랜잭션을 적용합니다.

트랜잭션 AOP 주의 사항 - 초기화 시점

NOTE
// 트랜잭션 적용 실패 @PostConstruct @Transactional public void initV1(){ boolean isActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("Hello init @PostConstruct tx active ={}", isActive); } // 트랜잭션 적용 성공 @EventListener(ApplicationReadyEvent.class) @Transactional public void initV2(){ boolean isActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("Hello init ApplicationReadyEvent tx active ={}", isActive); }
Java
복사
초기화 코드 (예: @PostConstruct)와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않습니다.
이는 초기화 코드가 먼저 호출되고, 그 후에 트랜잭션 AOP가 적용되기 때문에 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없기 때문입니다.
이 문제를 해결하는 방법은 ApplicationReadyEvent를 사용하는 것입니다.

@Transaction 옵션

NOTE
스프링 트랜잭션은 다양한 옵션(예외 발생시 정책, 트랜잭션 전파, 격리 등..)을 제공합니다!
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { // 트랜잭션 매니저 별칭 @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; String[] label() default {}; // 트랜잭션 전파 Propagation propagation() default Propagation.REQUIRED; // 트랜잭션 격리 Isolation isolation() default Isolation.DEFAULT; // 타임아웃 (기본값 -1은 없다는 의미) int timeout() default -1; String timeoutString() default ""; // ReadOnly(읽기전용) boolean readOnly() default false; // 롤백을 트리거할 예외 클래스 및 예외 클래스 이름 Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; // 롤백을 트리거하지 않을 예외 클래스 및 예외 클래스 이름 Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
Java
복사
@Transaction

transactionManager

트랜잭션을 관리할 트랜잭션 매니저를 명시적으로 지정할 수 있습니다. 기본적으로 스프링은 설정된 기본 트랜잭션 매니저를 사용합니다.
public class TxService { @Transactional("memberTxManager") public void member() {...} @Transactional("orderTxManager") public void order() {...} }
Java
복사
특정 트랜잭션 매니저 사용

readOnly

Spring에서 트랜잭션을 2가지 목적(성능 최적화 & 쓰기 방지)으로 읽기 적용으로 설정할 수 있습니다.
읽기 전용으로 설정함으로서 성능을 최적화한다.
쓰기 작업이 일어나는 것을 의도적으로 방지한다.

예외와 트랜잭션 커밋, 롤백(rollbackFor)

NOTE
스프링 트랜잭션은 예외의 종류에 따라 commitrollback 동작을 선택합니다.
스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임 예외는 복구 불가능한 예외로 가정합니다. 주문을 하는 상황으로 예를 들어보겠습니다.
시스템 예외(언체크 예외): 주문시 내부에 복구 불가능한 예외가 발생해 전체 데이터를 롤백한다.
비즈니스 예외(체크 예외): 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 대기 상태로 처리한다.( 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 한다.)
@SpringBootTest public class RollbackTest { @Autowired RollbackService service; // 런타임 예외시 트랜잭션 여부 (O) @Test void runtimeException() { Assertions.assertThatThrownBy(() -> service.runtimeException()) .isInstanceOf(RuntimeException.class); } // 런타임 예외시 트랜잭션 여부 (X) @Test void checkedException() { Assertions.assertThatThrownBy(() -> service.checkedException()) .isInstanceOf(MyException.class); } // MyException 예외시 트랜잭션 여부 (O) @Test void rollbackFor() { Assertions.assertThatThrownBy(() -> service.rollbackFor()) .isInstanceOf(MyException.class); } @TestConfiguration static class RollbackTestConfig { @Bean RollbackService rollbackService() { return new RollbackService(); } } @Slf4j static class RollbackService { // 런타임 예외 발생: 롤백 @Transactional public void runtimeException() { log.info("call runtimeException"); throw new RuntimeException(); } // 체크 예외 발생: 커밋 @Transactional public void checkedException() throws MyException { log.info("call checkedException"); throw new MyException(); } // 특정 예외 클래스 발생시 롤백 @Transactional(rollbackFor = MyException.class) public void rollbackFor() throws MyException { log.info("call rollbackFor"); throw new MyException(); } } // 체크 예외 static class MyException extends Exception { } }
Java
복사
체크 예외: commit
언체크 예외: rollback
rollbackFor 등록 예외: rollback

트랜잭션 전파 속성(Transaction Propagation)

NOTE
Spring은 기존의 트랜잭션이 진행줄일 때 추가적인 트랜잭션을 진행해야 하는 경우, 추가 트랜잭션에 대한 로직을 결정하는 전파 속성을 지원합니다.
트랜잭션은 DB에서 제공하는 기술 이므로, connection 객체를 통해 처리합니다. 그래서 1개의 트랜잭션을 사용한다는 것은 하나의 connection을 사용하는 것이고, 실제 DB의 트랜잭션을 사용한다는 점에서 물리 트랜잭션이라 합니다.
하지만 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는곳이 2곳입니다. 그래서 실제 DB 트랜잭션과 스프링이 처리하는 트랜잭션을 구분하기 위해, 논리 트랜잭션 개념을 추가했습니다.
물리 트랜잭션: 실제 DB에서 적용되는 트랜잭션, connection을 통해 commit/rollback하는 단위
논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위

트랜잭션 전파 속성 종류(REQUIRES, REQUIRES_NEW)

NOTE
Spring @Transaction은 총 7가지 전파 속성을 제공합니다.

REQUIRED(기본값)

REQUIRED는 스프링이 제공하는 기본적인 전파 속성으로, 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여하게 됩니다.
참여한 논리 트랜잭션이 모두 성공해야 물리 트랜잭션이 commit됩니다.
내부 트랜잭션이 실패한 경우 rollbackOnly = true 속성을 전달하며, 외부 트랜잭션에서 값 확인후 rollback 합니다.
참여한다는 것은 외부 트랜잭션을 그대로 이어간다는 의미이며, 외부 트랜잭셤의 범위가 내부까지 확장됩니다.
모든 논리 트랜잭션이 commit되어야 물리 트랜잭션이 commit되며, 하나의 논리 트랜잭션이라도 rollback되면 물리 트랜잭션은 rollback됩니다.
기존 트랜잭이 존재하면 참여하고, 없다면 생성합니다.

REQUIRES_NEW

REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하는 전파 속성입니다. 논리 트랜잭션은 각각의 connection을 가지며, 개별적으로 commitrollback이 이루어집니다.
실제로 다른 물리 트랜잭션이므로, 각 트랜잭션의 결과가 다른 트랜잭션에 영향을 주지 않습니다.
서로 다른 물리 트랜잭션을 가진다는것 각각이 connection을 가지고, 1번의 HTTP에 2개의 connection을 사용합니다. 그러므로 DB connection을 고갈시킬 수 있으니 조심해서 사용해야 합니다.
기존 트랜잭션이 있든 없든 새로운 트랜잭션을 만듭니다.

기타 옵션

SUPPORTS: 트랜잭션이 있으면 참여하고, 없다면 트랜잭션 없이 진행합니다.
NOT_SUPPORTED: 트랜잭션이 있으면 보류한다음 트랜잭션 없이 진행하고, 없다면 트랜잭션 없이 진행합니다.
MANDATORY: 트랜잭션이 있으면 참여하고, 트랜잭션이 없다면 예외가 터집니다.
NEVER: 트랜잭션이 있다면 예외가 터지고, 없다면 트랜잭션 없이 진행합니다.
NESTED: 중첩 트랜잭션을 생성하며, REQIROES_NEW와 다르게 외부 트랜잭션에 영향을 줍니다.

트랜잭션 전파 활용

NOTE
REQIORES 실행 흐름도
@Slf4j @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; private final LogRepository logRepository; @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 호출 종료 =="); // 내부 트랜잭션의 예외를 외부 트랜잭션 범위에서 잡는다. // 예외를 처리해도 이미 rollbackOnly = true 값을 받아서 최종적으로 rollback 처리된다. log.info("== logRepository 호출 시작 =="); try { logRepository.save(logMessage); } catch (RuntimeException e) { log.info("log 저장에 실패 했습니다. logMessage={}", logMessage); log.info("정상 흐름 반환"); } log.info("== logRepository 호출 종료 =="); } }
Java
복사
@Slf4j @Repository @RequiredArgsConstructor public class LogRepository { private final EntityManager entityManager; @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Log logMessage) { log.info("log 저장"); entityManager.persist(logMessage); // 로그 예외가 포함된 경우 언체크 예외 발생 if (logMessage.getMessage().contains("로그예외")) { log.info("log 저장시 예외 발생"); throw new RuntimeException("예외 발생"); } } public Optional<Log> find(String message) { return entityManager.createQuery("select l from Log l where l.message = :message", Log.class) .setParameter("message", message) .getResultList().stream().findAny(); } }
Java
복사
@Test void recoverException_fail() { //given String username = "로그예외_recoverException_fail"; //when assertThatThrownBy(() -> memberService.joinV2(username)) .isInstanceOf(UnexpectedRollbackException.class); // then (Member - rollback, Log - rollback) assertTrue(memberRepository.find(username).isEmpty()); assertTrue(logRepository.find(username).isEmpty()); }
Java
복사
REQIORES_NEW 실행 흐름도
@Slf4j @Repository @RequiredArgsConstructor public class LogRepository { private final EntityManager entityManager; // REQUIRES_NEW를 사용해서 새로운 물리 트랜잭션으로 분리 @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Log logMessage) { log.info("log 저장"); entityManager.persist(logMessage); if (logMessage.getMessage().contains("로그예외")) { log.info("log 저장시 예외 발생"); throw new RuntimeException("예외 발생"); } } // ... }
Java
복사
@Test void recoverException_success() { // given String username = "로그예외_recoverException_success"; // when memberService.joinV2(username); // then (Member - commit, Log - rollback) assertTrue(memberRepository.find(username).isPresent()); assertTrue(logRepository.find(username).isEmpty()); }
Java
복사