본문 바로가기
Study/Spring

[Spring] Transaction & 전파

by Lpromotion 2025. 7. 10.

1. 트랜잭션

1.1. 트랜잭션이란?

트랜잭션(Transaction)은 데이터베이스에서 하나의 작업 단위를 의미하며, 다음을 보장해야 한다.

  • 모든 연산이 성공적으로 완료되면 커밋
  • 하나라도 실패하면 전체 롤백

예시) 은행 이체:

A → B로 1만원 송금 = A 계좌 - 1 만원, B 계좌 + 1만원

둘 중 하나라도 실패하면 전체 최소되어야 함.

 

1.2. 트랜잭션의 4대 특성 (ACID)

특성 설명
A - Atomicity (원자성) 모든 작업이 전부 반영되거나, 전혀 반영되지 않아야 한다.
C - Consistency (일관성) 트랜잭션 수행 전후의 데이터는 일관된 상태를 유지해야 한다.
I - Isolation (격리성) 동시에 수행되는 트랜잭션은 서로의 연산에 간섭하지 않아야 한다.
D - Durability (지속성) 커밋된 트랜잭션의 결과는 시스템 장애가 나도 보존되어야 한다.

⇒ 실패해도 무결성 보장, 동시성 제어, 시스템 장애 복원성

 

1.3. DB에서 트랜잭션 처리 방식

JDBC 기반 예시

Connection conn = dataSource.getConnection();
try {
    conn.setAutoCommit(false); // 수동 커밋
    // 쿼리 실행
    ...
    conn.commit(); // 성공 시 커밋
} catch (Exception e) {
    conn.rollback(); // 실패 시 롤백
} finally {
    conn.close();
}
  • setAutoCommit(false) → 직접 커밋하기 전까지는 반영되지 않음
  • 실패하면 rollback() 호출로 모두 취소됨

 

4. Isolation Level

트랜잭션 간 충돌을 막기 위한 격리 수준 설정

격리 수준 방지되는 문제
READ UNCOMMITTED Dirty Read 허용됨
READ COMMITTED Dirty Read 방지, Non-Repeatable Read 허용됨
REPEATABLE READ Non-Repeatable Read 방지, Phantom Read 허용됨
SERIALIZABLE Phantom Read 방지 (가장 엄격)
  • Dirty Read: 한 트랜잭션이 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 경우
  • Non-Repeatable Read: 같은 쿼리를 두 번 실행했는데 사이에서 다른 트랜잭션이 값을 수정해서 결과가 다르게 나오는 경우
  • Phantom Read: 같은 조건으로 여러 번 조회했는데, 사이에서 새로운 행이 삽입되어 결과 개수가 달라지는 경우

트랜잭션 격리 수준이 높을수록 데이터 일관성은 보장되지만 성능과 동시성은 낮아짐.

대부분의 RDBMS는 기본값으로 READ COMMITED 사용하며, 특별한 경우에만 상위 격리 수준을 적용함.

 

 

2. Spring 트랜잭션의 기본 개념 이해

2.1. `@Transactional`은 무엇인가?

@Transactional은 Spring이 제공하는 선언적 트랜잭션 처리 방식이다.

  • 선언적: 코드로 명령하지 않고, 애노테이션으로 트랜잭션 범위를 설정한다.
  • 내부적으로는 AOP 기반의 프록시 패턴으로 작동한다.
@Transactional
public void transfer() {
    // 계좌 이체 로직
}

→ 해당 메서드가 호출되면, Spring은 트랜잭션을 시작하고, 정상 완료되면 commit, 예외 발생 시 rollback

한다.

 

@Transactional과 AOP

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 관심사를 분리하여 모듈성을 높이는 프로그래밍 패러다임

  • '관심사'란 애플리케이션의 핵심 비즈니스 로직과, 로깅, 트랜잭션, 보안과 같이 여러 모듈에 공통적으로 나타나는 부가 기능을 의미
  • AOP는 이러한 부가 기능(횡단 관심사, cross-cutting concerns)을 애스펙트(Aspect)라는 별도의 모듈로 분리하여 관리함. 그리고 이 애스펙트를 필요한 코드에 동적으로 삽입하여 마치 처음부터 그 코드가 있었던 것처럼 동작하게 만듬.
  • 쉽게 말해, 핵심 로직을 담고 있는 여러 메서드에 공통적으로 필요한 '트랜잭션 시작 및 종료' 코드를 직접 작성하는 대신, '트랜잭션'이라는 기능을 따로 만들어두고 "어떤 메서드가 실행될 때 이 기능을 적용해줘"라고 설정하는 방식

 

