FireDrago
[Spring] 스프링 트랜잭션 이해 본문


JDBC, JPA 등등 여러가지 DB 연결 기술들이 있지만,
각각 트랜잭션을 구현하는 코드는 모두 다르다. DB 연결 기술을 변경하게 되면, 트랜잭션 코드도 모두 바꿔야 한다.
그래서 스프링은 트랜잭션 기능을 추상화(PlatformTransactionManager)하여 제공하고 기술별 구현체도 제공한다.
스프링부트는 한발 더 나아가서 사용하는 기술을 자동으로 인식하고 구현체를 스프링빈으로 등록해준다.
개발자가 할 일은 @Transaction 애노테이션을 달아주는 일 밖에 없다. 스프링 트랜잭션에 대하여 알아보자
트랜잭션 적용위치
@SpringBootTest
public class TxLevelTest {
@Autowired LevelService service;
@Test
void orderTest() {
service.write();
service.read();
}
@Slf4j
@TestConfiguration
//readOnly 속성 트랜잭션 내에서 데이터 변경금지, 조회만 가능
@Transactional(readOnly = true)
static class LevelService{
//메서드 애너테이션이 우선적용
@Transactional(readOnly = false)
public void write() {
log.info("call write");
printTxInfo();
}
//클래스의 '@Transactional(readOnly = true)' 그대로 적용
public void read() {
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);
}
}
}
@Transactional 애너테이션은 메서드, 클래스 둘중 한 위치에라도 있다면, AOP프록시 객체가 스프링빈으로 등록된다.
만약 클래스와 메서드 둘다 애너테이션이 있다면, 메서드의 애너테이션이 우선순위를 가진다. (구체적인 것이 우선)
클래스에는 있지만, 메서드에 없다면, 클래스의 애너테이션이 메서드에 그대로 적용된다.
트랜잭션 주의사항 - 내부호출

프록시 객체가 스프링빈으로 등록되었지만 트랜잭션이 적용되지 않는 경우가 있다.
대표적인 경우는 프록시 객체의 대상객체의 내부에서 메서드 호출이 발생하여 프록시를 거치지 않고 대상객체를 직접
호출하는 것이다. 코드를 살펴보자. 실무에서 굉장히 중요한 문제이다.
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
void internalCall() {
//트랜잭션 걸린 메서드 호출
callService.internal();
}
@Test
void externalCall() {
//트랜잭션 없는 메서드 호출
callService.external();
}
@Slf4j
@TestConfiguration
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);
}
}
}
위 코드를 실행해보면, internalCall 테스트는 문제없이 트랜잭션이 정상 작동한다.
문제는 externalCall 테스트에서 발생한다.
externalCall 테스트의 callService.external() 메서드는 트랜잭션이 설정되어 있지 않다. 그런데 external 메서드 내부에서
트랜잭션이 걸린 internal 메서드를 내부호출하고 있다. 이 경우 internal 메서드의 트랜잭션이 실행되지 않는다.
<externalCall 테스트의 로그>
h.s.a.InternalCallV1Test$CallService : call external
[ main] h.s.a.InternalCallV1Test$CallService : tx active = false
[ main] h.s.a.InternalCallV1Test$CallService : call internal //external 통해 internal 호출
[ main] h.s.a.InternalCallV1Test$CallService : tx active = false //트랜잭션은 실행되지 않는다.
트랜잭션 내부호출 - 클래스 분리

