데이터베이스에서 수행되는 여러 작업을 하나의 논리적 단위로 수행하는 것
트랜잭션의 국룰 예시인 송금을 보자.

최소 3가지의 작업이 이뤄질것이다.
즉, 트랜잭션은 하나 이상의 쿼리가 모여서 실행되는 논리적 단위라고 이해하면 된다.
한 트랜잭션의 작업이 모두 성공하거나 모두 실패되어야 한다.
Me는 Other에게 만원을 입금한다. Me의 계좌에는 만원이상이 있으므로 돈을 보낼수 있다.
Other에게 “송금”버튼을 눌렀는데 딱 은행서비스 점검시간에 걸처버렸다. 중간에 오류가 발생하였으므로 송금은 수행되지 않았다.
Me계좌의 만원이 줄지도 않았고, Other계좌에 만원이 추가되지도 않았다. 이는 트랜잭션이 RollBack하였기 때문이다.
특정 쿼리를 실행 했는데 부분적인 오류가 발생한다면 트랜잭션은 실패처리 된다.
트랜잭션의 부분성공 or 부분실패는 존재할수 없다.
하나의 트랜잭션 이전과 이후, 데이터베이스의 상태는 이전과 같이 유효해야 한다.
데이터베이스의 제약과 규칙은 동일해야 한다.
금액이라는 컬럼이 숫자형(int, float…)이어야 하는데 varchar등이 될순 없다.
설명이 조금 난해한데 많은 글들을 참고할때에 일관성은 설명이 조금 제각각이다.
정의할 당시에도 많은 논의가 오고간 만큼 그려려니 하고 넘어가련다.
모든 트랜잭션은 다른 트랜잭션으로부터 독립 되어야 한다.
Me는 계좌에 3만원이 있는 상황에서 Other에게 만원을 송금하려고 한다.
은행에서 자동이체를 하려고 내 계좌에서 2만원을 출금했다.
출금과 송금이 순차적으로 이뤄졌고(실행순서는 중요하지 않다) 각 이체는 서로의 상태를 알지 못한다.
이체 작업을 연속으로 실행하는것과 동일한 결과가 나타난다.
트랜잭션이 성공적으로 수행되면 결과가 남아 영구적으로 유지 되어야 한다.
송금이 성공적으로 완료되면 송금 이력 내역등은 DB에 영구적으로 남아야 한다.
각 애플리케이션(JDBC, JPA, Hibernate)마다 트랜잭션을 처리하는 과정은 다릅니다. 저는 JPA에서 처리하는 과정을 중점적으로 살펴보겠습니다.
저는 이미 @Transactional을 사용하여 트랜잭션 처리를 하고 있습니다. 이미 AOP를 이용하고 있던거죠.
AOP를 이용하지 않다면 개발자는 일일히 아래와 같은 코드를 작성해야 합니다.
public void addLineDistance(List<LineDistance> lineDistanceList) {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
for (LineDistance lineDistance: lineDistanceList) {
ldRepository.save(lineDistance);
}
this.transactionManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
짧은 코드지만 여러 책임을 가지고 있어서 분리하기도 어렵고 의존성 주입 및 try-catch문도 일일히 작성해주어야 하는 번거로움이 있습니다. AOP를 이용하면 간결한 코드를 작성할 수 있습니다.
@Transactional
public void addLineDistance(List<LineDistance> lineDistanceList) {
for (LineDistance lineDistance: lineDistanceList) {
ldRepository.save(lineDistance);
}
}
READ-UNCOMMITTED → READ-COMMITTED → REPEATABLE-READ → SERIALIZABLE
왼쪽에서 오른쪽으로 갈수록 속도는 느리지만 데이터의 일관성을 보장해줍니다.


여기서도 문제가 발생한다. 트랜잭션 B내에서 같은 데이터를 여러번 조회 했는데 데이터가 다른 경우가 발생한다. 이를 Non-Repeatable-Read문제라고 한다. 추가로 Phantom Read 문제도 있다.
REPEATABLE-READ
특정 데이터를 반복 조회시 같은 값을 반환한다.
트랜잭션이 완료될 때까지 READ의 데이터에 Shared Lock을 사용한다. 트랜잭션 A가 커밋되었다 해도 트랜잭션 B는 result를 5만 반환하게 된다. MySQL의 default 설정이다. 하지만 Phantom-Read문제는 발생한다.

Phantom-Read는 Non-Repeatable-Read 문제의 한 종류로써 새로운 데이터가 생기거나 기존의 데이터가 사라졌을 때 동일한 쿼리가 다른 값을 반환하는 문제이다.
SERIALIZABLE

트랜잭션 A과 커밋되어야만 실행된다. 시간이 오래걸리더라도 데이터의 정합성을 지키기 위해서라면 고려할만 하다.
전파 타입을 알아보기 전에 @Transactional의 특징을 하나 짚고 넘어가자.
@Transactional
public LineDistance edit(LineDistance lineDistance) {
LineDistance ldRecord = ldRepository.findByDepartureTmlCdAndArrivalTmlCd(lineDistance.getDepartureTmlCd(), lineDistance.getArrivalTmlCd())
.orElseThrow(() -> new NoDataFoundException("NO LINE-DISTANCE FOUND"));
ldRecord.modify(lineDistance);
this.delete(ldRecord.getDepartureTmlCd(), ldRecord.getArrivalTmlCd());
return ldRecord;
}
...
@Transactional
public void delete(String departureTmlCd, String arrivalTmlCd) {
ldRepository.deleteByDepartureTmlCdAndArrivalTmlCd(departureTmlCd, arrivalTmlCd);
}
delete메소드에서 @Transactional이 선언되어 있고 이를 호출하는 edit메소드에서 @Transactional이 선언되어 있다. 기존 트랜잭션이 진행중인데 추가적인 트랜잭션을 진행해야 하는 경우가 빈번하게 발생한다. 추가 트랜잭션 진행을 어떻게 할지 결정하는것이 전파 속성이다.

트랜잭션이 전파되지 않을 때(논리 트랜잭션이 1개만 존재할때)에는 물리 트랜잭션만 사용된다.
하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백되고 모든 논리 트랜잭션이 커밋 되어야 물리 트랜잭션이 커밋되는거에 명심하자.
전파 속성은 아래 표를 보면 쉽게 이해할 수 있다.
| 종류 | 트랜잭션 존재 | 트랜잭션 미존재 | 비고 |
|---|---|---|---|
| REQUIRED | 기존 트랜잭션 이용 | 신규 트랜잭션 수행 | Default |
| SUPPORTS | 기존 트랜잭션 이용 | 트랜잭션 없이 수행 | |
| MANDATORY | 기존 트랜잭션 이용 | Exception 발생 | 꼭 이전 트랜잭션이 있어야 하는 경우 |
| NEVER | Exception 발생 | 정상적으로 트랜잭션 없이 수행 | 트랜잭션 없을때만 작업이 진행되어야 할때 |
| NOT_SUPPORTED | 트랜잭션이 종료될 때 까지 대기한 후 트랜잭션이 종료되고 나면 실행 | 트랜잭션 없이 로직 수행 | 기존 트랜잭션에 영향을 주지 않아야 할 때 사용 |
| REQUIRES_NEW | 현재 트랜잭션이 종료될 때 까지 대기한 후 새로운 트랜잭션을 생성하고 실행 | 신규 트랜잭션 생성하고 로직 실행 | 이전 트랜잭션과 구분하여 새로운 트랜잭션으로만 처리 필요할 때 사용 |
| NESTED | 현재 트랜잭션에 Save Point를 걸고 이후 트랜잭션을 수행 | 신규 트랜잭셔 ㄴ수행하고 로직 수행 | DBMS에 따라 미지원 |
A서비스는 데이터를 변경하니 @Transactional을 선언하자. B서비스도 데이터 변경이 일어나니 @Transactional을 선언하자. A서비스가 B서비스를 호출한다면 트랜잭션이 중첩된다. 이는 문제점을 야기할거 같은데 구체적으로 어떤 문제점을 일으킬까?
모든 서비스마다 트랜잭션을 설정한다기 보단 범위를 최소화 하여 나누자. 고립 레벨 및 전파 속성을 적절하게 선언하자.
근데 이게 말이 쉽지. B서비스가 A서비스뿐만 아니라 C, D서비스등에서 사용되고 있다면 범위를 쪼개기 어렵다. 이를 끊임없이 고민하고 해결하는 것이 나에게 주어진 과제이다.