안녕하세요. 개발자 Jindory입니다.
오늘은 Transacion과 Propagation Model(전파모델) 및 Isolation Level(격리 수준)에 대해서 글을 작성해보려고 합니다.
[ 글 작성 이유 ]
회사에서 Transactional 어노테이션과 함께 옵션들이 있었는데, 무심코 지나갔던것 같아서 Transaction과 Isolation Level, Propagation Model에 대해서 정리해보고자 합니다.
Transaction이란?
Transaction이란 데이터베이스의 상태를 변경할때 "하나의 논리적 기능을 수행하기 위한 작업의 단위 혹은 일련의 연산"을 의미합니다. 예를 들어 A라는 계좌에서 B라는 계좌에 송금하는 기능이 실행된다고 했을때, A라는 계좌에서 송금할 금액을 차감하는 연산과 B라는 계좌에 송금할 금액을 더하는 연산을 하나의 업무 처리 단위로 묶어 처리해야 문제가 생기지 않을 것입니다. 그래서 A 계좌 차감과 B 계좌 추가 연산 중 문제 발생시 모든 단위를 취소해야하며, 정상적으로 완료 되었을시 두 연산의 변경사항이 모두 반영되어야 합니다.
Spring Framework에서 @Transactional 어노테이션
@Transactional 어노테이션은 인터페이스, 클래스 또는 메서드가 정의된 트랜잭션을 사용해야한다고 알려주는 메타 데이터 입니다. @Transactional 어노테이션은 Spring 프레임워크에서 데이터베이스 트랜잭션 관리를 간편하게 도와주는 어노테이션입니다.
@Transactional 어노테이션이 정의된 메서드(클래스)는 해당 메서드가 실행될 때 트랜잭션을 시작하고 메서드(클래스)가 완료될 때 트랜잭션을 종료합니다.
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Account fromAccount, Account toAccount, BigDecimal amount) {
// 계좌 간 송금 로직
// ...
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
@Transactional 어노테이션에서 지원하는 설정
Transactional의 설정에는 아래와 같은 설정이 있습니다.
- Propagation Setting(전파 설정)
- Isolation Level(격리 수준)
- 읽기/쓰기 설정
- Transaction Timeout
- 트랜잭션 Rollback
이번 글에서는 Transactional의 Propagation(전파 모델)과 Isolation에 대해서만 다뤄보도록 하겠습니다.
Propagation Model
트랜잭션 어노테이션의 propagation 속성을 사용하면 트랜잭션 전파 유형을 지정할 수 있습니다.
propagation은 여러 트랜잭션이 관련된 경우 트랜잭션이 전파되는 방식을 결정합니다.
트랜잭션 전파(propagation)에는 여러가지 유형이 존재합니다.
1. Required
기본 전파 유형으로 이미 트랜잭션이 있는 경우 현재 메서드가 해당 트랜잭션에 참여합니다. 트랜잭션이 존재하지 않으면 메서드에 새 트랜잭션을 생성합니다. REQUIRED는 Default 옵션이므로 생략이 가능합니다. 어노테이션은 아래와 같이 작성할 수 있습니다.
@Transactional(propagation = Propagation.REQUIRED)
public void requiredMethod(String sample){
...
}
2. Requires new
이 전파 유형은 REQUIRED와 달리 각 트랜잭션 범위에 대해 항상 독립적인 새로운 트랜잭션을 생성하고 부모 트랜잭션에는 참여하지 않습니다. 트랜잭션이 이미 존재하는 경우 새 트랜잭션이 완료될 때까지 일시 중단합니다. 각각의 트랜잭션이 롤백되어도 서로 영향을 주지 않습니다.
@Transactional(propagation = Propagataion.REQUIRES_NEW)
public void requiresNewMethod(String sample){
...
}
3. Nested
NESTED는 중첩된 이라는 뜻으로 부모 트랜잭션이 존재할 경우 중첩된 새로운 트랜잭션을 생성합니다. 만약 중첩된 트랜잭션 내부에서 롤백이 발생될 경우 해당 내부 트랜잭션의 시작 지점까지만 롤백이 진행됩니다.
이렇게 내부된 트랜잭션이 롤백 될 경우 중첩 트랜잭션의 시작 지점까지만 롤백이 될 수 있는 이유는 여러개의 세이브 포인트를 가지고 있기 때문입니다. 이 세이브 포인트를 통해 일부 작업을 롤백할 수 있습니다. 중첩된 트랜잭션 범위에서 중첩 트랜잭션이 롤백되면 부모 트랜잭션은 일부 작업이 롤백되더라도 물리적 트랜잭션을 계속할 수 있습니다.
중첩 트랜잭션은 부모 트랜잭션이 커밋될 때 같이 커밋이 진행됩니다.
만일 부모 트랜잭션이 없다면 새로운 트랜잭션을 생성합니다.
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod(String sample) {
...
}
4. Never
트랜잭션을 생성하지 않으며, 부모 트랜잭션이 존재할 경우 Exception을 발생시킵니다.
@Transactional(propagation = Propagation.NEVER)
public void neverMethod(String sample) {
...
}
5. Mandatory
부모 트랜잭션이 있을 경우 참여한다. 만일 부모 트랜잭션이 없을 경우 Exception을 발생시킵니다.
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryMethod(String sample) {
...
}
6. Supports
부모 트랜잭션이 있을 경우 부모 트랜잭션에 참여하고, 없다면 트랜잭션을 생성하지 않습니다.
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsMethod(String sample) {
...
}
7. Not Supports
부모 트랜잭션이 있을 경우 일시 중단합니다. 진행중인 부모 트랜잭션이 없다면 트랜잭션을 생성하지 않습니다.
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedMethod(String sample) {
...
}
Transaction Isolation
Isolation은 데이터베이스 트랜잭션에서 동시성을 관리하는 방법을 정의하는 개념입니다. 이는 데이터베이스 관리 시스템에서 트랜잭션 간의 상호작용을 어떻게 제어할지 결정하기 위해서 Isolation Level을 설정한다고 볼 수 있습니다.
예를 들어 두 개의 트랜잭션이 동시에 실행되고 있을 때, 하나의 트랜잭션에서 데이터를 수정하면 다른 트랜잭션에서도 해당 변경 사항을 볼 수 있어야 합니다. 이를 통해 데이터베이스의 일관성과 정확성을 유지할 수 있습니다. 만일 변경사항이 다른 트랜잭션에 보이지 않는다면, 데이터의 불일치가 발생할 수 있습니다.
Spring의 @Transactional에서 Isolation Level을 설정하는 옵션은 아래와 같습니다.
DEFAULT(미설정), READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
1. DEFAULT
default isolation level은 isolation Level을 설정하지 않았을때를 의미합니다. 스프링이 새로운 트랜잭션을 생성할 때, 이 isolation level은 우리가 사용하는 RDBMS의 isolation level과 동일하게 설정됩니다. 우리가 흔히 사용하는 RDBMS의 기본 격리 수준은 아래와 같습니다.
- READ UNCOMMITTED :
- READ COMMITTED : MS SQL, Oracle, PostgreSQL, SQL Server
- REPEATABLE READ : MySQL
- SERIALIZABLE :
따라서 데이터베이스를 변경하면 기본 고립 수준도 함께 변경될 수 있으므로 이를 고려해야 합니다.
2. READ_UNCOMMITTED Isolation
READ_UNCOMMITTED는 가장 낮은 Isolation Level로 커밋되지 않은 데이터(트랜잭션 처리중인 데이터)에 대한 읽기를 허용합니다. 이는 가장 많은 동시 접근을 허용하게 됩니다. 하지만 커밋되지 않은 데이터를 읽을 수 있기 때문에 Dirty Read라는 동시성 문제가 발생할 수 있습니다.
Dirty Read란 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽어서 일관성을 유지 못하는 문제입니다. 아래의 그림을 보면서 Dirty Read에 대해서 더 알아보겠습니다.
-
- 위 그림을 봤을때 학번이 7인 학생의 전공을 변경하는 Transaction A 작업은 rollback되어 컴퓨터 공학이 아니지만 학번 7의 전공을 조회하는 Transaction B는 학생의 전공을 컴퓨터 공학이라는 잘못된 정보를 얻게 됩니다.
- 데이터를 조회할 때 발생 할 수 있습니다.
이렇게 commit되지 않은 정보를 가져올 수 있기 때문에 데이터의 일관성을 유지하지 못 하는 문제가 있을 수 있습니다.
그러나 이는 최고 수준의 동시성을 제공하며 특정 상황에서 유용할 수 있습니다. 예를 들어 Transaction B는 직원 데이터의 현재 값을 확인해야하고, Transaction A가 직원 데이터를 변경하고 있을 때, Transaction B는 Transaction A의 커밋되지 않은 데이터를 볼 수 있습니다. 커밋되지 않은 데이터를 봐야하는 경우에 READ_UNCOMMITED Isolation Level을 사용할 수 있습니다.
따라서 READ_UNCOMMITTED Isolation Level을 사용할 때 데이터 일관성 측면을 고려하여 이점을 유의하여 사용해야합니다.
아래는 READ_UNCOMMITTED 값을 설정하는 방법입니다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void log(String message) {
// ...
}
3. READ_COMMITTED Isolation
READ_COMMITTED Isolation Level은 Dirty read 문제를 방지합니다. 이 격리 수준의 트랜잭션은 행을 읽기 전에 다른 Transaction의 커밋이 될 때까지 기다려야 합니다. 이렇게 하면 트랜잭션이 다른 트랜잭션에 의해 변경중인 데이터를 읽지 못하게 되어 dirty read를 문제가 발생하지 않습니다. 그러나 다른 트랜잭션에서 커밋한 데이터를 조회할 수 있으므로 이 역시 트랜잭션 내에서 일관된 조회를 보장하지 않습니다. 이를 Non-Repeatable Read 현상이라고 합니다.
그렇다면 Nonrepeatable Read는 무슨 현상일까요?
Non-Repeatable Read는 한 트랜잭션에서 같은 쿼리를 2번 이상 조회했을 때 다른 결과가 나타남을 의미합니다.
- 위 그림을 보게되면 동일한 쿼리에 대해서 첫번째는 컴퓨터 공학, 두번째는 기계공학이라는 다른 결과가 조회되는 현상이 발생합니다
- 이 현상은 데이터를 변경할 때 발생할 수 있습니다.
따라서 다른 Transaction이 commit될때 까지 기다린다고 하더라도 데이터가 중간에 바뀌어 버리면, 동일한 쿼리에서 다른 결과가 나오는 현상이 발생할 수 있는것입니다. 아울러 Phantom Read 현상이 발생할 가능성도 공존합니다.
아래는 READ_COMMITTED 값을 설정하는 방법입니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void log(String message){
// ...
}
4. REPEATABLE_READ Isolation
이 격리 수준에서의 트랜잭션은 읽어야 할 레코드가 다른 트랜잭션에 의해서 변경되거나 삭제되는 중이라면 데이터 변경 이력에 언두 로그를 이용해서 레코드를 읽습니다. 따라서 다른 트랜잭션에서 Update를 하더라도 해당 트랜잭션이 실행되기 이전의 이력인 언두 로그의 값을 읽어오므로 Non-Repeatable Read 현상을 방지할 수 있습니다.
그러나 Phantom Read 현상에 대해서는 발생할 수 있습니다.
Phantom Read는 한 트랜잭션에서 같은 쿼리로 2번 이상 조회했을 때 없던 결과가 조회되는 현상을 의미합니다.
- 위 그림을 보게되면 Transaction A에서 전공이 컴퓨터 공학인 학생의 이름을 조회할 때 김서버만 조회되었지만, Transaction B 작업이 이뤄지고 나서 다시 전공이 컴퓨터 공학인 학생을 조회 했을때, 김서버와 지버깅이 조회 될 수 있습니다.
- 데이터를 추가하거나 삭제할 때 발생할 수 있습니다.
아래는 REPEATABLE_READ 값을 설정하는 방법입니다.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void log(String message){
// ...
}
5. SERIALIZABLE Isolation
이 격리 수준은 가장 높은 격리 수준을 나타냅니다. 이 격리는 동시에 여러 트랜잭션이 실행되더라도 순차적으로 진행시킵니다. SERIALIZABLE은 여러 트랜잭션이 동일한 레코드에 동시 접근할 수 없으므로, 위에 언급한 Dirty read, Nonrepeatable read, Phantom Read의 문제를 방지할 수 있습니다.
하지만 동시 호출을 순차적으로 실행하기 때문에 동시 접속률이 가장 낮을 수 있습니다.
아래는 SERIALIZABLE 값을 설정하는 방법입니다.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void log(String message){
// ...
}
이렇게 @Transactional의 Isolation Level을 설정할 수 있으며, 기능에 따라서 동시성과 일관성 중 어떤것을 더 중요시하는지에 따라서 Isolation level을 설정하면 목적에 맞게 Transaction을 사용할 수 있습니다.
각 옵션에 따라서 동시성과 격리성은 아래의 그림과 같은 Trade-off를 가지고 있습니다.
동시성이 높을수록 격리 수준이 낮아져 다른 Transaction의 영향을 많이 받고, 격리성이 높을수록 동시에 실행되지 않고 지연이 발생하게 됩니다.
이렇게 @Transactional의 Propagation Model과 Isolation Level에 대해서 알아봤습니다.
혹시라도 정정할 내용이나 추가적으로 필요하신 정보가 있다면 댓글 남겨주시면 감사하겠습니다.
오늘도 Jindory 블로그에 방문해주셔서 감사합니다.
[ 참고 ]
https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html
https://colevelup.tistory.com/34
https://dh37789.github.io/db/Transaction-003/
https://www.baeldung.com/spring-transactional-propagation-isolation
'개발 > Spring' 카테고리의 다른 글
[Spring] Controller에서 Exception 처리(@ExceptionnHandler와 @ControllerAdvice) (0) | 2024.05.28 |
---|---|
[Spring] Component Scan이란? (2) | 2024.05.27 |
[Spring] Spring 초기 데이터베이스 데이터 설정 방법 (0) | 2022.07.03 |
[Spring] 의존성 주입의 정의 및 의존성 주입 3가지 방식 (생성자 주입, 수정자 주입, 필드 주입) (0) | 2022.04.30 |
[Spring Boot] Thymeleaf 사용하기 (0) | 2022.03.06 |