코드몽키
@Transactional에 대한 고찰 본문
최근 레거시 코드를 리펙토링 하면서 Transactional 어노테이션의 개념에 대해서 두루뭉실하게 인지하고 무분별한 남발을 하고 있는것 같아 트랜잭션 어노테이션에 대해서 좀더 깊은 개념과 동작원리에 대해서 공부하고 정리해보았습니다.
Transaction이란 하나의 논리적 작업 단위를 뜻합니다. 여러개의 개별적인 연산들이 마치 하나의 묶음처럼 처리되어야 하는 것이 트랜잭션의 핵심이다. DBMS(Oracle, PostgreSQL, MySQL 등) 에서는 트랜잭션을 ACID 원칙을 통하여 트랜잭션을 보장합니다.
https://ko.wikipedia.org/wiki/ACID
ACID - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 다른 뜻에 대해서는 애시드 문서를 참고하십시오. ACID(원자성, 일관성, 독립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한
ko.wikipedia.org
@Transcational 의 개념
Spring Framework 2.0 이상의 버전에서 @Transactional은 선언적 데이터베이스 트랜잭션 관리를 제공한다. AOP 기반의 프록시를 통해 매서드레벨 혹은 클래스 레벨에서 Tx 시작, 커밋, 롤백을 자동으로 제어합니다. 따라서 개발자는 @Transactional만 선언하면 세부 구현(트랜잭션 시작/종료)를 신경 쓰지 않아도 됩니다. 즉, @Transactional은 DBMS가 제공하는 Tx 기능을 스프링이 추상화하여 애플리케이션 코드 레벨에서 선언적으로 트랜잭션을 관리할 수 있도록 해주는 어노테이션이라고 정의 할 수 있습니다.
@Transactional 구동 방식
Spring boot 애플리케이션 구동 시 spring-boot-autoconfigure 모듈에 있는 TransactionAutoConfiguration을 통해 애플리케이션 시작 시점에 조건(JDBC, JMS,JPA 등)에 따라 PlatformTransactionManager 빈을 등록합니다. 이 후 트랜잭션 매니저 (DataSourceTransactionManager 등) 빈으로 등록됨으로써 TransactionManager가 선정되면 애플리케이션에서 Tx의 컨트롤을 가능하게 합니다.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}
위와같이 jdbc나 jpa 의존성을 주입하게 되면 애플리케이션 구동 시에 @EnableTransactionManagement가 활성화 되어 @Transcational을 선언한 클래스나 매서드를 Tx AOP 프록시 객체로 만들 수 있습니다.
애플리케이션 구동 시점에 IoC 컨테이너가 @Transcational이 적용된 클래스의 빈을 생성할 때, 해당 빈이 프록시 대상인지 판단합니다. 이때 스프링은 ProxyFactoryBean 등을 활용하여 실제 객체를 감싸는 프록시 객체를 동적으로 생성하며, 이후 IoC 컨테이너에 등록되는 빈은 실제 객체가 아닌 프록시 객체가 됩니다.
클라이언트에서 요청이 오게 되면 IoC 컨테이너에 등록된 프록시 객체의 메서드가 호출되고 프록시는 @Transactional이 붙어있는지 확인하고, 메서드 실행 전에 트랜잭션을 시작합니다.

