스프링 트랜잭션 소개
스프링 트랜잭션 추상화
JDBC 기술과 JPA 기술은 트랜잭션을 사용하는 코드 자체가 다름
JDBC
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직
bizLogic(con, fromId, toId, money); con.commit();
// 성공시 커밋
con.commit();
} catch (Exception e) {
con.rollback(); // 실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
JPA
public static void main(String[] args) {
// 엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
EntityTransaction tx = em.getTransaction(); // 트랜잭션 기능 획득
try {
tx.begin(); // 트랜잭션 시작
logic(em); // 비즈니스 로직
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); // 트랜잭션 롤백
} finally {
em.close(); // 엔티티 매니저 종료
}
emf.close(); // 엔티티 매니저 팩토리 종료
}
-> JDBC를 사용하다 JPA로 변경하게 되면 트랜잭션 코드를 모두 변경해야 함
-> 스프링은 이런 문제를 해결하기 위해 트랜잭션 추상화를 제공
=> PlatformTransactionManager
PlatformTransactionManager : 인터페이스


1) 스프링은 트랜잭션을 추상화해 제공함
2) 스프링은 실무에서 주로 사용하는 데이터 접근 기술에 대한 트랜잭션 매니저의 구현체도 제공함 -> 필요한 구현체를 스프링 빈으로 등록하고 주입 받아서 사용하기만 하면 됨
3) 스프링 부트는 어떤 데이터 접근 기술을 사용하는지 자동으로 인식해 적절한 트랜잭션 매니저를 선택해 스프링 빈으로 등록해줌
스프링 트랜잭션 사용 방식
1. 선언적 트랜잭션 관리(Declarative Transactioin Management)
1) @Transactional 애노테이션 선언으로 관리
2) 과거 XML에 설정하기도 했음
3) 이름 그대로 선언하기만 하면 트랜잭션이 적용됨
4) 실무에서는 대부분 선언적 트랜잭션 관리를 사용
2. 프로그래밍 트랜잭션 관리(Programmatic Transaction Management
1) 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해 트랜잭션 관련 코드를 직접 작성
2) 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결함됨
선언적 트랜잭션 & AOP

트랜잭션을 처리하기 위한 프록시를 적용하면 트랜잭션 처리 객체와 비지니스 로직을 처리하는 서비스 객체를 명확히 분리할 수 있다.
1. 트랜잭션은 커넥션에 con.setAutocommit(false)를 지정하며 시작
2. 같은 트랜잭션을 유지하려면 같은 DB 커넥션을 사용해야 함
3. 이를 위해 스프링 내부에 트랜잭션 동기화 매니저가 사용됨
4. JdbcTemplate을 포함한 대부분의 데이터 접근 기술들은 트랜잭션을 유지하기 위해 내부에 트랜잭션 동기화 매니저를 통해 리소스(커넥션)을 동기화 함
=> 스프링 트랜잭션은 매우 중요한 기능이고 많이 사용하는 기능임
=> 스프링은 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공해줌
=> 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 됨
트랜잭션 적용 확인
@Transactional을 통해 선언적 트랜잭션 방식을 사용하면, 단순하게 트랜잭션 적용이 가능하지만 트랜잭션 관련 코드가 눈에 보지이 않고 AOP를 기반으로 동작하기 때문에 실제로 적용이 되었는지 확인이 어렵다.
스프링 트랜잭션이 잘 적용되고 있는지 확인하는 방법을 알아보자.
TxApplyBasicTest.java
package hello.springtx.apply;
import jakarta.persistence.Basic;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired BasicService basicService;
@Test
void proxyCheck() {
// aop class=class hello.springtx.apply.TxBasicTest$BasicService$$SpringCGLIB$$0
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("nonTx active={}", txActive);
}
}
}
-> TransactionSynchronizationManager.isActualTransactionActive() : 현재 스레드에 트랜잭션이 적용되어 있는지 확인할 수 있는 기능으로 true이면 트랜잭션이 적용되어 있는 것 = 트랜잭션 적용 여부를 가장 확실히 알 수 있음

