Post

02 동시성 문제

02 동시성 문제

1. Note

  • 1개 세션에 여러개 커넥션을 가질 수 있음
    • 세션(Session)은 사용자 또는 애플리케이션과 DB 사이의 “논리적인 대화 단위”
    • 커넥션(Connection): 실제 DB 서버와 맺는 “물리적인 연결”
    • 1개의 세션이 여러개의 커넥션을 가질 수 있음
  • 스프링에서는 커넥션풀이 “한개의 세션작업에서는 한개의 커넥션으로 쓰게 만듬”
    • 단, 해당 작업에 한정해서 한개의 커넥션을 쓰게함!
    • 그리고 다른 작업에서는 새로운 세션이 열리고, 기존에 끝난 커넥션을 가질 수 있음!
  • 동시성은 여러가지를 고려해야함
    • 극단적으로 보면 새벽에 배치만 돌리고, 대민사업으로 리드만 하는 경우라면 굳이 동시성 문제 고려 X
    • 어떤 상황에 어떤 수준으로 어떤걸 포기해야하는지에 대한 생각에 대한 문제
    • 시스템에서 문제가 있는 부분과 문제가 없는 부분은 무엇인가?

2. 동시성 문제

1. 동시성

  • 여러 스레드(또는 프로세스)가 동시에 같은 데이터를 읽고/수정할 때 “실행 순서에 따라 결과가 달라지는 문제”

2. 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 은행 계좌 잔액이 10,000원이라고 가정할때,
- 스레드 A: 5,000원 출금
- 스레드 B: 5,000원 출금

2. 정상 적인 흐름
- A → 5,000원
- B → 0원

3. 동시 실행 상황
- A가 잔액 10,000 읽음
- B도 잔액 10,000 읽음
- A가 5,000으로 수정
- B도 5,000으로 수정

4.최종 예상
- 5,000원이 남아야 하는데, 10,000 → 5,000으로 덮어씀
- 즉, 한 번의 출금이 “사라진 것처럼” 보임

3. 원인

  • 동시성 문제는 여러 스레드나 트랜잭션이 같은 공유 자원을 동시에 사용할 때 발생
    • 하나의 작업이 읽기, 수정, 쓰기로 나뉘어 실행되는 동안 실행 순서가 섞이면서 문제가 생김
    • 같은 데이터를 누군가는 쓰고 / 누군가는 읽고 / 누군가는 수정
    • 그 결과 한 작업의 변경이 다른 작업에 의해 덮이거나, 중간 값을 기준으로 잘못 계산될 수 있음
  • 원자성이 보장되지 않으면 이런 문제가 더 쉽게 발생

3. ACID

1. Atomicity (원자성)

  • 개념
    • 트랜잭션 안에서 실행되는 여러 작업은 하나의 묶음처럼 처리되어야 하며,
    • 모든 작업이 성공하면 커밋되고 하나라도 실패하면 전체가 롤백되어야함
    • 중간 단계가 남게 되면 돈이 사라지거나 중복되는 심각한 데이터 오류가 발생할 수 있기 때문에 반드시 보장되어야함
  • 예시
    • 은행 송금에서는 A 계좌에서 돈을 출금하고 B 계좌에 입금하는 두 작업이 함께 수행됨
    • 이때 중간에 오류가 발생하면 출금과 입금이 모두 취소되어야 데이터가 맞게 유지됨
  • 문제 유형
    • Partial Update (부분 업데이트) : 하나의 작업이 여러 단계로 나뉘어 있는데, 그중 일부만 DB에 반영된 상태
    • Inconsistent State (중간 상태) : DB 전체 관점에서 데이터 정합성이 깨진 상태

2. Consistency (일관성)

  • 개념
    • 트랜잭션이 시작되기 전과 끝난 후 모두 데이터베이스의 규칙과 제약 조건이 항상 유지되어야 한다는 성질
    • 데이터가 비정상적인 상태로 저장되는 것을 막아 전체 시스템의 신뢰성을 유지함
  • 예시
    • 잔액은 0보다 작을 수 없고, 주문 데이터는 반드시 존재하는 상품 ID를 참조해야 함
    • 이러한 규칙은 트랜잭션이 끝난 이후에도 반드시 지켜져야 함
  • 문제 유형 이름
    • Constraint Violation (제약조건 위반) : DB에 정의된 “규칙(제약조건)”을 어긴 상태
    • Data Corruption (논리적 불일치) : 데이터 자체는 저장되지만, 비즈니스 의미가 깨진 상태

