- 스프링은 트랜잭션을 추상화해서 제공한다. 개발자는 필요한 구현체를 스프링 빈으로 등록하고 주입 받아서 사용하면 된다.
- JdbcTemplate , MyBatis 를 사용하면 DataSourceTransactionManager(JdbcTransactionManager) 를 스프링 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager 를 스프링 빈으로 등록해준다.
PlatformTransactionManager 를 사용하는 방법
- 선언적 트랜잭션 관리 (Declarative Transaction Management)
@Transactional 애노테이션 하나만 선언 - 프로그래밍 방식의 트랜잭션 관리 (Programmatic Transaction Management)
트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것
선언적 트랜잭션과 AOP
@Transactional 을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용된다.


트랜잭션 프록시 코드 예시
public class TransactionProxy {
private MemberService target;
public void logic() {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(..);
try {
//실제 대상 호출
target.logic();
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
}
트랜잭션 프록시 적용 후 서비스 코드 예시
public class Service {
public void logic() {
//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
bizLogic(fromId, toId, money);
}
}
- 프록시 도입 전: 서비스에 비즈니스 로직과 트랜잭션 처리 로직이 함께 섞여있다.
- 프록시 도입 후: 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다.
트랜잭션 프록시 덕분에 서비스 계층에는 순수한 비즈니즈 로직만 남길 수 있다.

- 트랜잭션은 커넥션에 con.setAutocommit(false) 를 지정하면서 시작한다.
- 같은 트랜잭션을 유지하려면 같은 데이터베이스 커넥션을 사용해야 한다.
- 이것을 위해 스프링 내부에서는 트랜잭션 동기화 매니저가 사용된다
- JdbcTemplate 을 포함한 대부분의 데이터 접근 기술들은 트랜잭션을 유지하기 위해 내부에서 트랜잭션 동기화 매니저를 통해 리소스(커넥션)를 동기화 한다.
스프링이 제공하는 트랜잭션 AOP = @Transactional
트랜잭션 적용 확인

- @Transactional이 특정 클래스나 메서드 있으면 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록
ex) basicService 객체 대신에 프록시인 basicService$$CGLIB 를 스프링 빈에 등록 - 핵심은 실제 객체 대신에 프록시가 스프링 컨테이너에 등록되었다는 점
- 클라이언트인 txBasicTest 는 스프링 컨테이너에 @Autowired BasicService basicService 로 의존관계 주입을 요청
- 프록시는 BasicService 를 상속해서 만들어지기 때문에 다형성을 활용할 수 있다
- BasicService 대신에 프록시인 BasicService$$CGLIB 를 주입

basicService.tx() 호출
트랜잭션을 시작한 다음에 실제 basicService.tx() 를 호출한다.
그리고 실제 basicService.tx() 의 호출이 끝나서 프록시로 제어가(리턴) 돌아오면 프록시는
트랜잭션 로직을 커밋하거나 롤백해서 트랜잭션을 종료한다.
basicService.nonTx() 호출
- 트랜잭션을 시작하지 않고, basicService.nonTx() 를 호출하고 종료한다.
application.properties
// 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확하게 로그로 확인 가능
logging.level.org.springframework.transaction.interceptor=TRACE
TransactionSynchronizationManager.isActualTransactionActive()
- 현재 쓰레드에 트랜잭션이 적용되어 있는지 확인할 수 있는 기능이다.
- 결과가 true 면 트랜잭션이 적용되어 있는 것이다.
트랜잭션 적용 위치
스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
※ 스프링의 @Transactional 규칙
- 우선순위 규칙
- 클래스에 적용하면 메서드는 자동 적용
- @Transactional == @Transactional(readOnly=false)
- 기본 값: readOnly = false
TransactionSynchronizationManager.isCurrentTransactionReadOnly
- 현재 트랜잭션에 적용된 readOnly 옵션의 값을 반환
인터페이스에 @Transactional 적용
- 클래스의 메서드 (우선순위가 가장 높다.)
- 클래스의 타입
- 인터페이스의 메서드
- 인터페이스의 타입 (우선순위가 가장 낮다.)
- 인터페이스에 @Transactional 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이다.
- AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기 때문
트랜잭션 AOP 주의 사항 - 프록시 내부 호출1
- 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
- 이렇게 되면 @Transactional 이 있어도 트랜잭션이 적용되지 않는다.
InternalCall
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@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);
}
}
}


