트랜잭션 처리
트랜잭션 처리는 다음과 같은 방식을 따릅니다.
트랜잭션 관리 방식
트랜젝션은
@Transactional어노테이션을 통해 명시적으로 관리함(2023년 5월 개정)
데이터베이스 구조 및 연결 원칙
X2BEE Framework는 ReadWrite 데이터베이스(Master)와 ReadOnly 데이터베이스(Slave)로 구성된 이중화 구조를 기본으로 가정함.
개발 편의성을 위한 연결 방식 제공:
@Transactional어노테이션이 선언된 클래스 및 메서드는 ReadWrite 데이터베이스(Master)에 연결@Transactional어노테이션이 없는 Service 계층 메서드는 기본적으로 ReadOnly 데이터베이스(Slave)에 연결
다중 데이터소스 및 트랜잭션 연결 (2023년 6월 추가)
서로 다른 데이터베이스 스키마 및
TransactionManager를 활용하여 다중 데이터소스를 트랜잭션으로 연결하는 방법 지원
트랜잭션 전파 레벨
Propagation.REQUIRED (기본 값) 특정 메서드의 트랜잭션이
Propagation.REQUIRED로 설정되었을 때의 동작은 다음과 같다. 기본적으로 해당 메서드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않았다면 트랜잭션을 새로 시작한다(새로운 연결을 생성하고 실행). 만약 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 로직을 실행한다(동일한 연결 안에서 실행). 예외가 발생하면 롤백이 되고 호출한 곳에도 롤백이 전파된다. Propagation.REQUIRED는 기본값이므로 생략 가능. 다만 해당 메서드가 호출한 곳과 별도의 쓰레드라면 전파 레벨과 상관 없이 별도의 트랜잭션을 생성하여 실행한다. Spring은 내부적으로 트랜잭션 정보를 ThreadLocal 변수에 저장하기 때문에 다른 쓰레드로 트랜잭션이 전파되지 않음.Propagation.REQUIRES_NEW 매번 새로운 트랜잭션을 시작한다(새로운 연결을 생성하고 실행). 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존 트랜잭션은 메서드가 종료할 때까지 대기 상태가 되며, 새로운 트랜잭션은 독립적으로 실행된다. 새로운 트랜잭션에서 예외가 발생해도 호출한 곳에는 롤백이 전파되지 않는다.
Propagation.SUPPORTS 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 진행한다.
Propagation.NESTED 기본적으로 REQUIRED와 동일하게 작동하나, SAVEPOINT를 지정한 시점까지 부분 롤백이 가능하다. 데이터베이스가 SAVEPOINT 기능을 지원해야 사용 가능(예: Oracle).
Propagation.MANDATORY 이미 시작된 트랜잭션이 있으면 참여하지만, 없으면 예외를 발생시킨다. 독립적으로 트랜잭션을 진행하면 안 되는 경우 사용.
Propagation.NOT_SUPPORTED 트랜잭션을 사용하지 않음. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.
Propagation.NEVER 트랜잭션을 사용하지 않도록 강제. 이미 진행 중인 트랜잭션이 있으면 예외를 발생시킴.
자주 사용되는 전파 설정: REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS. 기본적으로는 REQUIRED와 트랜잭션을 분리하기 위한 REQUIRES_NEW를 주로 사용하면 됨.
트랜잭션 경계 설정
ReadWrite 트랜잭션은 아래 AOP 코드에 의해 결정됩니다. @Transactional 어노테이션이 선언된 함수의 경우 ReadWrite로 동작합니다.
ReadOnly 트랜잭션은 아래 AOP 코드에 의해 결정됩니다. 아래 코드에서는 @Transactional 어노테이션이 없는 서비스 클래스에 한해서만 ReadOnly 트랜잭션에 참여시키기 위해 한정자 within(@org.springframework.stereotype.Service *) 를 선언했습니다.
기본적으로 @Transactional 어노테이션이 없는 함수의 경우 ReadOnly로 동작하지만, 시작 함수가 @Transactional이 붙은 함수로 시작한 경우 @Transactional 어노테이션이 없는 조회 서비스라도 ReadWrite로 동작하게 됩니다.
Exception Rollback 처리
모든 Exception에 대해서 rollback 처리를 하기 위해 어노테이션 구조는 기본적으로 아래와 같습니다.
위 선언에서 propagation = Propagation.REQUIRES(=REQUIRED)가 기본값입니다.
트랜잭션의 rollback이 트리거되기 위해서는 기본적으로 RuntimeException과 그 하위 클래스여야 합니다. Checked Exception(예: Exception)을 캐치하여 rollback시키려면 rollbackFor = {Exception.class}처럼 명시적으로 설정해야 합니다.
작성 예 1
위 예에서는 Propagation.REQUIRED가 작용하여 하나의 트랜잭션(연결) 위에서 작동하므로 Exception이 발생하면 전체가 rollback됩니다.
작성 예 2
txTest10:
sampleService3.testinsert1()에서 발생한 Exception은 같은 트랜잭션이므로 전체가 rollback됨.txTest11:
sampleService3.testinsert2()는REQUIRES_NEW로 새로운 트랜잭션이므로 해당 부분만 rollback되고test2()의 데이터는 유지됨.
작성 예 3
위 예에서 txTest12는 루프 도중 마지막에서 Exception이 발생하면 루프 내에서 수행한 대부분의 데이터 저장은 rollback되지만, 성공 로그와 실패 로그는 REQUIRES_NEW 트랜잭션이므로 별도로 커밋되어 저장됩니다.