3. Isolation (격리성)

  • 개념
    • 여러 트랜잭션이 동시에 실행되더라도 서로의 작업 결과에 영향을 주지 않고 독립적으로 실행되는 것처럼 동작해야함
    • 동시성 환경에서 데이터 충돌을 방지하고, 예측 가능한 결과를 보장하기 위해 필요함
  • 예시
    • 두 사용자가 동시에 동일한 재고 1개 상품을 구매하는 상황에서,
    • 각각이 재고가 있다고 판단하고 동시에 구매를 진행하면 재고가 음수가 되는 문제가 발생할 수 있음
  • 문제 유형
    • Dirty Read
    • Non-Repeatable Read
    • Phantom Read
    • Lost Update

4. Durability (지속성)

  • 개념
    • 트랜잭션이 성공적으로 커밋된 이후에는 시스템 장애나 서버 다운이 발생하더라도 해당 결과는 영구적으로 저장되어야 함
    • 시스템 장애 상황에서도 중요한 데이터가 유실되지 않도록 보장하여 신뢰성을 유지함
  • 예시
    • 결제가 완료된 직후 서버가 다운되더라도, 결제 데이터는 DB에 안전하게 남아 있음
  • 문제유형
    • Data Loss (데이터 유실) : 트랜잭션이 “커밋됐다고 생각했는데” 데이터가 사라지는 문제
    • Crash Recovery Failure : 시스템이 장애 후 복구 과정에서 데이터 상태를 제대로 복원하지 못하는 문제

3. AICD - 격리성 레벨

1. READ UNCOMMITTED (Lv0)

  • 개념
    • 다른 트랜잭션이 아직 커밋하지 않은 데이터까지 읽을 수 있는 가장 낮은 격리 수준
    • 확정되지 않은 중간 데이터까지 그대로 보이게 됨
    • 데이터 정합성보다 성능과 동시성을 최우선으로 하기 때문에 락을 거의 사용하지 않음
    • 그 결과 Dirty Read(커밋되지 않은 데이터 읽기)가 발생할 수 있음
  • 동작 방식
    • 트랜잭션이 다른 트랜잭션의 “미커밋 데이터”까지 그대로 읽음
    • 락을 거의 사용하지 않거나 매우 약하게 사용
    • 쓰기 중인 데이터도 읽을 수 있어도 막지 않음
  • 장/단점
    • 장점
      • 락을 최소화하기 때문에 성능이 가장 빠르고,
      • 대량 조회나 분석처럼 정확도가 크게 중요하지 않은 경우에 유리함
    • 단점
      • 아직 확정되지 않은 데이터를 읽을 수 있어서 실제 DB 상태와 다른 결과가 나올 수 있음
      • 서비스 로직에서는 거의 사용되지 않음

2. READ COMMITTED (Lv1)

  • 개념
    • 커밋이 완료된 데이터만 읽을 수 있는 격리 수준
    • 대부분의 DB와 Spring에서 기본으로 사용하는 수준
    • Dirty Read는 방지되지만, 같은 데이터를 같은 트랜잭션 안에서 여러 번 조회하면 결과가 달라질 수 있음
    • 읽는 시점에 따라 값이 바뀔 수 있음
  • 동작 방식
    • SELECT 시점에 “커밋된 데이터만” 읽음
    • 쓰기 중인 데이터는 읽지 못하게 잠깐 막음
    • 하지만 읽을 때마다 최신 커밋 데이터를 다시 가져옴
  • 장/단점
    • 성능과 안정성의 균형이 가장 좋아서 일반적인 웹 서비스에서 가장 널리 사용됨
    • 조회 시점마다 결과가 달라질 수 있어, 완전히 고정된 데이터를 기대하면 문제가 될 수 있음

3. REPEATABLE READ (Lv2)

  • 개념
    • 한 트랜잭션이 시작된 이후에는 같은 데이터를 여러 번 읽어도 항상 동일한 결과를 보장하는 격리 수준
    • 트랜잭션 시작 시점의 데이터를 기준으로 읽기 때문에 중간에 다른 트랜잭션이 데이터를 변경해도 영향을 받지 않음
    • Non-repeatable Read는 방지되지만, 새로운 데이터가 “끼어드는” Phantom Read는 DB에 따라 발생할 수 있음
  • 동작 방식
    • 트랜잭션 시작 시점의 “스냅샷 데이터”를 기준으로 읽음
    • 이후 다른 트랜잭션이 데이터를 바꿔도 내 트랜잭션에서는 안 보임
    • 보통 MVCC(버전 관리) 방식으로 구현
  • 장단점
    • 읽기 일관성이 매우 높아서, 보고서 생성이나 정합성이 중요한 조회 작업에 적합
    • 동시성이 줄어들고, 내부적으로 락이나 MVCC 비용이 증가하여 성능 부담이 생길 수 있음