- 클라이언트인 테스트 코드는 callService.external() 을 호출한다. 여기서 callService 는 트랜잭션 프록시이다.
- callService 의 트랜잭션 프록시가 호출된다.
- external() 메서드에는 @Transactional 이 없다. 따라서 트랜잭션 프록시는 트랜잭션을 적용하지 않는다.
- 트랜잭션 적용하지 않고, 실제 callService 객체 인스턴스의 external() 을 호출한다.
- external() 은 내부에서 internal() 메서드를 호출한다. 그런데 여기서 문제가 발생한다.
문제 원인
- 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다.
- this.internal(), 즉 실제 대상 객체( target )의 인스턴스를 뜻한다.
프록시 방식의 AOP 한계 : 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.
트랜잭션 AOP 주의 사항 - 프록시 내부 호출2
InternalCall
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
void externalCallV2() {
callService.external();
}
//@TestConfiuration 생략
@Slf4j
@RequiredArgsConstructor
static class CallService {
private final InternalService internalService;
public void external() {
log.info("call external");
printTxInfo();
internalService.internal();
}
// printTxInfo() 생략
}
@Slf4j
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
// printTxInfo() 생략
}
}
- InternalService 클래스를 만들고 internal() 메서드를 여기로 옮겼다.
- 이렇게 메서드 내부 호출을 외부 호출로 변경했다.
- CallService 에는 트랜잭션 관련 코드가 전혀 없으므로 트랜잭션 프록시가 적용되지 않는다.
- InternalService 에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용된다.

- 클라이언트인 테스트 코드는 callService.external() 을 호출한다.
- callService 는 실제 callService 객체 인스턴스이다.
- callService 는 주입 받은 internalService.internal() 을 호출한다.
- internalService 는 트랜잭션 프록시이다.
internal() 메서드에 @Transactional 이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다. - 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 을 호출한다.
public 메서드만 트랜잭션 적용
- 스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있다.
- 그래서 protected , private , package-visible 에는 트랜잭션이 적용되지 않는다.
- public 이 아닌곳에 @Transactional 이 붙어 있으면 예외가 발생하지는 않고, 트랜잭션 적용만 무시된다.
트랜잭션 AOP 주의 사항 - 초기화 시점
@Slf4j
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={}",
}
}
- 초기화 코드(예: @PostConstruct )와 @Transactional 을 함께 사용하면 트랜잭션이 적용되지 않는다.
- 왜냐하면 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다.
- 가장 확실한 대안은 ApplicationReadyEvent 이벤트를 사용하는 것이다.
- 이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출해준다.
트랜잭션 옵션 소개
value, transactionManager
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
@Transactional("orderTxManager")
public void order() {...}
}
- 트랜잭션 매니저를 지정할 때는 value , transactionManager 둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어주면 된다.
- 애노테이션에서 속성이 하나인 경우 value는 생략하고 값을 바로 넣을 수 있다.
rollbackFor
- 언체크 예외인 RuntimeException , Error 와 그 하위 예외가 발생하면 롤백한다.
- 체크 예외인 Exception 과 그 하위 예외들은 커밋한다.
@Transactional(rollbackFor = Exception.class)
- 이 옵션을 사용하면 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정 가능
ex) 체크 예외인 Exception이 발생해도 롤백하게 된다.
- rollbackFor 는 예외 클래스를 직접 지정
- rollbackForClassName 는 예외 이름을 문자로 넣으면 된다.
noRollbackFor
- rollbackFor 와 반대
- 기본 정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정 가능
예외 이름을 문자로 넣을 수 있는 noRollbackForClassName 도 있다.
propagation
- 트랜잭션 전파에 대한 옵션이다.
isolation
- 트랜잭션 격리 수준을 지정
- 기본 값 : DEFAULT
- DEFAULT : 데이터베이스에서 설정한 격리 수준을 따른다.
- READ_UNCOMMITTED : 커밋되지 않은 읽기
- READ_COMMITTED : 커밋된 읽기
- REPEATABLE_READ : 반복 가능한 읽기
- SERIALIZABLE : 직렬화 가능
timeout
- 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정
- timeoutString 도 있는데, 숫자 대신 문자 값으로 지정
label
- 트랜잭션 애노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용
- 일반적으로 사용하지 않음.
readOnly
- 트랜잭션은 기본적으로 읽기 쓰기가 모두 가능한 트랜잭션이 생성
- readOnly=true 옵션을 사용하면 읽기 전용 트랜잭션이 생성
readOnly 옵션은 크게 3곳에서 적용
- 프레임워크
- JDBC 드라이버
- 데이터 베이스
예외와 트랜잭션 커밋, 롤백 - 기본