@Transcational 속성
다양한 속성을 통해 트랜잭션의 동작 방식을 세밀하게 제어할 수 있습니다. 주요 속성들을 정리하면 다음과 같습니다.
1. propagation
propagation은 여러 메서드가 호출될 때, 트랜잭션을 공유할지 아니면 독립적으로 새로운 트랜잭션을 시작할지 정의하는 규칙입니다.
| 명칭 | 설명 |
| REQUIRED | 트랜잭션이 있는 경우 참여하고 없으면 새 트랜잭션을 생성하며 propagation 설정이 없는 경우의 기본값 |
| REQUIRES_NEW | 항상 새 트랜잭션을 만들고 트랜잭션이 있다면 끝날 때까지 일시중지 |
| NESTED | 기존 트랜잭션과 중첩된 트랜잭션을 생성하고, 없다면 새로 트랜잭션을 생성 |
| SUPPORTS | 존재하는 트랜잭션이 있다면 지원하고, 없으면 트랜잭션 없이 메서드만 실행 |
| MANDATORY | 반드시 트랜잭션이 존재해야 하는 유형으로 없으면 예외(ThrowIllegalTransactionStateException)가 발생 |
| NOT_SUPPORTED | 트랜잭션이 있어도 중단되며, 트랜잭션을 지원하지 않는다 |
| NEVER | 트랜잭션이 존재하면 예외(ThrowIllegalTransactionStateException)가 발생 |
사용 예)
@Service
class SimpleService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun simpleTest(message: String) {
//...
}
}
2. isolation
여러 트랜잭션이 동시에 실행될 때, 서로의 변경 내용을 얼마나 볼 수 있는지 결정하여 데이터의 일관성을 제어하는 설정입니다.
| 명칭 | 설명 |
| READ_UNCOMMITTED | 가장 낮은 격리 수준. 다른 트랜잭션에서 아직 커밋되지 않은 데이터(Dirty Read)도 읽을 수 있음. 성능은 좋지만 데이터 정합성이 가장 취약하다. |
| READ_COMMITTED | 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있음. 대부분의 DBMS(Oracle 등) 기본값. Dirty Read는 방지되지만, 같은 쿼리를 실행했을 때 결과가 달라질 수 있는 Non-repeatable Read 문제는 발생 가능 |
| REPEATABLE_READ | 같은 트랜잭션 안에서는 같은 조건으로 조회 시 항상 같은 결과를 보장. Dirty Resad, Non-repeatable Read는 방지 되지만, 새로운 레코드가 끼어드는 Phantom Read는 발생 가능 |
| SERIALIZABLE | 가장 높은 격리 수준. 트랜잭션이 직렬적으로 실행된 것처럼 동작. Dirty Read, Non-repeatable Read, Phantom Read 모두 방지 가능. 하지만 동시성이 크게 떨어지고 성능 저하가 심함 |
@Service
class SimpleService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
fun simpleTest(message: String) {
//...
}
}
3. readOnly
이 속성을 true로 설정하면 해당 트랜잭션을 읽기 전용으로 만듭니다. DB의 쓰기 작업을 허용하지 않으며, 이 속성을 사용하여 성능을 최적화 할 수 있습니다.
4. timeout
트랜잭션이 완료되기까지 허용하는 최대 시간(초 단위)을 설정합니다. 이 시간을 초과하면 트랜잭션은 롤백됩니다. 이는 트랜잭션이 오랫동안 잠금을 점유하여 시스템에 문제를 일으키는 것을 방지하는 데 유용합니다.
5. rollbackFor / rollbackForClassName
예외 발생 시 롤백을 수행할 예외 타입을 지정합니다. Spring의 기본 정책은 RuntimeException과 Error만 롤백을 수행하는데, 이 속성을 사용하면 특정 체크 예외(Checked Exception)도 롤백되도록 설정할 수 있습니다.
6. noRollbackFor / noRollbackForClassName
예외가 발생해도 롤백을 수행하지 않을 예외 타입을 지정합니다. 예를 들어, 특정 비즈니스 로직 예외는 트랜잭션 실패로 간주하지 않고 커밋해야 할 때 사용됩니다.
7. value / transactionManager
@Transactional의 value 또는 transactionManager 속성은 트랜잭션 매니저의 Bean 이름을 지정합니다. 하지만 여러 개의 트랜잭션 매니저(ex. DB가 2개 이상이거나, JPA + JMS 같은 복합 환경) 가 존재한다면, 어떤 매니저를 사용할지 명시해야 합니다. 이때 value에 Bean 이름을 지정합니다.
@Transactional(transactionManager = "paymentTransactionManager")
fun processPayment() {
// payment DB에 트랜잭션 적용
}
@Transcational 올바른 사용법?
마지막으로 @Transactional를 사용할때 유의 해야될 사항을 정리해 보겠습니다.
1. private 접근 제어자는 사용 불가
스프링의 기본 AOP 방식은 JDK 동적 프록시나 CGLIB 프록시는 클래스나 메서드에 접근하여 기능을 추가 합니다.
이 때 private 매서드는 프록시 객체가 호출 할 수 없기 때문에 트랜잭션이 적용이되지 않습니다. @Transcational을 적용하려면
반드시 public 매서드에 사용해야 합니다.
2. 예외 발생 시 롤백여부

