티스토리 뷰
데이터베이스와 상호 작용하는 애플리케이션을 개발하다 보면, 여러 테이블에 대한 작업을 하나의 트랜잭션에서 처리해야 할 때가 있습니다. 예를 들어, 온라인 쇼핑몰에서 주문을 처리할 때, 주문 정보를 저장하는 테이블과 동시에 주문 상세 정보를 저장하는 테이블에 데이터를 추가해야 합니다. 이런 경우에는 Spring JPA를 사용하여 두 테이블을 하나의 트랜잭션에서 처리할 수 있습니다.
이때, 어떻게 트랜잭션이 적용되는 것일까요?
예제코드
@Entity
public class OuterEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public OuterEntity() {
}
public OuterEntity(String name) {
this.name = name;
}
}
@Entity
public class InnerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private final String name;
public InnerEntity() {
}
public InnerEntity(String name) {
this.name = name;
}
}
public interface OuterRepository extends JpaRepository<OuterEntity, Long> {
OuterEntity findByName(String outerName);
}
public interface InnerRepository extends JpaRepository<InnerEntity, Long> {
InnerEntity findByName(String innerName);
}
@RequiredArgsConstructor
@Service
public class InnerService {
private final InnerRepository innerRepository;
@Transactional
public void createInner(String innerName) {
InnerEntity innerEntity = new InnerEntity(innerName);
innerRepository.save(innerEntity);
}
}
@RequiredArgsConstructor
@Service
public class OuterService {
private final OuterRepository outerRepository;
private final InnerService innerService;
@Transactional
public void createOuterAndInner(String outerName, String innerName) {
OuterEntity outerEntity = new OuterEntity(outerName);
outerRepository.save(outerEntity);
innerService.createInner(innerName);
}
}
@SpringBootTest
class TestServiceTest {
@Autowired
private OuterService outerService;
@Autowired
private OuterRepository outerRepository;
@Autowired
private InnerRepository innerRepository;
@Test
public void testCreateOuterAndInner() {
String outerName = "Outer";
String innerName = "Inner";
outerService.createOuterAndInner(outerName, innerName);
OuterEntity outerEntity = outerRepository.findByName(outerName);
assertNotNull(outerEntity);
InnerEntity innerEntity = innerRepository.findByName(innerName);
assertNotNull(innerEntity);
}
}
트랜잭션 추상화
다양한 데이터베이스 및 트랜잭션 관리 기술과의 상호 작용을 일관된 방식으로 처리하기 위한 개념으로 특정 데이터베이스나 트랜잭션 관리 기술에 종속되지 않고, 일관된 API를 사용하여 트랜잭션을 관리할 수 있습니다.
Spring Framework에서 트랜잭션 추상화는 PlatformTransactionManager 인터페이스를 통해 제공되는데 JPA를 사용하는 경우 JpaTransactionManager 구현체를 빈으로 등록하여 사용합니다.
물리적 트랜잭션과 논리적 트랜잭션
물리적 트랜잭션
데이터베이스와 직접적으로 통신하는 실제 트랜잭션으로 데이터베이스에 대한 실제 연결이 이루어지고, SQL 쿼리가 실행되며, 데이터베이스에서 데이터를 읽거나 수정하는 작업이 이루어집니다.
실제 커넥션을 통해서 트랜잭션을 시작(setAutoCommit(false)) 하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위로 이해가 가능합니다.
논리적 트랜잭션
트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위로 논리적 트랜잭션 개념은 REQUIRED 전파 속성을 사용하고 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타납니다.
모든 트랜잭션 매니저에서 커밋되어야 물리 트랜잭션이 커밋되며 하나의 논리 트랜잭션이라도 롤백 되면 물리 트랜잭션은 롤백 됩니다.
트랜잭션 전파
REQUIRED(default)
설정이 적용되는 각 메서드에 대해 논리적 트랜잭션 범위가 생성되는 전파 속성으로 기존 트랜잭션이 없으면 신규 트랜잭션을 생성하고, 있으면 기존 트랜잭션에 참여합니다.
REQUIRES_NEW
REQUIRED와 달리 영향을 받는 각 트랜잭션 범위에 대해 완전히 독립적인 트랜잭션을 사용하여 논리적 트랜잭션당 물리적 트랜잭션을 사용합니다. 그러므로 외부 트랜잭션이 내부 트랜잭션의 롤백 상태에 영향을 받지 않고 독립적으로 커밋하거나 롤백할 수 있습니다.
그 외에도 여러 트랜잭션 전파 속성이 있으며 밑에서는 REQUIRED 에 관하여 설명하겠습니다.
트랜잭션 시작
TransactionInterceptor 링크
위의 예제코드의 경우 @Transactional를 사용하여 선언적 트랜잭션을 사용하고 있습니다. Spring AOP를 사용하는 선언적 트랜잭션에서는 TransactionInterceptor가 MethodInterceptor 인터페이스를 구현해 메서드 호출에 관한 AOP 프록시를 생성하고 메서드 호출을 가로채서 트랜잭션 관리를 위한 코드를 자동으로 삽입합니다.
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
public Object invoke(MethodInvocation invocation) throws Throwable {
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
@Override
@Nullable
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}
}
TransactionAspectSupport 링크
TransactionInterceptor의 부모 클래스로 트랜잭션 매니저를 처리하고, 트랜잭션의 시작, 커밋, 롤백 및 종료와 같은 트랜잭션 생명주기 이벤트를 관리합니다.
그리고 @Transactional 애노테이션의 전파 동작(propagation behavior), 격리 수준(isolation level), 읽기 전용(read-only) 플래그, 타임아웃(timeout) 및 롤백 규칙(rollback rules) 속성을 해석하여 트랜잭션의 동작을 결정합니다.
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
final TransactionManager tm = determineTransactionManager(txAttr);
//트랜잭션 속성에 기반하여 트랜잭션 생성
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
//.....
//@Transactional으로 트랜잭션을 적용할 메서드 실행
retVal = invocation.proceedWithInvocation();
}
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
TransactionStatus status = null;
//.....
status = tm.getTransaction(txAttr);
}
getTransaction메서드는 TransactionDefinition(위에서는 TransactionDefinition를 상속받는 TransactionAttribute) 매개변수에 따라 TransactionStatus 트랜잭션 상태 객체를 반환합니다.
TransactionDefinition 링크
public interface TransactionDefinition {
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
default boolean isReadOnly() {
return false;
}
}
TransactionDefinition은 다른 트랜잭션의 작업과의 격리되는 정도를 나타내는 Isolation, 트랜잭션 컨텍스트가 이미 존재할 때 트랜잭션 메서드가 실행되는 경우의 동작을 지정하는 Propagation, 시간 초과하여 기본 트랜잭션 인프라에 의해 자동으로 롤백되기 전까지 실행되는 시간인 timeout, 코드가 데이터를 읽지만 수정하지 않을 때 최적화를 위해 사용하는 readOnly로 이루어져 있습니다.
TransactionStatus 링크
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus 에 담아서 반환하는데, TransactionStatus는 트랜잭션 코드가 트랜잭션 실행을 제어하고 트랜잭션 상태를 쿼리할 수 있는 간단한 방법을 제공합니다.
isNewTransaction()을 통해서 신규 트랜잭션임을 나타낼 수도 있고, 현재 호출 스택에 일치하는 트랜잭션이 있는 경우 트랜잭션 상태가 실행 스레드와 연관되어 있는 기존 트랜잭션임을 나타낼 수도 있습니다.
JpaTransactionManager 링크
Spring AOP를 기반으로 하는 트랜잭션 관리를 구현하는 클래스로 메소드 호출 전 후에 트랜잭션 처리를 수행합니다.
메소드가 호출될 때 트랜잭션을 시작하고, 메소드가 정상적으로 완료되면 트랜잭션을 커밋하며, 예외가 발생하면 트랜잭션을 롤백하는 과정을 확인할 수 있습니다.
public class JpaTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {
public JpaTransactionManager(EntityManagerFactory emf) {
afterPropertiesSet();
}
public void afterPropertiesSet() {
//.....
EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) getEntityManagerFactory();
DataSource dataSource = emfInfo.getDataSource();
//.....
}
}
우선, JpaTransactionManager 생성시 엔티티 매니저 팩토리를 통해 JPA 구현체에 따라서 데이터베이스 커넥션 풀을 생성합니다.
따로 설정하지 않는다면 스프링 부트가 기본으로 생성하는 데이터소스인 커넥션풀을 제공하는 HikariDataSource을 사용합니다.
protected void doBegin(Object transaction, TransactionDefinition definition) {
if (!txObject.hasEntityManagerHolder() ||
txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
EntityManager newEm = createEntityManagerForTransaction();
txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
}
Object transactionData = getJpaDialect().beginTransaction(em,
new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
}
JpaTransactionObject 내에 트랜잭션 동안 동일한 EntityManager를 사용하도록 보장하기 위해 저장하고 관리하는 EntityManagerHolder가 존재하지 않거나 EntityManager가 현재 진행 중인 트랜잭션과 관련된 경우 새롭게 매니저 팩토리에서 EntityManager를 생성하고 그렇지 않은 경우 EntityManagerHolder에서 꺼내 사용합니다.
HibernateJpaDialect 링크
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
throws PersistenceException, SQLException, TransactionException {
//.....
entityManager.getTransaction().begin();
//.....
}
EntityManager에게 요청하여 트랜잭션을 시작합니다.
AbstractLogicalConnectionImplementor 링크
public abstract class AbstractLogicalConnectionImplementor implements LogicalConnectionImplementor, PhysicalJdbcTransaction {
private TransactionStatus status = TransactionStatus.NOT_ACTIVE;
@Override
public void begin() {
//.....
getConnectionForTransactionManagement().setAutoCommit( false );
status = TransactionStatus.ACTIVE;
//.....
}
}
Spring JPA를 사용하는 경우 트랜잭션 매너지가 아닌 데이터베이스 연결에 대한 추상화를 제공하는 AbstractLogicalConnectionImplementor에서 수동 커밋 모드로 변경하고 트랜잭션 상태를 ACTIVE로 변경하여 트랜잭션 리소스를 시작합니다.
JpaTransactionManager 링크
protected void doBegin(Object transaction, TransactionDefinition definition) {
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
//.....
ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
//.....
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
//.....
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
}
엔티티 매니저를 통해 커넥션을 획득할 때 기존 트랜잭션에 이미 동기화(연결)된 연결이 있는 경우 해당 인스턴스가 반환됩니다.
그렇지 않으면 메서드 호출이 새 연결 생성을 트리거하고 기존 트랜잭션에 동기화되어 동일한 트랜잭션에서 나중에 재사용할 수 있게 됩니다.
그리고 각 스레드에 데이터소스와 커넥션을 고유하게 바인딩합니다. 이러한 행위는 해당 스레드에서만 엔티티 매니저를 사용함으로써 각 스레드에서 독립적으로 트랜잭션을 처리할 수 있게 합니다.
TransactionSynchronizationManager 링크
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
//.....
public static void bindResource(Object key, Object value) throws IllegalStateException {
//.....
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
//.....
}
}
이것이 가능한 이유는 TransactionSynchronizationManager 내부 확인 시 ThreadLocal을 이용하여 스레드당 리소스와 트랜잭션 동기화를 관리하기 때문입니다.
TransactionSynchronizationManager는 여러 스레드에서 일관된 트랜잭션 처리를 보장하기 위해 동기화 메커니즘을 제공하여 트랜잭션 동기화를 관리하는 역할을 담당합니다. 또한, 현재 스레드에 바인딩 된 트랜잭션 시작, 커밋, 롤백 등 트랜잭션의 상태를 추적하고 관리하며 DataSource와 연결된 Connection 객체 등의 리소스를 저장하고 검색하는 기능을 담당합니다.
트랜잭션 커밋
TransactionAspectSupport 링크
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
retVal = invocation.proceedWithInvocation();
//.....
commitTransactionAfterReturning(txInfo);
}
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
트랜잭션을 적용할 메서드가 정상적으로 실행 종료되면 TransactionAspectSupport는 트랜잭션 속성과 함께 트랜잭션 매니저에게 커밋을 요청합니다.
이때, 트랜잭션 속성에는 현재 트랜잭션이 기존 트랜잭션에 참여하지 않고 독립적으로 시작되었음을 의미하는 신규 트랜잭션 여부가 포함되게 됩니다.
AbstractPlatformTransactionManager 링크
else if (status.isNewTransaction()) {
unexpectedRollback = status.isGlobalRollbackOnly();
doCommit(status);
}
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
신규 트랜잭션인 경우에는 DB 커넥션에 실제 커밋을 호출하며 물리 트랜잭션을 끝내고 아닌 경우에는 커밋이나 롤백 시 물리 트랜잭션이 끝나버림으로 실제 커밋을 호출하지 않습니다.
이때, rollback-only 마크가 되어 있다면 커밋하지 않고 UnexpectedRollbackException 런타임 예외를 던져 롤백시킵니다.
rollback-only 마크의 경우 아래의 트랜잭션 롤백에서 알아보겠습니다.
JpaTransactionManager 링크
EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction();
tx.commit();
트랜잭션 매니저는 엔티티 매니저로부터 트랜잭션을 획득하여 커밋을 요청합니다.
트랜잭션 롤백
JpaTransactionManager 링크
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction();
//.....
txObject.setRollbackOnly();
}
Spring 프레임워크의 선언적 트랜잭션 관리는 개별 메서드 수준까지 트랜잭션 동작을 지정할 수 있어 필요한 경우 트랜잭션 컨텍스트 내에서 setRollbackOnly() 호출을 수행할 수 있습니다.
TransactionAspectSupport 링크
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
retVal = invocation.proceedWithInvocation();
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
//......
public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
return ((Try<?>) retVal).onFailure(ex -> {
if (txAttr.rollbackOn(ex)) {
status.setRollbackOnly();
}
});
}
}
스프링 프레임워크의 트랜잭션 인프라 코드가 함수 호출이 발생하는 순서인 call stack을 올라가는 과정에서 처리되지 않은 예외를 발견하고 처리하는데 신규 트랜잭션이 아닌 경우 체크되지 않은 예외만 트랜잭션 동기화 매니저에 롤백 전용 마커(rollbackOnly=true)라는 표시합니다.
case 1 : 외부 트랜잭션에서 롤백, 내부 트랜잭션에서 커밋
AbstractPlatformTransactionManager 링크
private void doRollbackOnCommitException(DefaultTransactionStatus status, Throwable ex) throws TransactionException {
try {
if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback after commit exception", ex);
}
doRollback(status);
}
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
}
JpaTransactionManager 링크
protected void doRollback(DefaultTransactionStatus status) {
try {
EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction();
if (tx.isActive()) {
tx.rollback();
}
}
finally {
if (!txObject.isNewEntityManagerHolder()) {
txObject.getEntityManagerHolder().getEntityManager().clear();
}
}
}
외부 트랜잭션은 신규 트랜잭션임으로 DB 커넥션에서 실제 롤백이 가능하도록 롤백을 실행하고 엔티티 매니저를 비워줍니다.
case 2 : 외부 트랜잭션에서 커밋, 내부 트랜잭션에서 롤백
AbstractPlatformTransactionManager 링크
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
doSetRollbackOnly(status);
}
}
JpaTransactionManager 링크
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction();
txObject.setRollbackOnly();
}
내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하는 것이 아닌 트랜잭션 동기화 매니저에 롤백 전용 마커를 진행합니다.
내부 트랜잭션 범위가 롤백 전용 마커를 설정하는 경우, 외부 트랜잭션이 롤백을 결정하는 것이 아닌 내부 트랜잭션 범위에 의해 트리거되는것 임으로 트랜잭션 호출자가 커밋이 실제로 수행되지 않았는데도 커밋이 수행된 것으로 오해하지 않도록 하기 위해UnexpectedRollbackException이 발생합니다. 따라서 내부 트랜잭션이 트랜잭션을 롤백 전용으로 표시하는 경우에도 외부 호출자는 여전히 커밋을 호출하고 롤백이 대신 수행되었음을 명확하게 표시하기 위해 UnexpectedRollbackException을 수신해야 합니다.
참고 :
spring-projects/spring-framework
스프링 공식 문서 16. Transaction Management
Spring Transaction Propagation in a Nutshell
- Total
- Today
- Yesterday
- 회고
- 이동 윈도우 로깅 알고리즘
- 처리율 제한 알고리즘
- 알고리즘
- 카카오프로젝트100
- 누출 버킷 알고리즘
- 개발자
- 글또
- 고정 윈도우 카운터 알고리즘
- 처리율제한
- 이동 윈도우 카운터 알고리즘
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |