Transaction?
데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 or 한 번에 수행되어야 할 일련의 연산들
특성
원자성(Atomicity) | Transaction의 연산은 모두 실행되거나 모두 실행되지 않아야 한다. → Transaction 수행 중 하나라도 오류가 발생하면, 해당 Transaction 전부 취소 |
일관성(Consistensy) | Transaction이 성공적으로 실행되면, 항상 일관성 있는 데이터베이스 상태를 변환한다. |
독립성(Isolation) | 둘 이상의 Transaction이 동시에 실행되는 상황에서, 한 Transaction이 데이터베이스를 갱신중이면 다른 Transaction은 접근 불가능 해야한다. |
영속성(Durablity) | 성공적으로 수행된 Transaction의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다. |
트랜잭션 매니저?
: 스프링이 제공하는 트랜잭션 처리를 위한 부품이다.
: 데이터 액세스 기술을 은닉해 주어, 기술이 바뀌어도 일관되게 사용할 수 있다.
일관된 사용이 가능한 이유는 트랜잭션 매니저의 구현 클래스가 공통 인터페이스인 PlaFormTransactionManager를 구현하기 때문임. 이 인터페이스를 구현한 클래들 중 현재 지원하고 있는 클래스는 다음과 같다.

Transaction이 설정 가능한 정보
- 전파(propagation) 속성
- 독립성(isolation) 수준
- 타임아웃(timeout)
- 읽기 전용(readonly)
- 롤백 대상 예외
- 커밋 대상 예외
전파 속성
: 트랜잭션의 전파 방법을 설정하는 속성
아래의 코드는 다음과 같은 상황이다
- 컨트롤러 A가 서비스 C의 a()를 호출
- 컨트롤러 B가 서비스 C의 b()를 호출하고, b()가 같은 클래스 내 a()를 호출
C 클래스 위에 @Transaction이 선언되어 있다면 각각 두 상황은 어떤 결과가 발생할까? 결론을 먼저 말하면 설정된 전파 속성에 따라 달라진다. 물론 코드만 놓고 봤을 때는 @Transaction에서 기본으로 설정된 전파 속성으로 처리가 진행될 것이다.
@Controller
@RequiredArgsConstructor
public class A {
private final C c;
public void a() {
c.a();
}
}
@Controller
@RequiredArgsConstructor
public class B {
private final C c;
public void a() {
c.b();
}
}
@Service
@Transaction
public class C {
public void a() { }
public void b() {
a();
}
}
각각 어떤 전파 속성이 있고, 두 상황에서 전파 속성에 따라 처리되는 결과는 다음과 같다.
전파 속성 | 상황 1 | 상황 2 |
PROPAGATION_REQUIRED(기본 값) | 트랜잭션 시작 | a메서드가 b메서드의 트랜잭션에 참가 |
PROPAGATION_REQUIRES_NEW | 트랜잭션 시작 | 새 트랜잭션 시작 → (a메서드와 b메서드에 대히여 각각의 트랜잭션이 생성됨) |
PROPAGATION_SUPPORTS | 트랜잭션을 하지 않음 | a메서드가 b메서드의 트랜잭션에 참가 |
PROPAGATION_MANDATORY | 예외 발생 | a메서드가 b메서드의 트랜잭션에 참가 |
PROPAGATION_NESTED | 트랜잭션 시작 | b메서드의 트랜젝션 내에 새로운 트랜잭션이 생성 |
PROPAGATION_NEVER | 트랜잭션을 하지 않음 | 예외 발생 |
PROPAGATION_NOT_SUPPORTED | 트랜잭션을 하지 않음 | 트랜잭션을 하지 않음 |
여기서 트랜잭션 참가는 2개 이상의 트랜잭션이 하나의 트랜잭션으로 합쳐짐을 의미한다.
독립성 수준
: 트랜잭션 처리가 병행해서 실행될 때, 각 트랜잭션의 독립성을 결정하는 것
독립성을 방해하는 상태는 다음과 같이 있다.
Dirty Read
: 다른 트랜잭션이 변경했지만, 아직 커밋하지 않은 데이터를 읽어내는 것, 여기서 Dirty는 구분되지 않는 어중간한 데이터를 의미한다.
(변경된 데이터들이 commit을 할지 rollback을 할지 모르기 때문)
Unrepeatable Read
: 트랜잭션 내에서 같은 데이터를 읽는 동안 다른 트랜잭션이 해당 데이터를 변경하여, 이전에 읽은 데이터와 다른 데이터를 읽어내는 것
(같은 값을 반복해서 읽을 수 없다는 의미)
Phantom Read
: 트랜잭션 내에서 같은 데이터를 여러 번 읽을 때 다른 트랜잭션이 새로운 데이터를 추가하여, 이전에 읽은 데이터에서 새로 추가된 데이터까지 읽어내는 것(없던 것이 갑자기 생겨났다는 의미)
즉, 독립성 수준은 이러한 모순들을 어디까지 허용할지 결정하는 것을 의미한다.
종류는 다음과 같이 있다.
독립성 수준 | 의미 | 허용 범위 |
ISOLATION_READ_COMMITED | 다른 트랜잭션이 변경했지만 커밋하지 않은 데이터는 읽을 수 없음 | Dirty Read - 불허 Unrepeatable Read - 허용 Phantom Read - 허용 |
ISOLATION_READ_UNCOMMITED | 다른 트랜잭션이 변경하고 커밋하지 않은 데이터를 읽을 수 있음 | Dirty Read - 허용 Unrepeatable Read - 허용 Phantom Read - 허용 |
ISOLATION_REPEATABLE_READ | 트랜잭션 내에서 여러 번 데이터를 읽을 때, 다른 트랜잭션이 해당 데이터를 변경해도 읽을 수 있음 | Dirty Read - 불허 Unrepeatable Read - 불허 Phantom Read - 허용 |
ISOLATION_SERIALIZABLE | 트랜잭션을 하나씩 순서대로 처리해 독립시킴 (= 직렬화) |
Dirty Read - 불허 Unrepeatable Read - 불허 Phantom Read - 불허 |
ISOLATION_DEFAULT | 데이터베이스가 제공하는 기본 독립성 수준 | 사용하는 DB에 따라 다름. |
트랜잭션 사용 방법
선언적 사용법과 명시적 사용법이 있다.
선언적 트랜잭션 설정
1. Bean 파일로 설정
트랜잭션 어드바이스 설정 → AOP Advisor로 트랜잭션 처리를 수행할 클래스 지정
<tx:advice>
<tx:attributes>
<!-- get으로 시작하는 메서드의 설정 -->
<tx:method name="get*" read-only="true" />
<!-- update으로 시작하는 메서드의 설정 -->
<tx:method name="update*"
propagation="REQUIRED"
isolation="READ_COMMITED"
timeout="10"
read-only="false"
rollback-for="BusinessException"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="transactionAdvice"
<!-- 임의의 패키지, 클래스 및 인터페이스 끝에 Service가 있는 모든 것을 지정 -->
pointcut="execution(* *..*Service.*(..))" />
</aop:config>
2. @Transactional
Spring은 @Transactional으로 트랜젝션 처리를 지원한다. @Transactional은 주로 mvc 패턴 중 Service 레이어에서 선언하며, 클래스, 메서드 단위로 선언이 가능하다.
- 클래스에 선언하면, 해당 클래스에 정의된 모든 메서드에서 트랜젝션 처리를 진행한다.
@Transactional
public class DefaultFooService implements FooService {
//이 내부에 정의된 모든 메서드에 @Transaction이 선언된 것과 동일함
}
그러면 트랜잭션은 Controller에서 서비스 클래스의 메서드를 호출하는 시점에 시작해서, 메서드를 마치고 다시 Controller로 돌아가는 시점에 종료가 되는 것까지 어느 정도 생각해 볼 수 있을 것이다.
@Transactional을 사용함으로써 얻는 이점
commit & rollback의 연산을 따로 명시하지 않아도 된다. 트랜잭션의 특성 중 원자성(Atomicity)을 그대로 반영하기 때문에 Transaction이 정상 수행 후 종료 되면 commit, 그렇지 않으면 rollback 연산을 자동으로 수행한다.
아래의 코드는 예전에 JDBC를 이용하여 자바 프로그램을 만들었을 때 작성한 코드의 일부이다.
public class OrderService {
...
//결제 내역에 추가
result = orderDao.addPay(newPayLog, conn);
if(result == 1) {
JDBCTemplate.commit(conn);
result = orderDao.updateMoney(Main.loginMember.getMemberNo(), balance, conn);
if(result == 1) {
JDBCTemplate.commit(conn);
} else {
JDBCTemplate.rollback(conn);
throw new Exception("구매 후 잔액 수정 실패");
}
} else {
JDBCTemplate.rollback(conn);
throw new Exception("페이 테이블 추가 실패");
}
...
}
이 코드에서는 다음과 같은 문제점이 있다
- commit이나 rollback의 연산이 OrderService에서 직접적으로 선언이 되어 OrderService가 JDBC의 API에 직접적으로 의존함.
- OrderService에서 Dao층으로 Connection 객체를 넘겨줌(Dao에서 사용하는 Connection 객체와 OrderService에서 가져온 Connection과 동일)
- Dao에서 DB에 연달아 접근하는 경우 코드의 가독성이 떨어짐.
Spring은 이러한 문제점을 해결하기 위해 TransactionManager와 Transaction Advice를 이용할 수 있다.
명시적 트랜잭션 설정
소스 코드에서 Transaction 처리 매서드를 호출, 코드에서 직접 설정하기 때문에, 가독성이 나쁠 수 있다.
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);
//... 기타 등등 설정 가능
명시적 트랜잭션의 사용 용도
같은 클래스 안의에서 트랜잭션을 처리해야 할 때 사용할 수 있다.
선언적 트랜잭션은 Proxy 객체를 매개로 하여 Service 레이어를 호출하는 방식으로 트랜잭션을 처리하기 때문에 같은 클래스 내에서 @Transactional로 지정한 다른 메서드를 호출해도 트랜잭션 처리가 이루어지지 않는다
(물론 같은 클래스 내에서 @Transactional로 지정한 다른 메서드를 다른 클래스로 분리하여 호출하면 해소가 가능.)
참고
https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html
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://www.baeldung.com/spring-programmatic-transaction-management
하세가와 유이치, 오오노 와타루, 토키 코헤이. (2019). 스프링 4 입문(2). 한빛미디어