1. @Transactional 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어 스프링 컨테이너에 등록한다. -> 실제 basicService 객체 대신 프록시인 basicService&&CGLIB을 스프링 빈에 등록
2. 프록시는 내부에 실제 basicService를 참조 -> 실제 객체 대신 프록시가 컨테이너에 등록
3. 클라이언트인 txBasicTest는 스프링 컨테이너에 @Autowired BasicService basicService로 의존관계 주입을 요청한다. 스프링 컨테이너에 프록시가 등록되어 있기 때문에 프록시를 주입한다.
4. 프록시는 BasicService를 상속해 만들어지기 때문에 다형성을 활용할 수 있다.
application.properties에 로그 추가

-> 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확히 로그로 확인 가능
실행 결과 확인 : tx() & nonTx()


tx() 호출
1. 클라이언트가 basicService.tx()를 호출하면 프록시의 tx()가 호출됨
2. 프록시는 tx()가 트랜잭션을 사용할 수 있는지 확인 : @Transactional이 붙어있으므로 트랜잭션 적용 대상이 됨
3. 트랜잭션을 시작한 다음 실제 basicService.tx() 호출
4. 실제 tx() 호출이 끝나고 프록시로 제어가 돌아오면 트랜잭션 로직을 커밋 or 롤백 하며 트랜잭션 종료
nonTx() 호출
1. 클라이언트가 basicService.nonTx()를 호출하면 프록시의 nonTx()가 호출됨
2. 프록시는 tx()가 트랜잭션을 사용할 수 있는지 확인 : @Transactional이 없으므로 적용 대상이 아님
3. 트랜잭션을 시작하지 않고 basicService.nonTx()를 호출하고 종료
트랜잭션 적용 위치
스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다!
ex) 메서드와 클래스에 애노테이션을 붙일 수 있다면, 더 구체적인 메서드가 더 높은 우선순위를 가짐
스프링 @Transactional의 규칙
1. 우선순위 규칙
2. 클래스에 적용하면 메서드는 자동 적용
TxLevelTest.java
package hello.springtx.apply;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@SpringBootTest
public class TxLevelTest {
@Autowired LevelService levelService;
@Test
void orderTest() {
levelService.write();
levelService.read();
}
@TestConfiguration
static class TxLevelTestConfig {
@Bean
LevelService levelService() {
return new LevelService();
}
}
@Slf4j
@Transactional(readOnly = true)
static class LevelService {
@Transactional(readOnly = false)
public void write() {
log.info("call write");
printTxInfo();
}
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);
}
}
}
-> write()에는 읽기 쓰기 트랜잭션이 적용되었다.
-> read()에는 읽기 전용 트랜잭션 옵션인 readOnly가 적용되었다.
실행 결과

인터페이스에 @Transactional 적용 시 우선순위
1. 클래스의 메서드(가장 높은 우선순위)
2. 클래스 타입
3. 인터페이스 메서드
4. 인터페이스 타입
but 인터페이스에 @Transactional을 사용하는 것은 스프링 공식 메뉴얼에서 권장 X
-> AOP 적용 방식에 따라 인터페이스에 애노테이션을 두면 AOP가 적용되지 않는 경우도 있기 때문
트랜잭션 AOP 주의 사항 - 프록시 내부 호출 1
@Transactional을 사용하면 스프링의 트랜잭션 AOP가 적용된다. 그리고 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용한다.
프록시 객체가 요청을 먼저 받아 트랜잭션을 처리하고, 실제 객체를 호출해준다.
따라서 트랜잭션을 적용하려면 항상 프록시를 통해 대상 객체(Target)을 호출해야 한다.
만약 프록시를 거치지 않고 바로 타겟을 호출하면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다.