4. SERIALIZABLE (Lv3)

  • 개념
    • 가장 강한 격리 수준으로, 모든 트랜잭션이 마치 하나씩 순서대로 실행된 것처럼 처리됨
    • 동시에 실행되더라도 결과적으로는 완전히 순차 실행된 것처럼 보이게 만들기 때문에
    • Dirty Read, Non-repeatable Read, Phantom Read까지 모두 방지
  • 동작 방식
    • 모든 트랜잭션을 “완전히 순서대로 실행된 것처럼” 처리
    • 읽기/쓰기 모두 강하게 락을 검
    • 범위까지 잠그는 경우가 많아 Phantom Read도 막음
  • 장단점
    • 데이터 정확성과 일관성이 가장 높아서 금융처럼 절대 오류가 나면 안 되는 시스템에서 사용
    • 동시 처리가 거의 불가능해지고 성능이 크게 떨어지며, 트랜잭션 충돌이 자주 발생할 수 있음

4. AICD - 격리성 문제

1. 전체 분류

격리 수준Dirty ReadNon-Repeatable ReadPhantom ReadLost Update
READ UNCOMMITTEDOOOO
READ COMMITTEDXOOO
REPEATABLE READXXO*O
SERIALIZABLEXXXX

2. Dirty Read (더티 리드)

  • 개념
    • 아직 확정되지 않은 데이터를 읽어버려 잘못된 판단을 하게 되는 문제
    • Dirty Read는 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 현상
    • 즉, 확정되지 않은 “중간 상태 데이터”를 읽어버리는 문제
  • 예시
    1
    2
    3
    4
    5
    6
    7
    
    금액 조회에서 
      
    트랜잭션 A: 사용자 잔액을 10,000원 → 5,000원으로 변경 (아직 커밋 전)
    트랜잭션 B: 해당 사용자의 잔액을 조회 → 5,000원으로 읽음
    이후 트랜잭션 A가 실패하여 롤백됨 (실제 잔액은 10,000원 유지)
      
    -> 트랜잭션 B는 존재하지 않는 5,000원이라는 데이터를 읽게 됨
    
  • 해결
    • READ COMMITTED 이상을 사용하여 Dirty Read를 방지 가능
    • 이 수준부터는 커밋이 완료된 데이터만 읽을 수 있기 때문에,
    • 아직 커밋되지 않은 중간 상태 데이터는 조회되지 않음

3. Non-Repeatable Read (반복 불가능 읽기)

  • 개념
    • Non-Repeatable Read는 같은 트랜잭션 안에서 같은 데이터를 다시 조회했을 때 값이 달라지는 현상
    • 같은 트랜잭션 안에서 같은 데이터를 다시 조회했을 때 값이 달라지는 문제
  • 예시
    1
    2
    3
    4
    5
    6
    7
    
    주문 시스템에서
    
    트랜잭션 A: 주문 상태를 조회 → “결제 대기”
    트랜잭션 B: 해당 주문 상태를 “결제 완료”로 변경 후 커밋
    트랜잭션 A: 같은 주문을 다시 조회 → “결제 완료”
    
    -> 같은 트랜잭션 안에서 조회 결과가 달라지게 됨
    
  • 해결
    • REPEATABLE READ 이상을 사용하여 Non-Repeatable Read를 방지
    • 트랜잭션 시작 시점의 데이터를 기준으로 조회되기 때문에,
    • 다른 트랜잭션이 값을 변경하고 커밋하더라도 현재 트랜잭션에서는 변경된 값이 보이지 않음

4. Phantom Read (팬텀 리드)

  • 개념
    • 같은 조건으로 데이터를 다시 조회했을 때 결과 행의 개수가 달라지는 현상
    • 기존 데이터가 바뀌는 것이 아니라 새로운 데이터가 추가되거나 삭제되어 조회 결과 자체가 달라지는 문제
  • 예시
    1
    2
    3
    4
    5
    6
    7
    
    주문 시스템에서
    
    트랜잭션 A: “결제 금액 10,000원 이상 주문” 조회 → 3건
    트랜잭션 B: 새로운 주문(15,000원) 추가 후 커밋
    트랜잭션 A: 같은 조건으로 다시 조회 → 4건
    
    -> 같은 조건인데 조회 결과 행 수가 달라지게 됨
    
  • 해결
    • SERIALIZABLE 수준을 사용하여 Phantom Read를 방지
    • 범위 자체를 잠그거나 완전히 직렬 실행처럼 처리하기 때문에,
    • 다른 트랜잭션이 새로운 데이터를 추가하더라도 현재 트랜잭션의 조회 결과에는 영향을 주지 않음
This post is licensed under CC BY 4.0 by the author.