트랜잭션 분리
Service 함수 내에서 DAO(Repository) 호출 단위로 트랜잭션을 분리하기 위한 어노테이션 구조는 아래와 같습니다.
작성 예:
위 예에서는 test3()이 REQUIRES_NEW로 별도의 트랜잭션이므로, 외부에서 예외가 발생해도 test3()의 저장 데이터는 rollback되지 않음. 반대로 test2()의 데이터는 외부 트랜잭션의 영향을 받아 rollback될 수 있음.
@Transactional 사용 시 주의사항
@Transactional(readOnly=true) 동작
@Transactional(readOnly=true) 일때 ReadWrite 데이터베이스에 연결되지만 ReadOnly로 작동됩니다.
내부적으로 Connection.setReadOnly(true)가 호출됩니다.
readOnly 속성의 기본값 및 JPA와의 관계
readOnly 속성의 기본값은 false 입니다.
JPA의 영속성 Context 내에서 readOnly=true 일때, Commit 시 Entity의 변경감지(Dirty Checking)를 수행하지 않으므로 성능상 이점을 얻을 수 있습니다.
따라서 ReadWrite DB를 대상으로 JPA Entity를 조회할 때 readOnly=true를 사용합니다.
코드 가독성을 위해 readOnly=false/true 속성은 명시해주길 권장합니다.
@Transactional의 적용 대상 및 한계
@Transactional을 클래스 또는 메서드 레벨에 명시하면 해당 메서드 호출 시 지정된 트랜잭션이 작동합니다.
단, 조건이 있는데, 해당 클래스의 Bean을 다른 클래스의 Bean에서 호출할 때만 @Transactional을 인지하고 작동합니다.
같은 빈 내에서 @Transactional이 명시된 다른 메서드를 호출해도 작동하지 않습니다.
이유: Spring Framework는 내부적으로 AOP를 통해 해당 어노테이션을 인지하여 프록시를 생성하여 트랜잭션을 자동 관리하기 때문입니다.
이것을 해결하기 위한 방법으로는 다음 두 가지가 있습니다.
구조 변경: ReadWrite, ReadOnly의 함수들을 2개의 서비스로 분리하여, 각각의 서비스가 호출될 때마다 AOP가 동작하도록 해서 각각의 서비스에 맞는 ReadWrite / ReadOnly 커넥션 풀을 설정하도록 합니다.
지연조회: ObjectProvider 등을 사용하여, 실제 코드가 동작할 때 해당 인스턴스를 지연 조회하여 AOP가 동작하도록 합니다.
아래는 지연조회(ObjectProvider)를 사용하는 예제 코드입니다.
위 예에서 txTest13() 함수를 호출하였을 때는 같은 서비스 내의 함수를 호출했기 때문에 트랜잭션 경계 설정이 작동하지 않아 testInsert()의 REQUIRES_NEW 옵션이 적용되지 않고 모든 데이터가 rollback 됩니다.
그러나 txTest14() 함수의 경우 지연조회 방법인 ObjectProvider를 사용하였기 때문에 같은 서비스 내의 함수를 호출하더라도 REQUIRES_NEW 옵션의 트랜잭션 경계가 제대로 적용되어 test2()의 데이터는 rollback 되지만 testInsert()의 데이터는 정상적으로 저장됩니다.
스프링내에서는 b의 지연조회 방법보다는 a의 구조변경 방식을 지향합니다.
다중 데이터 소스 연결하기
서로 다른 DB 스키마 및 TransactionManager를 사용한 다중 데이터소스 트랜잭션 연결 방법(예: api-member에서 orderrwdb와 drmcrwdb 간 @Transactional rollbackFor 사용방법 및 설명)
서비스 예:
@Transactional 사용 시 value = "chainedTransactionManager"를 지정하여 사용합니다.
ChainedTransactionManager 구성 예:
ChainedTransactionManager는 org.springframework.data(Spring Data Commons)에서 제공하는 방식으로, 여러 트랜잭션 매니저를 하나로 묶어(Start/Commit을 순차 수행) 사용함으로써 하나의 트랜잭션처럼 동작하게 함.다만
ChainedTransactionManager는 '완벽한' 트랜잭션을 제공하지 않으며, 에러 영향도가 큰 트랜잭션을 체인의 뒤쪽에 배치하는 등 주의가 필요함.
마지막 업데이트