@Transactional이 AOP 기반으로 동작한다는 것은 다음과 같은 의미를 가진다.

  1. 프록시(Proxy) 생성: 스프링은 @Transactional이 붙은 클래스나 메서드에 대해 프록시(대리인) 객체를 생성함. 실제 객체 대신 이 프록시 객체가 요청을 먼저 받음.
  2. 부가 기능 실행: 프록시 객체는 핵심 비즈니스 로직을 실행하기 전(Before)에 트랜잭션을 시작하고, 로직이 실행된 후(After)에 트랜잭션을 커밋하거나 롤백하는 부가 기능을 수행함.
  3. 핵심 로직 호출: 프록시는 부가 기능 처리가 끝나면 실제 객체의 메서드를 호출하여 원래의 비즈니스 로직을 실행함.

→ 개발자는 비즈니스 로직에만 집중할 수 있고, 트랜잭션 처리와 같은 공통 부가 기능은 AOP를 통해 일관되게 관리할 수 있다. 즉, @Transactional이라는 애노테이션 하나로 복잡한 트랜잭션 관리 코드를 핵심 로직에서 분리해내는 것이다.

 

2.2. 어떻게 작동하는가? (내부 구조)

Spring 트랜잭션 처리 흐름

  1. @Transactional 메서드 호출
  2. AOP 프록시가 트랜잭션 시작 여부 결정
  3. 트랜잭션 시작 (PlatformTransactionManager.begin())
  4. 실제 메서드 실행
  5. 정상 종료: commit() / 예외 발생: rollback()
[프록시 객체] --(@Transactional)--> 트랜잭션 시작
     |
     +-- 실제 메서드 호출
     |
     +-- 트랜잭션 커밋 or 롤백

 

2.3. 예외에 따른 commit vs rollback

Spring의 기본 설정

예외 타입 rollback 여부
RuntimeException 계열 (unchecked) O (자동 rollback)
Exception 또는 CheckedException X (commit 됨)

해결 방법

@Transactional(rollbackFor = Exception.class) 설정을 통해 checked 예외도 rollback 가능

@Transactional(rollbackFor = IOException.class)
public void uploadFile() throws IOException {
    ...
}

 

2.4. PlatformTransactionManager의 역할

트랜잭션의 실제 동작은 이 인터페이스 구현체가 담당한다.

구현체  설명
DataSourceTransactionManager JDBC용
JpaTransactionManager JPA + Hibernate용
ChainedTransactionManager 다중 DB 트랜잭션 처리용

→ Spring은 설정된 DataSource 또는 EntityManager에 따라 적절한 트랜잭션 매니저를 자동 등록한다.

 

2.5. 메서드 간 트랜잭션 전파 유의사항

@Transactional프록시 객체를 통해 트랜잭션을 적용한다.

따라서 내부 메서드 호출 시 트랜잭션이 적용되지 않을 수 있다.

@Service
public class AccountService {

    @Transactional
    public void outer() {
        inner(); // 이건 프록시를 거치지 않음 → 트랜잭션 적용 X
    }

    @Transactional
    public void inner() {
        // 실제 트랜잭션이 필요한 로직
    }
}

 

왜 트랜잭션이 적용되지 않는가?

스프링의 @Transactional이 프록시 객체를 통해 동작하기 때문

  1. 외부에서의 호출은 프록시를 거친다.
  2. 다른 클래스(예: 컨트롤러)에서 accountService.outer()를 호출하면, 실제 AccountService 객체가 아닌 그것을 감싸고 있는 프록시 객체가 요청을 받는다. 프록시는 outer() 메서드에 @Transactional이 있으므로, 트랜잭션을 시작한 후 실제 객체의 outer()를 호출한다.
  3. 내부에서의 호출은 프록시를 거치지 않는다.따라서 프록시는 inner()가 호출되었다는 사실조차 알지 못하므로, inner()에 선언된 @Transactional의 설정을 적용할 기회가 없다.
  4. 문제는 outer() 메서드 안에서 inner()를 호출하는 부분이다. 이 호출은 프록시를 통하는 것이 아니라, 실제 객체(target) 내부에서 자기 자신의 다른 메서드를 직접 호출(this.inner())하는 것이다. 이를 내부 호출(self-invocation)이라고 한다.