- 언체크 예외인 RuntimeException , Error 와 그 하위 예외가 발생하면 트랜잭션을 롤백한다. (복구 불가능한 예외)
- 체크 예외인 Exception 과 그 하위 예외가 발생하면 트랜잭션을 커밋한다. (비지니스 의미가 있을 때)
application.properties : 트랜잭션이 커밋되었는지 롤백 되었는지 로그로 확인
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
예외와 트랜잭션 커밋, 롤백 - 활용
- 정상 : payStatus 를 완료 상태로 처리하고 정상 처리된다.
- 시스템 예외 : RuntimeException("시스템 예외") 런타임 예외가 발생한다.
- 비지니스 예외 (잔고부족) : payStatus를 대기 상태로처리한다.
NotEnoughMoneyException("잔고가 부족합니다") 체크 예외가 발생한다.
잔고 부족은 payStatus 를 대기 상태로 두고, 체크 예외가 발생하지만, order 데이터는 커밋되기를 기대
logging.level.org.hibernate.SQL=DEBUG
application.properties : 테이블 자동 생성
spring.jpa.hibernate.ddl-auto
- none : 테이블을 생성하지 않는다.
- create : 애플리케이션 시작 시점에 테이블을 생성한다.
complete()
- 사용자 이름을 정상 으로 설정했다. 모든 프로세스가 정상 수행된다.
runtimeException()
- 사용자 이름을 예외 로 설정했다.
- RuntimeException("시스템 예외")이발생한다.
- 런타임 예외로 롤백이 수행되었기 때문에 Order 데이터가 비어 있는 것을 확인할 수 있다.
bizException()
- 체크 예외로 커밋이 수행되었기 때문에 Order 데이터가 저장된다.
◎ 정리
- NotEnoughMoneyException 은 비즈니스 문제 상황을 예외를 알려준다. 마치 예외가 리턴 값 처럼 사용된다.
따라서 이 경우에는 트랜잭션을 커밋하는 것이 맞다. 이 경우 롤백하면 생성한 Order 자체가 사라진다.
고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내해도 주문( Order ) 자체가 사라지기 때문에 문제가 된다. - 그런데 비즈니스 상황에 따라 체크 예외의 경우에도 롤백하고 싶을 수 있다.
- 이때는 rollbackFor 옵션을 사용
'Spring > DB 2편' 카테고리의 다른 글
스프링 트랜잭션 전파2 - 활용 (0) | 2023.08.12 |
---|---|
스프링 트랜잭션 전파1 - 기본 (0) | 2023.08.11 |
데이터 접근 기술 - 활용 방안 (0) | 2023.08.11 |
데이터 접근 기술 - Querydsl (0) | 2023.08.11 |
데이터 접근 기술 - 스프링 데이터 JPA (0) | 2023.08.11 |