스프링 트랜잭션은 기본적으로 UnCheckedException이 발생하면 rollback를 수행합니다.
반면 CheckedException(RuntimeException를 상속하지 않는 예외)가 발생하면 커밋을 수행합니다.
만약에 체크드 예외 발생 시에도 롤백을 원하면 @Transactional에 rollbackFor 속성을 추가하여 직접 설정해 줘야합니다.
@Transactional(rollbackFor = [MyCheckedException::class])
fun myMethod() {
// ...
throw MyCheckedException("checked 예외 발생")
}
3. 간단한 쿼리에서 성능 저하 가능성
@Transactional을 사용하면 트랜잭션을 시작하고 커밋하는 과정에서 추가적인 오버헤드가 발생합니다. 예를 들어, DB 커넥션 확보, 트랜잭션 동기화 등과 같은 작업이 필요합니다. 단순한 SELECT 쿼리와 같이 일기 전용 작업에선 트랜잭션이 반드시 필요하지 않을 수 있으며, 불필요하게 @Transcational를 사용하면 오히려 성능 저하를 야기 할 수 있습니다. 이런 경우 readOnly = true 속성을 사용하여 읽기 전용 트랜잭션으로 최적화 하거나, readOnly를 선언하는 것 보다 단순 조회 시 @Transactional 자체를 생략하는 것이 효율적이라고 합니다.
https://tech.kakaopay.com/post/jpa-transactional-bri/
JPA Transactional 잘 알고 쓰고 계신가요? | 카카오페이 기술 블로그
JPA Transactional과 그에 따른 DB 쿼리 성능과의 관계에 대해서 설명합니다.
tech.kakaopay.com
4. 서비스 계층에서 사용하자.
스프링 데이터 JPA의 기본 구현체인 SimpleJpaRepository 내의 메서드들(save, delete 등)에는 이미 @Transactional이 적용되어 있습니다. 이는 단일 리포지토리 메서드 호출에 대한 트랜잭션을 처리하기 위함입니다. 즉, save() 메서드가 호출되면 해당 작업에 대한 트랜잭션이 시작되고 완료됩니다.
예를 들어, 주문을 처리하는 OrderService가 orderRepository.save()와 paymentRepository.save()를 모두 호출해야 한다면, 각 리포지토리 메서드에 개별적으로 적용된 트랜잭션으로는 전체 비즈니스 로직을 하나의 성공/실패 단위로 보장할 수 없습니다.
@Service
class OrderService(
private val orderRepository: OrderRepository,
private val paymentRepository: PaymentRepository
) {
fun createOrder(order: Order, payment: Payment) {
//Transaction 1 시작 커밋
orderRepository.save(order)
//Transaction 2 시작 커밋
//여기서 롤백이 발생하더라도 Transaction 1 작업은 커밋이 됨으로 원자성에 위배
paymentRepository.save(payment)
}
}
내부적으로 이미 @Transactional이 적용되어 있더라고 상위의 호출에서 어노테이션이 선언되어 있다면, 상위의 호출의 트랜잭션이 우선순위를 가지게 되어있습니다.
@Service
class OrderService(
private val orderRepository: OrderRepository,
private val paymentRepository: PaymentRepository
) {
@Transactional
fun createOrder(order: Order, payment: Payment) {
orderRepository.save(order)
paymentRepository.save(payment)
}
}
결론적으로, 복잡한 비즈니스 로직을 다루는 서비스 계층에서는 트랜잭션의 원자적 처리를 보장하기 위해 서비스 레이어에서 @Transactional을 선언하는 것이 Spring 애플리케이션의 핵심 원칙인 책임 분리와 응집도(Cohesion)를 높히고 트랜잭션의 원자적 처리를 위한 권장되는 올바른 사용법입니다.
5. AOP 순서 충돌
스프링 AOP는 여러 어드바이스(ex: @Secured, @Cacheable 등)가 적용될때 우선순위(Order)에 따라 실행됩니다.
만약 @Secured과 @Transactional의 순서가 예상과 다르게 적용되면, 보안 검증이 트랜잭션 시작 전에 수행되지 않거나, 트랜잭션 롤백에 영향을 줄 수 있습니다. 결국 예상치 못한 에러가 발생 할 수 있습니다. 그래서 여러 AOP와 혼합하여 사용할 땐 반드시 Ordered 인터페이스나 @Order를 사용하여 순서를 명시적으로 지정해야 합니다.
6. 트랜잭션 범위 설정
트랜잭션을 설계할 때 실제 비즈니스 로직의 트랜잭션의 범위를 너무 넓게 잡으면 다음과 같은 문제가 발생 할 수 있습니다.
- 락(Lock) 경합
트랜잭션이 오래 유지되면 DB 리소스를 오래 점유합니다. 다른 트랜잭션이 동일 자원(행/테이블 등)에 접근하려고 할 때 대기하거나 충돌이 발생합니다. - 데드락(Deadlock) 가능성 증가
여러 트랜잭션이 서로가 가진 자원을 기다리며 무한 대기에 빠질 수 있습니다. DBMS는 보통 데드락 감지를 통해 한 쪽 트랜잭션을 강제로 롤백시키지만, 애플리케이션 입장에서는 실패가 발생하는 것이고 심할 경우 서비스 장애로 이어질 수 있습니다. - 불필요한 자원 점유
트랜잭션이 길게 유지되면 커넥션 풀, 세션, 버퍼 같은 리소스를 오래 점유하면 동시 처리량(throughput) 감소합니다.
따라서, 비즈니스 로직의 원자성(Atomicity)을 보장하는 최소한의 범위에서만 트랜잭션을 설정하는 것이 가장 중요하고 올바른 설계 원칙입니다.
7. 선언적 어노테이션 외의 트랜잭션 관리
스프링은 보통 @Transcational 같은 선언적 트랜잭션 관리 방식을 권장 하지만, 경우에 따라서 프로그래밍적으로 직접 구현하여 관리 할 수 있습니다. 좀 더 디테일하게 트랜잭션의 범위를 핸들링하여 성능을 최적화 할 경우, 서로 다른 이기종 DB를 동시에 제어해야 할 경우 가 있습니다. PlatformTransactionManager를 직접 사용하거나 TransactionTemplate을 통해 트랜잭션을 프로그래밍적으로 제어할 수 있습니다.
[Reference]
https://medium.com/gdgsongdo/transactional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-7b0105eb5ed6
@Transactional 바르게 알고 사용하기
@Transactional 그거 어떻게 동작하는 건가요?
medium.com
Using @Transactional :: Spring Framework
The @Transactional annotation is metadata that specifies that an interface, class, or method must have transactional semantics (for example, "start a brand new read-only transaction when this method is invoked, suspending any existing transaction"). The de
docs.spring.io
https://docs.spring.io/spring-data/jpa/reference/jpa/transactions.html
Transactionality :: Spring Data JPA
Example 3. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void delet
docs.spring.io