inner() 메서드의 로직은 outer()가 시작한 트랜잭션에 포함되어 실행되지만, inner() 메서드 자체에 설정된 트랜잭션 옵션(예: propagation = REQUIRES_NEW)은 무시된다.

 

해결 방법

inner()다른 클래스에 분리하거나, self-invocation 회피 로직 적용 필요

  1. 빈(Bean) 분리
    : 트랜잭션이 필요한 메서드를 별도의 클래스로 분리하고, 그 클래스를 주입받아 사용하는 방식
  2. 자기 자신을 주입받아 호출
    : AccountService가 자기 자신을 주입받아서 프록시를 통해 호출하도록 만드는 방법

 

3. 트랜잭션 전파(Propagation) 속성

3.1. Propagation이란?

전파 속성(Propagation)은 하나의 트랜잭션 안에서 또 다른 트랜잭션이 호출될 때,
"기존 트랜잭션을 따를지, 새로 만들지, 아예 트랜잭션 없이 실행할지"를 결정하는 설정이다.

("기존 트랜잭션에 얹혀갈 건가? 새로 독립적으로 할 건가? 아니면 얽히지 말 건가?")

 

3.2. 주요 전파 속성

Propagation.REQUIRED (기본값)

  • 기존 트랜잭션이 있으면 참여하고, 없으면 새로 시작
  • 대부분의 서비스 로직 (@Transactional을 생략하지 않았다면 대부분 이거)
@Transactional // A
public void outer() {
    inner(); // B
}

@Transactional(propagation = REQUIRED) // 기본값
public void inner() {
    ...
}
  • A, B 모두 하나의 트랜잭션 안에서 실행됨
  • B에서 예외가 나면 → A도 같이 rollback됨

 

Propagation.REQUIRES_NEW

  • 무조건 새로운 트랜잭션을 생성, 기존 트랜잭션은 중단(보류)
  • 실패해도 본 트랜잭션에 영향 주지 않아야 할 로직 (ex. 알림, 로그 기록)
@Transactional // A
public void outer() {
    inner(); // B
}

@Transactional(propagation = REQUIRES_NEW)
public void inner() {
    ...
    throw new RuntimeException(); // rollback
}
  • A 트랜잭션은 B와 별개로 유지됨
  • B가 예외로 rollback돼도 A는 commit 가능
  • 단, 실제로는 DB 커넥션이 하나뿐이면 보류 동작이 제대로 안될 수 있음 (주의)

 

Propagation.NESTED

  • 기존 트랜잭션 내에서 저장점(Savepoint)을 만들어 분리된 롤백 처리 가능
  • 부분 롤백 (예: 전체 실패는 아니고, 일부 작업만 rollback 하고 싶을 때)
@Transactional // A
public void outer() {
    inner(); // B
}

@Transactional(propagation = NESTED)
public void inner() {
    ...
    throw new RuntimeException();
}
  • A 트랜잭션은 유지되며, B 내부의 변경만 rollback됨
  • 저장점(savepoint)을 사용하므로 JDBC 드라이버의 지원이 필요
  • 주의: REQUIRES_NEW와 달리, 새 트랜잭션이 아니라 서브 트랜잭션이라는 차이 존재

 

기타 전파 속성

전파 속성 설명
SUPPORTS 트랜잭션 있으면 참여, 없으면 트랜잭션 없이 실행
NOT_SUPPORTED 트랜잭션 없이 실행 (기존 트랜잭션은 중단됨)
MANDATORY 반드시 기존 트랜잭션이 있어야 함 (없으면 예외)
NEVER 트랜잭션이 있으면 예외 발생, 무조건 비트랜잭션 실행

이 속성들은 일반적인 서비스 로직에서는 드물게 쓰이지만,
알림 발송, 로그 기록, 조회 전용 API 등에 비트랜잭션 실행 목적으로 사용될 수 있음

 

실무에서 전파 속성 전택 기준

상황 추천 전파 속성 이유
핵심 로직 (회원가입, 결제 등) REQUIRED 기본 트랜잭션으로 전체 묶어서 처리
실패해도 본 로직은 계속돼야 할 때 REQUIRES_NEW 독립 커밋 or 롤백
부분적으로만 rollback 하고 싶을 때 NESTED savepoint 롤백 가능
단순 조회, 캐시 갱신 등 NOT_SUPPORTED 또는 readOnly 트랜잭션 오버헤드 제거

 

반응형

댓글