- 스프링은 의존관계 주입 시 항상 실제 객체 대신 프록시 객체를 주입한다.
- 일반적으로는 대상 객체를 직접 호출하는 문제가 발생하지 않지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 직접 대상 객체를 호출하는 문제가 발생한다.
- 이렇게 될 경우 @Transactional이 있어도 트랜잭션이 적용되지 않는다. -> 실무에서 만나게 될 가능성이 높은 문제
InternalCallV1Test.java
package hello.springtx.apply;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired CallService callService;
@Test
void printProxy() {
// callService class=class hello.springtx.apply.InternalCallV1Test$CallService$$SpringCGLIB$$0
log.info("callService class={}", callService.getClass());
}
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall() {
callService.external();
}
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService();
}
}
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);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}", readOnly);
}
}
}
internalCall() 함수 실행 결과

externalCall() 함수 실행 결과

프록시 방식의 AOP 한계

1. callService.external() 호출 : callService는 트랜잭션 프록시
2. callService의 트랜잭션 프로시가 호출됨
3. external() 메서드에는 @Transactional이 없으므로 트랜잭션 프록시가 트랜잭션을 적용하지 않음
4. 트랜잭션을 적용하지 않고 실제 callService 객체 인스턴스의 external() 호출
5. external() 내부에서 internal() 호출 -> 문제 발생
문제 원인
: 자바 언어에서 메서드 앞에 별도 참조가 없으면 this로 자기자신의 인스턴스를 가리킨다.
결국 this.internal()이 되고 여기서 this는 실제 대상 객체(target)의 인스턴스를 뜻한다.
이러한 내부 호출은 프록시를 거치지 않게 되므로 트랜잭션을 적용할 수 없다.
=> 프록시를 사용해도 메서드 내부 호출에 프록시를 적용할 수 없다.
트랜잭션 AOP 주의 사항 - 프록시 내부 호출 2
internal()을 별도의 클래스로 분리해서 해결해보자.
InternalCallV2Test.java : 메서드 내부 호출을 외부 호출로 변경


외부 호출로 변경해 트랜잭션 적용

1. callService.external() 호출 : callService는 실제 callService의 객체 인스턴스
2. callService는 주입받은 internalService.internal() 호출
3. internalService는 트랜잭션 프록시 : internal()에 @Transactional이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
4. 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 호출
public 메서드만 트랜잭션 적용
스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어 있다.
protected, private, package-visible 에는 트랜잭션이 적용되지 않는다.
public에만 트랜잭션을 적용하는 이유?
@Transactional
public class Hello {
public method1();
method2():
protected method3();
private method4();
}
-> 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있어 의도하지 않은 곳까지 트랜잭션이 과도하게 적용됨
-> 트랜잭션은 주로 비지니스 로직 시작점에 걸기에 대부분 외부에 열어준 곳을 시작점으로 사용함
=> 이러한 이유로 public 메서드에만 트랜잭션을 적용하도록 설정되어 있음
* 스프링 부트 3.0 부터는 protected, package-visible에도 트랜잭션이 적용된다.
트랜잭션 AOP 주의 사항 - 초기화 시점
스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.
InitTxTest.java
package hello.springtx.apply;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@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
@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);
}
}
}

