Introduce
결제 시스템을 구축하면서 동시성 문제를 경험한 적이 있다. 여러 사용자가 동시에 결제를 시도할 때, 예상하지 못한 데이터 불일치와 정합성 문제가 발생했으며, 이를 해결하기 위해 다양한 접근 방식을 고민해야 했다.
이러한 동시성 문제는 단순히 결제 시스템에서만 발생하는 것이 아니다. 데이터베이스에서 트랜잭션이 동시에 실행될 때, 공유 자원의 일관성과 무결성이 깨지는 문제는 모든 시스템에서 중요한 고려 사항일 것이다.
동시성이란 여러 주체가 하나의 공유 자원에 동시에 접근하여 의도하지 않은 결과를 발생시키는 것을 의미하며, 식사하는 철학자 문제가 유명한 예시이다. 즉, 데이터베이스에서 동시성 문제란 여러개의 트랜잭션이 동시에 실행되어, 공유 자원의 일관성과 무결성을 깨뜨리는 문제를 말한다.
본 글에서는 결제 시스템에서 동시성 문제를 해결해 나가는 과정에 앞서, 트랜잭션의 특징(ACID), 동시성 문제, 격리 수준 등 데이터베이스 단에서 반드시 이해하고 있어야 할 개념들을 먼저 정리하고자 한다.
Transaction
📌 Concept
더 이상 분할이 불가능한 업무 처리의 단위
데이터의 일관성과 무결성을 보장하기 위해 수행된다.
예를 들어, A가 B에게 10만 원을 자동 이체하는 경우, 이 과정은 더 이상 분할될 수 없는 1개의 트랜잭션이 된다.
- A 계좌에서 10만원 출금
- B 계좌에 10만원 입금
만약 입금 과정에서 오류가 발생했다면 해당 거래는 성립되지 않는다. 이처럼 A가 돈을 출금하는 행위와 B에게 돈을 입금하는 행위는 별개로 분리될 수 없으며 1개의 거래로 처리되어야 한다. 이러한 거래의 최소 단위를 트랜잭션이라고 한다. 트랜잭션은 모든 작업이 완료된다면 커밋(반영)을 하고, 오류가 발생할 경우 롤백(되돌리기)을 한다.
📌 트랜잭션 ACID
트랜잭션은 일반적으로 ACID 원칙을 따르고 있다.
- Atomicity(원자성)
- 트랜잭션은 모두 수행되거나, 아무것도 수행되지 않아야 한다.
- 1개의 작업이라도 실패할 경우 전체가 롤백된다.
- Consistency(일관성)
- 트랜잭션의 실행 전과 후에 데이터베이스가 항상 일관된 상태를 유지되어야 한다.
- ex) 송금은 한 쪽 계좌에서 돈이 출금 되었다면 반드시 다른 계좌에는 입금되어야 한다.
- Isolation(격리성)
- 동시에 여러 트랜잭션이 실행될 때, 서로의 작업이 영향을 미치지 않고 독립적으로 수행되어야 한다.
- Durability(지속성)
- 트랜잭션이 성공적으로 수행된 후에 결과는 영구적으로 저장되어야 한다.
- 시스템 장애가 발생하더라도 변경된 데이터는 유지되어야 한다.
그 중에서도 실제 서비스에서 가장 많이 발생할 수 있는 문제가 Isolation(격리성) 이다. 이는 트랜잭션의 격리 수준을 제어하는 것으로 문제를 해결할 수 있다.
Isolation
여러 개의 트랜잭션이 동시에 처리될 때, 서로 간섭하지 않도록 보장하는 정도를 결정하는 것이다.
트랜잭션이 동시에 실행될 때 어떠한 문제가 발생할 수 있을까?
📌 이상 현상
1. Dirty Read
1. Tx 2가 계좌 잔액(X) 중 200원을 송금한다. (500 → 300)
2. TX 1은 변경된 300원을 읽고, 잔액 1000원을 추가하고 커밋한다. (300 → 1300)
3. TX 2에서 오류가 발생하여 Rollback 한다. (원래 데이터인 500으로 복구)
4. 최종 계좌 금액이 500원이 된다. → 데이터 정합성이 깨짐
특정 트랜잭션이 아직 commit되지 않은 데이터를 읽는 현상이다. 만약 해당 트랜잭션이 Rollback되면, 특정 트랜잭션은 잘못된 데이터를 사용하게 된다.
2. Non-Repeatable
- Tx 1이 계좌 잔액을 조회한다. (500원)
- TX 2가 200원을 송금한다. (500원 → 300원)
- Tx 1이 다시 계좌를 조회한다. (300원) → 처음과 다른 결과가 조회됨
한 트랜잭션에서 같은 데이터를 2번 조회했을 때, 다른 트랜잭션이 데이터를 변경하여 결과가 달라지는 현상이다. 트랜잭션 내에서 동일한 데이터 조회 결과가 일관되지 않으면, 연산 오류 발생 가능성이 생긴다.
3. Phantom Read
- Tx 1은 v가 10인 데이터를 조회한다. (t1)
- Tx 2는 t2의 v를 10으로 변경한다. (t2(…,v=10))
- Tx 1은 v가 10인 데이터를 다시 조회한다. (t1, t2) → 새로운 데이터가 추가됨
한 트랜잭션에서 같은 조건으로 데이터를 조회했을 때, 다른 트랜잭션이 새로운 데이터를 추가하거나 삭제하여 조회 결과가 달라지는 현상이다. 쿼리 결과가 예상과 다르게 달라질 수 있으며, 특히 집계 연산시 문제가 발생할 수 있다.
이처럼 트랜잭션이 동시에 실행될 때 여러가지 문제가 발생할 가능성이 있다. 그렇기에 데이터의 일관성을 보장하고, 여러가지 동시성 문제를 방자히기 위해서는 Isolation을 지켜야한다. 따라서 Isolotaion의 정도를 조정하면 동시성 문제를 방지하고, 데이터 정합성을 보장할 수 있는데 이 것을 Isoltaion Level (격리 수준) 이라고 한다.
📌 Isolation Level
격리 수준이 낮은 순서대로 정리해보자면 READ UNCOMMUTED (커밋되지 않은 읽기) < READ COMMITED (커밋된 읽기) < REAPEATABLE READ(반복 가능한 읽기) < SERIALIZABLE (직렬화) 순서이다.
- READ UNCOMMITTED (커밋되지 않은 읽기)
- 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있다.
- Dirty Read가 발생 가능하다.
- READ COMMITTED (커밋된 읽기)
- 커밋된 데이터만 읽을 수 있다.
- Non-Repeatbale Read가 발생 가능 하다.
- REPEATABLE READ (반복 가능한 읽기)
- 동일 트랜잭션 내에서 같은 데이터를 여러번 조회해도 결과가 항상 같다.
- Phantom Read가 발생 가능하다.
- SERIALIZABLE (직렬화)
- 가장 엄격한 수준으로, 어떠한 동시성 문제도 발생하지 않는다.
- 성능이 가장 안좋다.
격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
Read Uncommitted | O | O | O |
Read Committed | X | O | O |
Repeatable Read | X | X | O |
Serializable | X | X | X |
일반적으로 격리 수준이 낮다면 성능은 좋아지지만, 데이터 정합성이 깨질 위험이 커지고, 반대로 격리 수준이 높다면 데이터 정합성은 보장되지만 성능은 저하될 수 있다.
Serializable 의 경우 어떠한 동시성 문제도 발생하지 않지만, 그만큼 성능 저하가 발생한다는 문제가 있다. 따라서 실무에서는 필요한 만큼의 격리 수준을 적용하는데 보통 Read Committed 또는 Repeatable Read를 가장 많이 사용한다고 한다.
스프링의 격리수준?
스프링 프레임워크의 Transactional 은 어떠한 격리 수준을 사용하고 있을까?

@Transactional 의 옵션인 isolation의 기본값은 Isolation.DEFAULT 라고 한다.
그렇다면 Isolation.DEFAULT 는 무엇일까?

solation은 TransactionDefinition 인터페이스에 해당하는 Transactional 어노테이션과 함계 사용되는 격리 수준을 나타내는 enum이다. DEFAUL 의 javaDoc 을 읽어보면 기본 데이터 저장소의 기본 격리 수준 을 사용한다는 것을 알 수 있다.
즉, 스프링 프레임워크는 독립적으로 격리수준을 설정하는게 아닌 내가 선택한 데이터베이스의 기본 격리값을 사용한다는 것을 알 수 있다.
나는 현재 Mysql InnoDB를 사용중이다. 그렇다면 MySQL의 기본 격리수준이 적용되어있을 것이다.

MySQL 8.0 버전에 따르면 기본 격리 수준은 REPEATABLE_READ 라고 한다.
'Computer Science > Database' 카테고리의 다른 글
[DB] Lock을 사용하여 동시성 제어하기 (+ 2PL Protocol) (0) | 2025.03.01 |
---|