Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

채움의 길

[DeepDive] 동시성 제어 기법을 알아보자 본문

지식 채우기/동아리

[DeepDive] 동시성 제어 기법을 알아보자

chae-um 2025. 9. 27. 14:20

RDBMS 동시성 제어의 정석

동시성 제어 : 여러 사용자가 동시에 데이터베이스에 접근할 때 데이터의 일관성과 무결성을 유지하기 위한 중요한 개념

1. 데이터 경합과 동시성 문제 유형 분석

데이터 경합(Data Contention) : 여러 트랜잭션이 동일한 데이터에 동시에 접근하려 할 때 발생

Critical Section (임계 영역)

  1. 진입 (Entry)
    • 프로세스가 임계 영역에 진입하기 전에 다른 프로세스가 임계 영역을 사용하고 있는지 확인
    • 만약 사용 중이라면 해당 프로세스는 대기
  2. 임계 영역 수행(Execution)
    • 임계 영역이 비어 있음을 확인하면 프로세스는 임계 영역에 진입하여 공유 자원을 사용
    • 이 단계에서 다른 어떤 프로세스도 임계 영역에 진입할 수 없음
  3. 퇴장(Exit)
    • 작업을 마친 프로세스는 임계 영역에서 나오면서 다른 프로세스가 진입할 수 있도록 잠금을 해제
    • 대기 중이던 다른 프로세스가 임계 영역에 접근할 수 있게 됨

→ 이러한 단계를 통해, 여러 프로세스가 동시에 공유 자원에 접근하려 할 때 발생하는 경쟁 조건(Race Condition)을 피하고 데이터의 일관성을 유지

그럼에도 불구하고 Race Condition이 발생하는 이유?

Race Condition (경쟁 조건)

여러 트랜잭션이 공유 자원의 상태를 확인하고 그 상태를 변경하는 작업이 원자적(Atomic)이지 않을 때 발생.

즉, 상태 확인과 상태 변경 사이에 다른 트랜잭션이 끼어들어 작업을 방해

  • 정상적인 플로우가 깨지는 지점
    • '진입 - 수행' 과정에서 "어느 프로세스도 없다고 인지"하는 그 순간에 이미 다른 트랜잭션이 들어올 수 있는 가능성이 있음
    • SELECT 문은 기본적으로 읽기만 할 뿐, 해당 데이터에 대한 잠금을 설정하지 않기 때문
  • 발생하는 문제
    • 여러 트랜잭션이 임계 영역(Critical Section)에 동시에 접근할 때 발생하는 문제 상황을 총칭하는 용어

동시성 문제 유형 분석

  1. 갱신 손실 (Lost Update)
    • 두 트랜잭션이 같은 공유 자원을 읽고 각자 다른 값으로 업데이트 하여, 결과적으로 한 트랜잭션의 변경 내용이 사라지는 문제
    1. 트랜잭션 A가 계좌 잔액($100)을 읽습니다.
    2. 트랜잭션 B도 같은 계좌 잔액($100)을 읽습니다.
    3. 트랜잭션 A가 $50을 입금하여 잔액을 $150으로 업데이트합니다.
    4. 트랜잭션 B가 $30을 입금하여 잔액을 $130으로 업데이트합니다.
    → 이 과정에서 3번의 변경 내용이 사라짐
    
  2. 비현실적인 읽기 (Unrepeatable Read)
    • 한 트랜잭션이 동일한 데이터를 여러 번 읽을 때, 그 사이에 다른 트랜잭션이 데이터를 수정하고 커밋하여 두 번의 읽기 결과가 달라지는 현상 ⇒ "반복 가능한 읽기"가 보장되지 않는다는 말과 같음
    1. 트랜잭션 A가 `book_id=101`인 책의 가격을 $100으로 읽습니다.
    2. 트랜잭션 B가 같은 책의 가격을 $120으로 업데이트하고 커밋합니다.
    3. 트랜잭션 A가 다시 `book_id=101`인 책의 가격을 읽으면, 이제 $120으로 나옵니다.
    → 트랜잭션 A는 하나의 트랜잭션 내에서 같은 데이터를 읽었음에도, 값이 일치하지 않는 비일관적인 현상이 발생함
    
  3. 더티 읽기 (Dirty Read)
    • 한 트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽는 문제
    • 만일 다른 트랜잭션이 롤백될 경우, 읽은 데이터는 실제로는 존재하지 않는 Dirty Data가 됨
    1. 트랜잭션 A가 계좌 잔액($100)을 $200으로 업데이트하지만, 아직 커밋하지 않습니다.
    2. 트랜잭션 B가 계좌 잔액을 읽어 $200이라고 인식합니다.
    3. 트랜잭션 A가 어떤 이유로 롤백되어 변경 사항이 취소됩니다.
    4. 트랜잭션 B는 존재하지 않는 $200이라는 잘못된 데이터를 기반으로 작업을 계속하게 됩니다.
    
  4. 유령 읽기 (Phantom Read)
    • 한 트랜잭션이 특정 조건으로 데이터를 조회했을 때, 그 사이에 다른 트랜잭션이 조건에 맞는 새로운 레코드(행)를 삽입하여 다음 조회 시 이전에 없던 '유령' 레코드가 나타나는 문제
    • 읽기 작업만 하는 트랜잭션이 여러 번 조회할 경우, 결과 집합이 변경되어 예측할 수 없는 상황이 벌어지는 것 → 한 트랜잭션 내에서 일관성이 깨지는 현상이 핵심
    • 트래잭션 원칙 중 하나인 ‘고립성’이 성립하지 않게 됨.
      • 고립성 : 하나의 트랜잭션이 진행되는 동안 다른 트랜잭션의 영향을 받지 않고 고립되어야 함
    • 유령 읽기는 기존 레코드가 수정되는 비현실적인 읽기(Unrepeatable Read)와 달리 새로운 레코드가 삽입되어 조회 결과의 집합 자체가 바뀌는 문제