initV1()
@PostConstruct와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.
: 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문
initV2()
확실한 대안은 ApplicaionReadyEvent를 사용하는 것이다.
이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음 이벤트가 붙은 메서드를 호출해준다.
트랜잭션 옵션 소개
value, transactionManager
- 트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할지 알아야 한다.
- @Transactional 에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다.
- 사용할 트랜잭션 매니저를 지정할 때는 value, transactionManager 둘 중 하나에 트랜잭션 매니저의 스프링 빈 이름을 적어주면 된다.
- 이 값을 생략하면, 기본으로 등록된 트랜잭션 매니저를 사용하기에 대부분 생략한다.
@Transactional("memberTxManager")
public void member() {...}
-> 애노테이션 속성이 하나인 경우 "value="는 생략하고 값을 바로 넣을 수 있음
rollbackFor
예외 발생 시 스프링 트랜잭션의 기본 정책
- 언체크 예외인 RuntimeException, Error와 그 하위 예외 발생 시 롤백
- 체크 예외인 Exception과 그 하위 예외들은 커밋
@Transactional(rollbackFor = Exception.class)
-> 위 옵션을 사용해 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할지 지정 가능
-> rollbackForClassName 옵션도 존재하는데, rollbackFor는 예외 클래스를 직접 지정하고 rollbackForClassName은 예외 이름을 문자로 넣으면 됨
noRollbackFor
- rollbackFor 옵션과 반대로 기본 정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정 가능
propagation
- 트랜잭션 전파에 대한 옵션으로 뒤에 다시 설명
isolation
- 트랜잭션 격리 수준 지정
- 기본 값을 DB에서 설정한 트랜잭션 격리 수준을 사용하는 'DEFAULT'
격리 수준 종류
1. DEFAULT : DB에서 설정한 격리 수준을 따른다.
2. READ_UNCOMMITTED : 커밋되지 않은 읽기
3. READ_COMMITTED : 커밋된 읽기
4. REPEATABLE_READ : 반복 가능한 읽기
5. SERIALIZABLE : 직렬화 기능
timeout
- 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정
- 기본 값은 트랜잭션 시스템의 타임아웃을 사용
- timeoutString 옵션도 존재하는데, 숫자 대신 문자 값으로 지정
label
- 트랜잭션 애노테이션에 있는 값을 직접 읽어 어떤 동작을 하고 싶을 때 사용(일반적으로는 사용 X)
readOnly
- 기본적으로 읽기 쓰기가 모두 가능한 트랜잭션이 생성됨
- readOnly=true 옵션을 사용하면 읽기 전용 트랜잭션이 생성됨 : 등록, 수정, 삭제 불가능
- readOnly 옵션 사용 시 읽기에서 다양한 성능 최적화가 발생할 수 있음
readOnly 옵션이 크게 3곳에서 적용된다.
1. 프레임워크
- JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던진다.
- JPA는 읽기 전용 트랜잭션의 경우 커밋 시점에 플러시를 호출하지 않는다. 스냅샷 객체도 생성하지 않는다. -> 최적화 발생
2. JDBC 드라이버
- DB와 드라이버 버전에 따라 다르게 동작하기 때문에 사전 확인 필요
- 읽기 전용 트랜잭션에서 변경 쿼리 발생 시 예외를 던진다.
- 읽기, 쓰기 데이터베이스를 구분해 요청 : 읽기 전용 트랜잭션의 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해 사용
3. 데이터베이스
- 데이터베이스에 따라 읽기 전용 트랜잭션의 경우 읽기만 하면 되므로 내부에서 성능 최적화 발생
예외와 트랜잭션 커밋, 롤백
예외가 발생했는데 내부에서 예외를 처리하지 못하고 트랜잭션 범위 밖으로 예외를 던지면 어떻게 될까?
예외 발생 시 스프리이 트랜잭션 AOP는 예외 종류에 따라 트랜잭션을 커밋 or 롤백한다.

-> 언체크 예외인 RuntimeException, Error와 하위 예외 발생 시 트랜잭션 롤백
-> 체크 예외인 Exception과 하위 예외 발생 시 트랜잭션 커밋
RollbackTest.java
package hello.springtx.apply.exception;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
public class RollbackTest {
@Autowired
RollbackService rollbackService;
@Test
void runtimeException() {
rollbackService.runtimeException();
}
@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 {
}
}
application.properties 추가

-> 현재 JPA를 사용하므로 트랜잭션 매니저로 JpaTransactionManager가 실행되고, 여기의 로그를 출력할 수 있도록
1. runtimeException() 실행결과

2. checkedException() 실행 결과

3. rollbackFor()

'인프런 > 스프링 DB 2편 - 데이터 접근 활용 기술' 카테고리의 다른 글
| [인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 11. 스프링 트랜잭션 전파2 - 활용 (1) | 2024.09.22 |
|---|---|
| [인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 10. 스프링 트랜잭션 전파1 - 기본 (2) | 2024.09.17 |
| [인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 8. 데이터 접근 기술 - 활용 방안 (2) | 2024.09.01 |
| [인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 7. 데이터 접근 기술 - Querydsl (0) | 2024.08.22 |
| [인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 6. 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2024.08.11 |