
이번 포스팅에서는 Spring으로 구현하다 보면 자주 사용하는 @Transactional 어노테이션에 대해서 기술하려고 합니다.
우선 데이터베이스 트랜잭션의 기본적인 개념부터 알고 넘어가야 합니다. 해당 내용은 다음의 링크에서 참조해 주세요.
데이터베이스 트랜잭션(Database Transaction)
@Transactional
@Transactional은 스프링 프레임워크에서 선언적으로 트랜잭션을 관리하기 위한 어노테이션으로, 메서드나 클래스 레벨에 적용하여 해당 작업의 성공 여부에 따라 자동으로 데이터베이스 트랜잭션을 시작하고 커밋 또는 롤백하는 방식으로 작동합니다.
트랜잭션 관련 코드를 직접 작성하는 번거로움 없이 데이터의 일관성과 안정성을 확보하며 비즈니스 로직에 집중할 수 있습니다.
다만, 무분별한 @Transactional 어노테이션을 사용하게 되면 성능 저하, 트랜잭션 관리 복잡성 증가, 데이터베이스 부하 증대 등 여러 부작용을 초래할 수 있기 때문에 잘 알고 사용해야 합니다.
JDBC API 트랜잭션
JDBC API를 사용해서 직접 트랜잭션을 생성하려면 다음의 코드와 같습니다.
import java.sql.Connection;
public void transaction() throws SQLException {
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
// 비즈니스 로직 처리
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.setAutoCommit(true);
conn.close();
}
}
위와 같이 구현하면 데이터 액세스 기술에 의존적인 코드를 반복적으로 작성해야 되고 비즈니스 로직과는 다른 관심사의 일을 함께 수행하게 됩니다. 코드가 길어지면서 가독성도 떨어지고 하나의 메서드에서 여러 일을 하기 때문에 기능의 분석도 오래 걸릴 수 있습니다.
위의 코드를 Spring의 @Transactional 어노테이션을 사용하면 비즈니스 로직과는 별개로 작성했던 코드들을 제거할 수 있습니다.
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void transaction() {
// 비즈니스 로직 처리
}
@Transactional 속성
| 속성 | 설명 | 기본값 |
| propagation | 트랜잭션이 이미 있을 때 다른 트랜잭션이 어떻게 참여할지 지정 | Propagation.REQUIRED |
| isolation | 트랜잭션이 동시에 실행될 때 데이터 정합성을 어떻게 보장할지 지정 | Isolation.DEFAULT |
| readOnly | 트랜잭션이 읽기 전용인지 여부 | false |
| rollbackFor | 어떤 예외가 발생했을 때 롤백할지 지정 | RuntimeException만 기본 롤백 |
| noRollbackFor | 특정 예외는 롤백 하지 않도록 지정 | 없음 |
| timeout | 트랜잭션이 지정 시간에 끝나지 않으면 rollback | 데이터베이스 설정 따름 |
Propagation 속성
여러 트랜잭션이 있는 경우에 트랜잭션 전파 방식을 결정합니다.
1. Propagation.REQUIRED
이미 트랜잭션이 있으면 참여, 없으면 새 트랜잭션을 생성해 실행합니다. propagation 설정의 기본값입니다.
2. Propagation.REQUIRED_NEW
항상 새로운 트랜잭션 생성, 기존(외부) 트랜잭션이 있다면 끝날 때까지 일시중지합니다.
3. Propagation.NESTED
기존 트랜잭션 안에 중첩 트랜잭션 생성, 없으면 새 트랜잭션을 생성합니다.
4. Propagation.SUPPORTS
존재하는 트랜잭션이 있으면 참여, 없으면 트랜잭션이 없이 메서드만 실행합니다.
5. Propagation.NOT_SUPPORTED
트랜잭션이 있으면 중단시키고, 새 트랜잭션 없이 실행합니다.
6. Propagation.MANDATORY
무조건 트랜잭션이 있어야만 실행이 되고 없으면 예외가 발생합니다.
7. Propagation.NEVER
트랜잭션이 존재하면 예외가 발생합니다.
Isolation 속성
동시에 여러 트랜잭션이 실행될 때 트랜잭션 격리 수준을 지정하고, 따로 설정하지 않을 경우 기본값으로 데이터베이스의 격리 수준 레벨로 적용됩니다.
1. Isolation.DEFAULT
연결된 데이터베이스의 기본 설정에 따릅니다.
2. Isolation.READ_UNCOMMITTED
가장 낮은 레벨의 격리 수준으로 다른 트랜잭션에서 커밋 안 된 데이터도 읽을 수 있습니다.
즉, Dirty Read 문제가 발생합니다.
3. Isolation.READ_COMMITTED
READ_COMMITTED는 READ_UNCOMMITTED에서 발생할 수 있는 Dirty Read 문제를 방지하며, 커밋된 데이터만 읽어옵니다.
하지만 트랜잭션이 동시에 실행 중일 때, 같은 데이터를 다시 읽으면 값이 바뀌는 경우가 발생할 수 있습니다.
즉, Non-repeatable Read 문제가 발생합니다. 이는 다른 트랜잭션이 중간에 해당 데이터를 수정하거나 삭제했기 때문입니다.
4. Isolation.REPEATABLE_READ
REPEATABLE_READ는 READ_COMMITTED에서 발생할 수 있는 Non-repeatable Read 문제를 방지하며, 트랜잭션 내에서 같은 SELECT 쿼리를 여러 번 실행해도 항상 동일한 결과를 보장합니다.
그러나 REPEATABLE_READ에서도 Phantom Read 문제가 발생할 수 있습니다. 예를 들어, 같은 조건으로 반복해서 데이터를 조회했을 때, 다른 트랜잭션이 새로운 레코드를 추가하면 결과가 달라질 수 있습니다.
5. Isolation.SERIALIZABLE
SERIALIZABLE는 REPEATABLE_READ에서 발생할 수 있는 Phantom Read 문제를 방지하며, 가장 엄격한 격리 수준으로 트랜잭션을 순차적으로 실행합니다.
@Transactional 예제
@Transactional 선언하지 않았을 때
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final AuditService auditService;
public void join(RequestJoin requestJoin) throws InterruptedException {
log.info("UserService.join requestJoin: {}", requestJoin);
User user = User.builder()
.name(requestJoin.name())
.email(requestJoin.email())
.build();
userRepository.save(user);
auditService.logUserCreation(user);
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class AuditService {
private final AuditRepository auditRepository;
public void logUserCreation(User user) {
log.info("AuditService.logUserCreation user: {}", user);
AuditLog auditLog = AuditLog.builder()
.name(user.getName())
.build();
auditRepository.save(auditLog);
}
}
2025-09-17T11:37:50.925+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2025-09-17T11:37:51.435+09:00 INFO 32411 --- [demo] [nio-8080-exec-3] c.e.demo.controller.UserController : UserController.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T11:37:51.437+09:00 INFO 32411 --- [demo] [nio-8080-exec-3] com.example.demo.service.UserService : UserService.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T11:37:51.446+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(17429454<open>)] for JPA transaction
2025-09-17T11:37:51.447+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T11:37:51.451+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@20cc44ad]
2025-09-17T11:37:51.452+09:00 TRACE 32411 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:37:51.534+09:00 TRACE 32411 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:37:51.535+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-17T11:37:51.540+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(17429454<open>)]
2025-09-17T11:37:51.557+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2025-09-17T11:37:51.558+09:00 INFO 32411 --- [demo] [nio-8080-exec-3] com.example.demo.service.AuditService : AuditService.logUserCreation user: com.example.demo.domain.User@35a87f2d
2025-09-17T11:37:51.562+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(17429454<open>)] for JPA transaction
2025-09-17T11:37:51.563+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T11:37:51.563+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@712fd6a0]
2025-09-17T11:37:51.563+09:00 TRACE 32411 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:37:51.564+09:00 TRACE 32411 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:37:51.565+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-17T11:37:51.568+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(17429454<open>)]
2025-09-17T11:37:51.569+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2025-09-17T11:37:51.648+09:00 DEBUG 32411 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
총 2번의 "Creating new transaction" 트랜잭션이 생성되는 것을 볼 수 있습니다. 각각의 트랜잭션이 생성되어 처리됩니다.
join() 메서드에서 save()를 처리 및 커밋 완료 후 트랜잭션을 종료하고 logUserCreation() 메서드에서 save()를 처리 및 커밋 완료 후 트랜잭션을 종료합니다.
로그에 대한 내용을 좀 더 자세하게 살펴보겠습니다.
Found thread-bound EntityManager [SessionImpl(17429454<open>)] for JPA transaction
- 현재 스레드에 이미 바인딩된 EntityManager가 존재한다는 의미.
- Spring이 관리하는 EntityManager이며, 이미 스레드 로컬에 존재하고 있어 재사용.
- OpenEntityManagerInViewInterceptor에 의해 바인딩.
Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- @Transactional이 선언된 SimpleJpaRepository.save() 메서드가 호출되면서 새로운 트랜잭션이 시작.
- PROPAGATION_REQUIRED: 기존 트랜잭션이 있다면 참여, 없다면 새로 생성.
- ISOLATION_DEFAULT: 기본 격리 수준 사용 (DB 설정에 따라 다름).
Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
- save() 호출 시 TransactionInterceptor가 트랜잭션 정보를 다시 확인.
Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
- save() 메서드 실행 완료.
Initiating transaction commit
- 트랜잭션 커밋 시작.
- 여기서 실제 DB에 반영되는 단계.
Committing JPA transaction on EntityManager [SessionImpl(17429454<open>)]
- JPA 트랜잭션 커밋 실행.
- 영속성 컨텍스트에 있는 변경 내용이 플러시(flush)되고 DB에 커밋.
Not closing pre-bound JPA EntityManager after transaction
- 트랜잭션은 끝났지만 EntityManager는 닫지 않음.
- OpenEntityManagerInViewInterceptor가 바인딩한 것이므로 요청이 완전히 끝날 때까지 유지.
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
- HTTP 요청 응답 완료 직전에 EntityManager를 정리.
- View 렌더링 후 또는 JSON 응답 후 정리되는 시점.
@Transactional을 선언하지 않았는데 어떻게 트랜잭션이 생성돼서 실행되는 걸까요? 이에 대한 내용은 다음과 같습니다.
Spring Data JPA에서 JpaRepository를 상속하면, 실제로는 Spring 내부에서 SimpleJpaRepository라는 클래스를 통해 구현체를 자동으로 만들어 줍니다. 이 클래스가 트랜잭션 처리를 이미 해놓았기 때문에 우리가 따로 @Transactional을 선언하지 않아도 트랜잭션이 동작하는 것입니다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
...
}
SimpleJpaRepository 클래스에는 @Transactional(readOnly = true)으로 선언되어 읽기 작업을 기본으로 하고, save()과 saveAll() 메서드에는 별도로 @Transactional이 선언되어 있어 쓰기 작업에 대해 트랜잭션이 활성화됩니다.
위의 로그에서 "Creating new transaction with name [o.s.d.j.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT"가 SimpleJpaRepository.save() 메서드 실행 시 트랜잭션이 생성되었음을 의미하며, 트랜잭션의 이름으로 메서드명이 찍힙니다.
@Transactional 선언
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final AuditService auditService;
@Transactional
public void join(RequestJoin requestJoin) throws InterruptedException {
log.info("UserService.join requestJoin: {}", requestJoin);
User user = User.builder()
.name(requestJoin.name())
.email(requestJoin.email())
.build();
userRepository.save(user);
auditService.logUserCreation(user);
}
}
코드는 이전과 동일하지만, UserService.join() 메서드에만 @Transactional을 선언합니다.
2025-09-22T16:48:12.536+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2025-09-22T16:48:12.866+09:00 INFO 43893 --- [demo] [nio-8080-exec-3] c.e.demo.controller.UserController : UserController.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-22T16:48:12.883+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1368937090<open>)] for JPA transaction
2025-09-22T16:48:12.884+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.example.demo.service.UserService.join]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-22T16:48:12.896+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7433621b]
2025-09-22T16:48:12.904+09:00 TRACE 43893 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.demo.service.UserService.join]
2025-09-22T16:48:12.907+09:00 INFO 43893 --- [demo] [nio-8080-exec-3] com.example.demo.service.UserService : UserService.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-22T16:48:12.908+09:00 INFO 43893 --- [demo] [nio-8080-exec-3] com.example.demo.service.AuditService : AuditService.logUserCreation user: com.example.demo.domain.User@513fb930
2025-09-22T16:48:12.919+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1368937090<open>)] for JPA transaction
2025-09-22T16:48:12.919+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-22T16:48:12.924+09:00 TRACE 43893 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-22T16:48:12.995+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] org.hibernate.SQL : insert into audit_log (name,id) values (?,default)
2025-09-22T16:48:13.004+09:00 TRACE 43893 --- [demo] [nio-8080-exec-3] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [a]
2025-09-22T16:48:13.176+09:00 TRACE 43893 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-22T16:48:13.178+09:00 TRACE 43893 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.demo.service.UserService.join]
2025-09-22T16:48:13.179+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-22T16:48:13.183+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1368937090<open>)]
2025-09-22T16:48:13.208+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2025-09-22T16:48:13.271+09:00 DEBUG 43893 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
@Transactional을 선언하지 않았을 때와 달리 "Creating new transactional" 트랜잭션이 한 번만 생성되는 것을 볼 수 있습니다.
즉, AuditService.logUserCreation() 메서드에서 save()는 UserService.join()에서 생성된 트랜잭션에 참여하게 된 것으로 하나의 트랜잭션에서 실행됩니다.
Participating in existing transaction
- AuditSerivce.save()는 이미 존재하는 트랜잭션에 참여.
- 새로운 트랜잭션을 만들지 않고 상위 트랜잭션(join())에 포함.
@Transactional(propagation = Propagation.REQUIRES_NEW) 선언
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final AuditService auditService;
@Transactional
public void join(RequestJoin requestJoin) throws InterruptedException {
log.info("UserService.join requestJoin: {}", requestJoin);
User user = User.builder()
.name(requestJoin.name())
.email(requestJoin.email())
.build();
userRepository.save(user);
auditService.logUserCreation(user);
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class AuditService {
private final AuditRepository auditRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logUserCreation(User user) {
log.info("AuditService.logUserCreation user: {}", user);
AuditLog auditLog = AuditLog.builder()
.name(user.getName())
.build();
auditRepository.save(auditLog);
}
}
이번에는 "Propagation.REQUIRES_NEW" 전파 속성으로 이미 존재하는 트랜잭션을 참여하지 않고 새 트랜잭션을 생성하도록 하겠습니다.
2025-09-17T11:31:41.128+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2025-09-17T11:31:41.318+09:00 INFO 26279 --- [demo] [nio-8080-exec-3] c.e.demo.controller.UserController : UserController.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T11:31:41.324+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1286678171<open>)] for JPA transaction
2025-09-17T11:31:41.325+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.example.demo.service.UserService.join]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T11:31:41.329+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@77503e07]
2025-09-17T11:31:41.329+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.demo.service.UserService.join]
2025-09-17T11:31:41.330+09:00 INFO 26279 --- [demo] [nio-8080-exec-3] com.example.demo.service.UserService : UserService.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T11:31:41.332+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1286678171<open>)] for JPA transaction
2025-09-17T11:31:41.333+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T11:31:41.333+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:31:41.394+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:31:41.394+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1286678171<open>)] for JPA transaction
2025-09-17T11:31:41.395+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Suspending current transaction, creating new transaction with name [com.example.demo.service.AuditService.logUserCreation]
2025-09-17T11:31:41.399+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(2128098990<open>)] for JPA transaction
2025-09-17T11:31:41.399+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@62d4beec]
2025-09-17T11:31:41.399+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.demo.service.AuditService.logUserCreation]
2025-09-17T11:31:41.399+09:00 INFO 26279 --- [demo] [nio-8080-exec-3] com.example.demo.service.AuditService : AuditService.logUserCreation user: com.example.demo.domain.User@50bc138a
2025-09-17T11:31:41.400+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2128098990<open>)] for JPA transaction
2025-09-17T11:31:41.400+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T11:31:41.400+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:31:41.412+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T11:31:41.422+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.demo.service.AuditService.logUserCreation]
2025-09-17T11:31:41.423+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-17T11:31:41.423+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(2128098990<open>)]
2025-09-17T11:31:41.462+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(2128098990<open>)] after transaction
2025-09-17T11:31:41.463+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Resuming suspended transaction after completion of inner transaction
2025-09-17T11:31:41.463+09:00 TRACE 26279 --- [demo] [nio-8080-exec-3] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.demo.service.UserService.join]
2025-09-17T11:31:41.463+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-17T11:31:41.463+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1286678171<open>)]
2025-09-17T11:31:41.464+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2025-09-17T11:31:41.503+09:00 DEBUG 26279 --- [demo] [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
"Propagation.REQUIRES"기본값이랑 큰 차이가 없습니다. 다른 부분만 살펴보도록 하겠습니다.
Suspending current transaction, creating new transaction with name [com.example.demo.service.AuditService.logUserCreation]
- 현재 트랜잭션을 일시 중지하고, 새로운 트랜잭션을 생성.
- 해당 트랜잭션은 AuditService.logUserCreation() 메서드에서 생성.
Resuming suspended transaction after completion of inner transaction
- AuditService.logUserCreation() 메서드에서 생성한 내부 트랜잭션이 완료된 후 중단했던 외부 트랜잭션을 다시 시작.
"Propagation.REQUIRES_NEW" 전파 속성은 다음과 같은 예시에서 사용할 수 있습니다.
영화를 예매하면 데이터베이스에 저장하고, 해당 기록을 따로 데이터베이스에 저장하는 영화 예매 서비스가 있다고 가정하겠습니다.
두 개의 로직을 하나의 트랜잭션에서 처리하면, 영화 예매는 성공했는데 기록을 저장할 때 예외가 발생하여 롤백이 된다면 두 개의 로직 모두 롤백이 되어 성공한 영화 예매도 취소가 됩니다. 그러면 사용자 입장에서는 납득이 되지 않는 상황이 발생하여 좋지 못한 사용자 경험을 남겨주게 됩니다. 이러한 문제를 "Propagation.REQUIRES_NEW" 속성으로 트랜잭션을 분리한다면 기록을 저장할 때 실패해 롤백된다고 해도 영화 예매는 성공적으로 처리되어 사용자에게 영화 예매 완료를 전달할 수 있게 됩니다. 대신에 기록 저장 실패는 개발자가 다른 방안으로 처리를 해야겠죠.
@Transactional(rollback = class)
특정 예외 발생 시 트랜잭션을 롤백할 수 있도록 설정이 가능합니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transacational(rollbackFor = Exception.class)
public void createUser(String name, String email) throws Exception {
User user = new User(name, email);
userRepository.save(user);
if (email.contains("error")) {
throw new Exception("Invalid email format!");
}
}
}
rollbackFor = 클래스명.class를 설정하면 해당 예외가 발생할 경우 롤백됩니다.
트랜잭션 롤백을 활용하면 "데이터 정합성을 유지하며, 비정상적인 데이터 저장을 방지 가능합니다."
또한 트랜잭션은 RuntimeException과 Error에서는 롤백되지만, Checked Exception에서는 롤백되지 않기 때문에 rollbackFor 속성으로 롤백 처리가 되도록 할 수 있습니다.
@Transactional 선언이 무시되는 경우
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final AuditRepository auditRepository;
@Transactional
public void join(RequestJoin requestJoin) {
log.info("UserService.join requestJoin: {}", requestJoin);
User user = User.builder()
.name(requestJoin.name())
.email(requestJoin.email())
.build();
log.info("UserService.join user count: {}", userRepository.count());
userRepository.save(user);
persistAuditLog(user);
notiftyAuditLog(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
protected long persistAuditLog(User user) {
log.info("UserService.persistAuditLog user: {}", user);
auditRepository.save(
AuditLog.builder()
.name(user.getName())
.build()
);
return auditRepository.count();
}
private void notiftyAuditLog(User user) {
log.info("UserService.notiftyAuditLog user: {}", user);
}
}
2025-09-17T13:58:40.441+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2025-09-17T13:58:40.976+09:00 INFO 107739 --- [demo] [nio-8080-exec-1] c.e.demo.controller.UserController : UserController.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T13:58:41.006+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2109536066<open>)] for JPA transaction
2025-09-17T13:58:41.021+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.example.demo.service.UserService.join]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T13:58:41.027+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@22441983]
2025-09-17T13:58:41.030+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.demo.service.UserService.join]
2025-09-17T13:58:41.034+09:00 INFO 107739 --- [demo] [nio-8080-exec-1] com.example.demo.service.UserService : UserService.join requestJoin: RequestJoin[name=a, email=a@a.com]
2025-09-17T13:58:41.104+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2109536066<open>)] for JPA transaction
2025-09-17T13:58:41.105+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T13:58:41.105+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.count]
2025-09-17T13:58:42.394+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.count]
2025-09-17T13:58:42.395+09:00 INFO 107739 --- [demo] [nio-8080-exec-1] com.example.demo.service.UserService : UserService.join user count: 0
2025-09-17T13:58:42.396+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2109536066<open>)] for JPA transaction
2025-09-17T13:58:42.397+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T13:58:42.397+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T13:58:42.488+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T13:58:42.488+09:00 INFO 107739 --- [demo] [nio-8080-exec-1] com.example.demo.service.UserService : UserService.persistAuditLog user: com.example.demo.domain.User@1be5dc94
2025-09-17T13:58:42.489+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2109536066<open>)] for JPA transaction
2025-09-17T13:58:42.489+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T13:58:42.489+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T13:58:42.493+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2025-09-17T13:58:42.494+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2109536066<open>)] for JPA transaction
2025-09-17T13:58:42.494+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2025-09-17T13:58:42.494+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.count]
2025-09-17T13:58:42.512+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.count]
2025-09-17T13:58:42.513+09:00 INFO 107739 --- [demo] [nio-8080-exec-1] com.example.demo.service.UserService : UserService.notiftyAuditLog user: com.example.demo.domain.User@1be5dc94
2025-09-17T13:58:42.514+09:00 TRACE 107739 --- [demo] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.demo.service.UserService.join]
2025-09-17T13:58:42.514+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2025-09-17T13:58:42.519+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(2109536066<open>)]
2025-09-17T13:58:42.524+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2025-09-17T13:58:42.538+09:00 DEBUG 107739 --- [demo] [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
UserService.persistAuditLog() 메서드에 Propagation.REQUIRES_NEW인 @Transactional 선언을 했지만, 로그에는 "Suspending current transaction, creating new transaction with name" 로 새 트랜잭션을 만드는 로그가 없고, 하나의 트랜잭션에서 전부 처리가 됩니다.
즉, @Transactional 선언이 무시가 됐습니다. 그 이유는 Spring의 트랜잭션 처리 방식은 프록시(proxy) 기반 AOP 방식이기 때문입니다.
프록시는 클래스 밖에서 메서드를 호출할 때만 개입할 수 있기 때문에, 일반적으로 외부에서 호출 가능한 public 메서드에 붙이는 것이 권장됩니다.
Spring은 @Transactional, @Async, @Cacheable 같은 AOP 기능을 프록시 객체(proxy object)로 감싸서 구현합니다.
즉, 진짜 객체가 아닌, 프록시를 통해 메서드를 실행할 때만 AOP가 작동합니다.
스프링 공식 문서에 다음과 같이 나와있습니다.
The @Transactional annotation is typically used on methods with public visibility. As of 6.0, protected or package-visible methods can also be made transactional for class-based proxies by default. Note that transactional methods in interface-based proxies must always be public and defined in the proxied interface. For both kinds of proxies, only external method calls coming in through the proxy are intercepted.
@Transactional 어노테이션은 일반적으로 public 메서드에 붙여서 사용합니다.
@Transactional
public void doSomething() {
// 트랜잭션이 정상적으로 적용됨
}
이게 가장 흔하고 권장되는 방식입니다.
@Transactional
protected void doSomethingProtected() {
// Spring 6.0 이상에서는 class-based proxy면 적용됨
}
Spring 6.0부터는 protected나 package-private(default) 메서드도, 클래스 기반 프록시(class-based proxy, CGLIB)를 사용하는 경우에는 기본적으로 트랜잭션 적용이 가능합니다.
하지만, 인터페이스 기반 프록시(interface-based proxy, JDK 동적 프록시)를 사용하는 경우에는 무조건 public이어야 트랜잭션이 적용됩니다.
그래서 다음과 같은 코드의 경우 문제가 됩니다.
@Transactional
public void methodA() {
methodB(); // ❌ 내부 호출: 트랜잭션 적용 안 됨
}
@Transactional
public void methodB() {
// 트랜잭션 기대하지만 실제로는 적용되지 않음
}
methodA()는 외부에서 호출되므로 프록시를 통해 트랜잭션이 적용됩니다.
그러나 methodA() 안에서 this.methodB() 식으로 자기 메서드를 호출하면, 프록시를 거치지 않고 직접 호출하기 때문에 트랜잭션이 적용되지 않습니다.
프록시 동작 흐름
프록시 객체는 실제 객체를 감싸고 있다가 외부에서 메서드 호출이 들어올 때, 중간에 낚아채서 트랜잭션 시작 처리를 하고 본체 메서드를 실행한 뒤 트랜잭션 커밋 or 롤백 후 다시 빠져나옵니다.
✅ AOP 적용 잘 되는 경우 (외부 호출)

❌ AOP 적용 안 되는 경우 (내부 호출)

- join()은 프록시에서 호출.
- persistAuditLog()는 같은 클래스에서 직접 호출했기 때문에 프록시가 가로채지 못함.
- 새 트랜잭션(REQUIRES_NEW)이 무시되고, 기존 트랜잭션에 그냥 참여.
🔗 Reference
'spring' 카테고리의 다른 글
| 애플리케이션 성능 개선 Part 2 (feat. 트랜잭션) (0) | 2025.10.11 |
|---|---|
| 애플리케이션 성능 개선 Part 1 (feat. 병목 현상, N+1, 인덱스) (0) | 2025.08.21 |
| Spring REST Docs API 문서화 (0) | 2025.05.09 |
| [Spring] 인터셉터(Interceptor) 적용 (0) | 2022.04.17 |
| JPA @MappedSuperclass (0) | 2022.03.07 |