2. 트랜잭션과 잠금(공유/배타 락)의 기본 원리

공유 락과 배타 락

  • 공유 락(Shared Lock, S-Lock)
    • 읽기 작업을 위한 잠금
    • 핵심 원리는 데이터를 변경하지 않는 읽기 작업은 서로에게 영향을 주지 않는다는 점에 기반
    • 작동 방식
      • 여러 트랜잭션의 동시 접근 허용 특정 데이터에 공유 락이 걸려 있으면 다른 트랜잭션들도 자유롭게 같은 데이터에 공유 락을 걸고 읽기 작업을 할 수 있음
      • 배타 락과의 충돌 하지만 어떤 트랜잭션이 이미 공유 락이 걸린 데이터를 변경하려고 하면(배타 락을 걸려고 하면) 그 트랜잭션은 대기해야 함. 이는 공유 락이 데이터의 일관된 상태를 유지해주기 때문 → 읽고 있는 동안 데이터가 변경되지 않도록 보호하는 역할
    사용 예시 - 비현실적인 읽기(Unrepeatable Read) 방지
    
    트랜잭션 A가 특정 상품의 재고를 조회한 후, 다음 작업으로 넘어가려고 합니다. 
    그런데 그 사이에 트랜잭션 B가 재고를 수정하고 커밋해버리면, 트랜잭션 A는 일관성 없는 데이터를 보게 됩니다. 
    이런 상황을 막기 위해, 트랜잭션 A가 `SELECT`를 할 때 공유 락을 걸어놓으면, 트랜잭션 B의 `UPDATE` 작업이 대기하게 되어 A는 안정적으로 작업을 수행할 수 있습니다.
    
  • 배타 락(Exclusive Lock, X-Lock)
    • 쓰기(변경) 작업을 위한 잠금
    • 데이터를 변경하는 작업은 단독으로 이루어져야 한다는 원칙에 기반
    • 작동 방식
      • 단 하나의 트랜잭션만 허용 배타 락이 걸린 데이터에는 다른 어떤 종류의 락(공유 락, 배타 락 등)도 걸 수 없음. 데이터를 수정하는 동안 다른 트랜잭션이 읽거나 수정할 수 없도록 완전히 격리
      • 갱신 손실 방지 갱신 손실(Lost Update)과 같은 동시성 문제를 해결하는 데 필수. 여러 트랜잭션이 동시에 데이터를 수정하려 할 때, 가장 먼저 배타 락을 획득한 트랜잭션이 안전하게 작업을 수행하고 나머지는 대기
    사용 예시 - 갱신 손실(Lost Update) 방지
    
    두 트랜잭션이 거의 동시에 계좌 잔액을 업데이트하려 합니다.
    1. 트랜잭션 A가 `UPDATE`를 시작하며 잔액 데이터에 배타 락을 겁니다.
    2. 이때 트랜잭션 B가 같은 데이터에 `UPDATE`를 시도하지만, A가 건 배타 락 때문에 대기하게 됩니다.
    3. 트랜잭션 A가 작업을 마친 후 락을 해제하면, B가 락을 획득하고 작업을 시작합니다.
    
    이러한 과정을 통해 A의 변경 사항이 B에 의해 덮어씌워지는 것을 막을 수 있습니다.
    