@SpringBootTest
public class InternalCallV2Test {
@Autowired CallService callService;
@Test
void externalCallV2() {
callService.external();
}
@Slf4j
@TestConfiguration
@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
@TestConfiguration
static class InternalService {
// 트랜잭션 적용한 메서드를 별도의 클래스로 분리했다.
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
InternalService 클래스를 만들고 internal() 메서드를 여기로 옮겼다. 이렇게 메서드 내부 호출을 외부 호출로 변경했다.
CallService 에는 트랜잭션 관련 코드가 전혀 없으므로 트랜잭션 프록시가 적용되지 않는다.
InternalService 에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용된다.
1. 클라이언트인 테스트 코드는 callService.external() 을 호출한다.
2. callService 는 실제 callService 객체 인스턴스이다.
3. callService 는 주입 받은 internalService.internal() 을 호출한다.
4. internalService 는 트랜잭션 프록시이다. internal() 메서드에 @Transactional 이 붙어 있으므로
트랜잭션 프록시는 트랜잭션을 적용한다.
5. 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 을 호출한다.
트랜잭션 초기화 시점
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
void go() {
}
@Slf4j
@TestConfiguration
static class Hello {
// 트랜잭션이 작동하지 않는다.
@PostConstruct
@Transactional
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);
}
}
}
위 테스트 코드를 실행할 경우, @PostConstruct 애너테이션을 사용한 initV1() 메서드는 트랜잭션이 실행되지 않는다.
반면 @EventListener(value = ApplicationReadyEvent.class) 사용한 initV2() 메서드는 트랜잭션이 실행된다.
@PostContruct 애너테이션은 초기화 코드가 먼저 호출되고, 그 뒤에 트랜잭션 AOP가 적용되기 때문이다.
따라서 초기화 시점에는 트랜잭션을 획득할 수 없다.
생성시점에 트랜잭션을 획득하고 싶다면, @EventListener(value = ApplicationReadyEvent.class) 를 사용하자
트랜잭션 여러 옵션
| 옵션 | 설명 | 예시 |
| value | 트랜잭션 매니저를 여러개 사용하는 경우, 트랜잭션 매니저의 이름을 구분할 수 있다. |
@Transactional("memberTxManager") public void member() {...} |
| rollbackFor | 언체크 예외는 롤백이 기본설정이지만, 체크예외는 커밋되는 것이 기본이다. 이 설정을 사용하면 체크예외도 롤백을 하도록 설정할 수 있다. |
@Transactiional(rollbackFor = Exception.class) |
| noRollbackFor | rollbackFor와 반대 어떤 예외가 발생했을때, 롤백하지 않도록 설정 |
@Transactional (noRollbackFor = RuntimeException.class) |
| readOnly | 읽기 전용 트랜잭션이 생성된다. 읽기에서 다양한 성능 최적화가 발생할 수 있다. 즉 데이터 변경 할 수 없다. |
@Transactional(readOnly = true) |
예외와 트랜잭션 커밋, 롤백
스프링은 체크 예외 (Exception 예외) 는 비지니스 의미가 있을때 사용하고,
언체크 예외 (RuntimeException 예외) 는 복구 불가능한 시스템 예외일때 사용한다.
예를들어, 할인혜택을 받을 수 없는 고객이 할인혜택을 신청한 경우, 이는 시스템의 잘못이 아니라 고객이 잘못된 요청을
한 것이고, 이에따라 시스템은 고객의 잘못된 요청에 대한 대응이 필요하다. 이때 필요한 것이 체크예외이다.
반면 고객이 할인 혜택을 신청했지만, 시스템에서 할인혜택을 담는 변수 타입이 잘못되었다면, 이는 시스템의 잘못이고,
개발자가 반드시 수정해야 한다. 이때 필요한 것이 런타임 예외이다.
그렇다면 트랜잭션에서 각각의 예외를 어떻게 처리할지 생각해보자, 체크예외는 커밋한다.
그래야 고객의 잘못된 요청에 대해 비지니스적으로 대응을 해야 하기 때문이다. 할인혜택을 잘못 선택했다고,
고객의 주문요청 자체를 없애버리는 것을 올바르지 않다. 할인혜택을 사용할 수 없음을 안내하고 다시 주문하도록 한다.
반면 런타임 예외는 롤백한다. 할인요청이 제대로 처리되지 않은 주문을 DB에 저장하면 안될것이다.
이후 모든 처리가 잘못된다.
'프로그래밍 > Spring' 카테고리의 다른 글
| [Spring] 동시성 문제와 쓰레드 로컬 (Thread Local) (0) | 2024.05.07 |
|---|---|
| [Spring] 스프링 트랜잭션 전파 (0) | 2024.03.18 |
| [Spring] 스프링 데이터 JPA (0) | 2024.03.11 |
| [Spring] MyBatis 사용하기 (0) | 2024.03.07 |
| [Spring] DB 연결을 테스트 하기 (0) | 2024.03.06 |