⬜ 섹션9 스프링 트랜잭션 이해
◼️ 스프링 트랜잭션 소개
라고 하고 복습이라 읽는...복습을 위한 나를 위한 정리
🟢트랜잭션 추상화
- 기술마다 다른 트랜잭션 사용 코드(JPA,JDBC...)
- PlatformTransactionManager라는 인터페이스를 통해 트랜잭션을 추상화
- 스프링은 트랜잭션 추상화, 매니저 구현체 모두 제공
- 우리는 필요한 구현체를 스프링 빈으로 등록하고 주입 받아서 사용하기만 하면 된다.
🟢선언적 트랜잭션과 AOP
보통 선언적 트랜잭션 ( @Transactional ) 을 사용한다.
@Transactional 애노테이션만 붙으면 AOP는 이를 인식해 트랜잭션 처리 프록시를 적용해준다.
◼️ 트랜잭션 적용 확인
@Transactional 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어서 실제 객체 대신 프록시를 스프링 컨테이너에 등록한다.
@Slf4j
@SpringBootTest
public class TxBasicTest {
/*
* 트랜잭션 적용,적용X 메서드 동작비교.
* AOP 프록시가 올바르게 적용되었는지 확인.
* */
@Autowired
BasicService basicService;
@Test
void proxyCheck() {
//BasicService$$EnhancerBySpringCGLIB...
log.info("aop class={}", basicService.getClass());// 트랜잭션 적용되었기때문에 프록시 클래스
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Test
void txTest() {
basicService.tx();
basicService.nonTx();
}
@TestConfiguration
static class TxApplyBasicConfig {
@Bean
BasicService basicService() {
return new BasicService();
}
}
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}",txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}",txActive);
}
}
}
위에서, 메서드 tx()를 호출할땐 트랜잭션 적용 대상이니 트랜잭션 시작후 tx() 메소드 호출한다.
실제 tx() 호출 끝나고 프록시로 리턴되면 프록시는 트랜잭션 종료 (커밋or롤백)
nonTx()호출할땐 트랜잭션 적용X이니 트랜잭션시작하지않고 메소드 호출 후 종료한다.
+) 트랜잭션 시작 종료 로그로 확인하기
//application.properties
logging.level.org.springframework.transaction.interceptor=TRACE
◼️ 트랜잭션 적용 위치
🟢@Transactional 규칙
- 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
- 또한 클래스에 적용하면 메서드는 자동 적용된다.
- 트랜잭션은 기본적으로 읽기쓰기 모두 가능한것 기본옵션이다. @Transactional(readOnly=false)
@SpringBootTest
public class TxLevelTest {
/*
* 트랜잭션 설정 다른 두 메서드 호출
* write 메서드는 readOnly = false
* read 메서드는 readOnly = true
* */
@Autowired
LevelService service;
@Test
void orderTest(){
service.write();
service.read();
}
@TestConfiguration
static class TxApplyLevelConfig {
@Bean
LevelService levelService() {
return new LevelService();
}
}
@Slf4j
@Transactional(readOnly = true)
static class LevelService {
@Transactional(readOnly = false)
public void write() { //readOnly = false
log.info("call write");
printTxInfo();
}
public void read() { // 클래스레벨 readOnly = true 활성화
log.info("call read");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}",txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}",readOnly);
}
}
}
◼️🌟 트랜잭션 AOP 주의 사항 - 프록시 내부 호출1
트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체를 호출해야 한다.
대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
즉, @Transactional이 적용된 메서드가 클래스 내에서 직접 호출될 때 트랜잭션이 적용되지 않는다.
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
void printProxy() {
//@Transactional 하나라도 있으면 트랜잭션 프록시 객체가 만들어진다.
log.info("callService class={}",callService.getClass());
}
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall(){
callService.external();
}
@TestConfiguration
static class InternalCallV1Config{
@Bean
CallService callService() {
return new CallService();
}
}
@Slf4j
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);
}
}
}
TransactionInterceptor : Getting transaction for //internalCall()
[..CallService.internal]
..rnalCallV1Test$CallService : call internal
..rnalCallV1Test$CallService : tx active=true
TransactionInterceptor : Completing transaction for
[..CallService.internal]
CallService : call external //externalCall()
CallService : tx active=false
CallService : call internal
CallService : tx active=false
해결책 중 하나는 internal() 메서드를 별도 클래스로 분리하는 것이다.
아래는 메서드 내부 호출을 외부 호출로 변경한다. CallService의 external() 메서드에서 InternalService의 internal() 메서드를 호출하도록 변경했다. >> 이렇게 함으로써 internal() 메서드가 프록시를 통해 호출되므로 트랜잭션이 적용된다!
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
void externalCallV2() {
callService.external();
}
@TestConfiguration
static class InternalCallV2Config {
@Bean
CallService callService() {
return new CallService(innerService());
}
@Bean
InternalService innerService() {
return new InternalService();
}
}
@Slf4j
@RequiredArgsConstructor
static class CallService {
// 메서드 내부 호출을 외부 호출로 변경
private final InternalService internalService;
public void external() {
log.info("call external");
printTxInfo();
internalService.internal();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
@Slf4j
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
🟢@Transactional은 public 메서드만 적용
- 트랜잭션을 의도하지 않은 곳에 과도하게 적용되는 것을 방지하기 위해서 public에만 트랜잭션 적용
- 스프링 부트 3.0부터는 protected,default에도 트랜잭션이 적용된다.
◼️트랜잭션 AOP 주의 사항 - 초기화 시점
🤔초기화 코드(예: @PostConstruct)와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다. 초기화 코드가 먼저 호출되고, 그후 트랜잭션 AOP가 적용되기 때문이다.
😉ApplicationReadyEvent 사용시 이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출해준다.
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
void go() {
// 초기화 코드는 스프링이 초기화 시점에 호출한다.
}
@TestConfiguration
static class InitTxTestConfig{
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct // 두개 함께 사용시 트랜잭션 적용X
@Transactional // 초기화 코드 호출 후 트랜잭션 AOP 적용되기때문.
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void init2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
}
◼️트랜잭션 옵션
🟢value, transactionManager
@Transactional 에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다.
값 생략시 기본등록 트랜잭션 매니저를 사용하기때문에 대부분 생략하지만, 둘 이상시 이름을 지정해 구분해야 한다.
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
@Transactional("orderTxManager")
public void order() {...}
}
🟢rollbackFor
언체크 예외는 롤백, 체크예외는 커밋한다.
rollbackFor을 사용하면 체크예외(Exception)가 발생해도 롤백할 수 있다.
🟢readOnly
- 프레임워크
- JdbcTemplate: 변경기능 실행시 예외 던진다.
- JPA(하이버네이트): 커밋 시점에 플러시를 호출하지 않는다. 또한 변경감지 스냅샷도 생성하지 않는다.
- JDBC 드라이버
- 변경 쿼리 발생시 예외 던진다.
- 데이터베이스
- DB에 따라 읽기 전용 트랜잭션은 내부에서 성능 최적화가 발생
◼️예외와 트랜잭션 커밋, 롤백 - 기본
🟢예외발생시 트랜잭션 동작
- 언체크 예외(RuntimeException,Error) : 트랜잭션 롤백
- 체크 예외(Exception): 트랜잭션 커밋
- 정상 응답(리턴): 트랜잭션 커밋
@SpringBootTest
public class RollbackTest { //동작방식 확인 코드
@Autowired
RollbackService service;
@Test
void runtimeException() {
assertThatThrownBy(()->service.runtimeException())
.isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
assertThatThrownBy(() -> service.checkedException())
.isInstanceOf(MyException.class);
}
@Test
void rollbackFor() {
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();
}
//체크 예외 rollbackFor 지정: 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException{
log.info("call rollbackFor");
throw new MyException();
}
static class MyException extends Exception {
}
}
}
◼️예외와 트랜잭션 커밋, 롤백 - 활용
🟢예외 활용
- 언체크 예외(RuntimeException,Error) : 비즈니스 의미가 있을 때 사용
- 체크 예외(Exception): 복구 불가능 예외
비즈니스 예외(ex_결제시 잔고부족)는 별도 처리를 위해 체크 예외를 고려할 수도 있다.
커밋하지않고 롤백하고 싶을때도 있는데,이때는 rollbackFor 옵션을 사용할 수 있다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션11 스프링 트랜잭션 전파2 - 활용 (0) | 2024.07.14 |
---|---|
[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션10 스프링 트랜잭션 전파1 - 기본 (0) | 2024.07.10 |
[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션8 데이터 접근 기술 - 활용 방안 (0) | 2024.07.07 |
[Spring] 스프링 DB 2편 - 데이터 접근 활용 기술 섹션6 데이터 접근 기술 - Querydsl (0) | 2024.07.07 |
[Spring] 스프링 DB 1편 - 데이터 접근 핵심 원리 섹션5,6 예외 & 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.06.11 |