ACID

  1. 원자성 (Atomicity)
    • 트랜잭션 내의 모든 작업이 완전히 실행되거나, 아예 실행되지 않거나 둘 중 하나만 보장하는 속성
    • 구현 방법 : 로그 파일
  2. 일관성 (Consistency)
    • 트랜잭션이 성공적으로 완료되면 데이터베이스가 항상 논리적으로 일관된 상태를 유지해야 한다는 속성
    • 구현 방법 : 트리거, 제약 조건
  3. 고립성 (Isolation)
    • 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션이 다른 트랜잭션의 작업에 간섭받지 않고 독립적으로 실행되어야 한다는 속성
    • 구현 방법 : 잠금, 다중 버전 동시성 제어(MVCC)
  4. 지속성 (Durability)
    • 트랜잭션이 성공적으로 커밋된 후에는 시스템에 영구적으로 저장되어, 시스템에 오류가 발생하더라도 데이터가 유실되지 않도록 보장하는 속성
    • 구현 방법 : 트랜잭션 로그 파일, 체크포인트

3. 트랜잭션 격리 수준별 동시성 제어 범위 이해

  • ANSI/ISO SQL 표준은 네 가지 격리 수준을 정의하고 있음
  • 높은 수준으로 갈수록 동시성 문제는 줄어들지만, 성능 저하가 발생할 수 있음
  • 각 수준은 이전 단계에서 해결하지 못한 동시성 문제를 해결하도록 설계

격리 수준 해결하는 문제 발생 가능한 문제

READ UNCOMMITTED - 더티 읽기, 비현실적인 읽기, 유령 읽기
READ COMMITTED 더티 읽기 비현실적인 읽기, 유령 읽기
REPEATABLE READ 더티 읽기, 비현실적인 읽기 유령 읽기
SERIALIZABLE 더티 읽기, 비현실적인 읽기, 유령 읽기 -
  • READ UNCOMMITTED가 해결하는 문제가 하나도 없는데도 존재하는 이유
    • 동시성 문제를 해결하는 것이 아니라, 동시성 문제를 허용하는 대신 최고의 성능을 추구하는 방식 → 데이터의 일관성이라는 중요한 트랜잭션 속성을 포기하고 성능과 속도를 택한 방식
    • 금융 거래 시스템처럼 데이터의 정확성이 매우 중요한 경우에는 절대 사용해서는 안 됩
    • 예시 - 데이터의 정확성보다 속도가 훨씬 중요한 상황에서 사용
      • 웹 서버 로그 분석 : 수많은 웹 서버의 접속 로그를 실시간으로 집계할 때, 1~2개의 잘못된 데이터가 섞여도 전체적인 트래픽 추이를 파악하는 데는 문제 없음
      • 실시간 통계 및 대시보드 : 주식 시장의 실시간 거래량이나 웹사이트의 방문자 수와 같이 데이터가 초 단위로 계속 갱신되는 경우. 잠시 동안의 부정확한 데이터보다는 최신 상태를 가능한 한 빨리 보여주는 것이 더 중요
      • 대용량 데이터 읽기 : 특정 시점의 정확한 데이터가 필요하지 않은 단순히 대량의 데이터를 빠르게 읽어와 다른 시스템으로 옮기는 ETL(Extract, Transform, Load) 작업에도 활용될 수 있음

4. SELECT ... FOR UPDATE를 이용한 명시적 행 잠금

비관적 락 (Pessimistic Lock)

  • 데이터를 수정하기 전, 충돌이 일어날 것이라 판단하고(비관적) 미리 해당 트랜잭션에 잠금을 걸어두는 방식
    • 장점 : 데이터 충돌을 확실하게 방지하여 데이터 일관성을 보장
    • 단점 : 잠금이 걸린 동안 다른 트랜잭션들이 대기해야 하므로 동시성이 낮아지고 성능 저하 가능성
@Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 락(Exclusive Lock)을 겁니다.
@Query("select p from Product p where p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);
  • @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션을 사용하면 JPA 구현체(Hibernate 등)가 해당 JPQL쿼리를 데이터베이스의 SQL 구문으로 변환할 때 FOR UPDATE 구문을 자동으로 추가
  • 실제 실행되는 쿼리
  • SELECT * FROM product WHERE id = ? FOR UPDATE;

@Lock 어노테이션의 역할

  • JPA를 통해 선언적으로 잠금을 제어할 수 있게 해주는 기능
    • @Lock(LockModeType.PESSIMISTIC_WRITE)
      • PESSIMISTIC_WRITE는 비관적 락의 쓰기 모드
      • SELECT ... 쿼리 뒤에 데이터베이스 방언에 맞는 배타적 잠금(Exclusive Lock) 구문을 추가. 대부분의 경우 이는 FOR UPDATE로 변환됨
    • @Lock(LockModeType.PESSIMISTIC_READ)
      • PESSIMISTIC_READ는 비관적 락의 읽기 모드
      • SELECT ... FOR SHARE와 같이 공유 잠금(Shared Lock)을 추가하여 다른 트랜잭션의 변경을 막음