Article/트랜잭션 완전정복

[트랜잭션 완전정복] 1편 - 진정한 의미의 트랜잭션(Transaction)이란?

조금씩 차근차근 2025. 5. 3. 21:47

습관적으로 우리가 사용하는 DB 트랜잭션이라는 용어가 아닌, WAS가 작업하는 단위인 "트랜잭션" 그 자체의 정의와 동작 방식에 집중하여, 다양한 곳에 적용할 수 있는 기준을 가져보자.

  • 주요 용어
    • 비즈니스 트랜잭션
    • 시스템 트랜잭션
    • 충돌
    • 경쟁 상태
    • 락 경합
    • 데드락
    • 오프라인 트랜잭션

트랜잭션(Transaction)이란?

트랜잭션이란?

  • 클라이언트와 서버 간에, 요청을 주고 응답을 받는 하나의 작업의 단위를 정의한 것.
  • 주어진 요청에 대하여, 서버 내부의 상태를 변화시키거나, 적절한 응답을 반환하는 것을 목표로 한다.
  • 이를 "비즈니스 트랜잭션" 이라고 부르겠다.

DB에서의 트랜잭션

  • 작업의 단위를 보장하려고 하니, 하나의 작업처럼 다루고, 이를 영속성 저장소에 저장할 필요가 있었다.
  • 하지만, 이 비즈니스 트랜잭션을 위한 인프라 로직을 매번 새로 구현하기는 어려웠고, 따라서 이를 RDBMS에서 미리 구현하여 지원하기 시작했다.
  • 이 과정에서 ACID 라는 트랜잭션의 규칙이 정의되었다.
  • 이를 DBMS라는 시스템에서 지원하는, "시스템 트랜잭션" 이라고 부르겠다.

동시성 문제와 데이터 일관성

트랜잭션은 일반적으로 내부의 상태 변화를 다음과 같은 과정으로 구현한다.

    1. 읽기
      • 일단 현재 값이 어떤 상황인지 이해해야 함
    1. 쓰기
      • 기존의 값을 내가 지정한 값으로 대치해야 함

간단한 예시로, 모든 트랜잭션은 아래와 같이, 스케줄에서 Read() 와 Write()만 남길 수 있다.

 

만약 조회만 존재하는 로직이라면, 서로가 서로에게 영향을 줄 수 없을 것이다.

충돌(Conflict)

Conflict란?

서로 다른 트랜잭션 내 작업 간의 간섭을 이야기한다.

(잠시 버전 관리, MVCC 관련 내용을 잊고, 한 데이터가 오직 한곳의 저장공간에만 존재하고, UNDO LOG와 같은 데이터는 없다고 생각해보자.)

충돌의 종류에는, 크게 다음과 같은 세가지가 있다.

  • Read-Write Conflict
    • Unrepeatable Read, Read Skew
    • 첫번째 Read 이후, Write 후에 다시 Read 하면 결과가 달라지는 문제

Read-Write Conflict의 대표적인 예시인 Unrepeatable Read

  • Write-Write Conflict
    • Dirty Write
    • 커밋되지 않은 데이터 사이에 덮어씌우기가 일어나는 것
    • 먼저 실행한 Write 연산이 사라지는 문제가 발생한다.

Write-Write Conflict의 대표적인 예시인 Dirty Write

  • Write-Read Concflict
    • Dirty Read
    • Write 된 내용이 커밋되지 않았는데 읽히면 안된다
    • 앞의 Write 가 롤백되면, 문제가 된다.

Write-Read Conflict의 대표적인 예시인 Dirty Read

참고) Phantom Read

  • 결과 집합 조회의 문제
    • 이전과 다음의 결과에 특정 레코드의 존재 여부가 달라짐
    • 이전에 없던 데이터가 생길 경우
    • 이전에 있던 데이터가 없어질 경우
  • Read-Write Conflict와 유사해 보이나
    • Read-Write Conflict : 데이터 수정의 변화
    • Phantom Read : 데이터 추가/삭제의 변화

Phantom Read.

충돌의 해결 방법 - 어떠한 수단이 있는가?

  • 서로 다른 트랜잭션이 같은 리소스에 애초에 접근하지 못하도록 막을 수 있다.
    • 스냅샷
    • 애초에 충돌이 없는 자료구조 설계(CRDT, OT)
  • 또한, 충돌 동등한 순서를 결정해줌으로써, Conflict를 막을 수 있다.

참고) Lost Update(갱신 손실)

  • Lost Update는 트랜잭션 커밋 시점 사이의 덮어씌우기 문제이다
  • DB에서 Dirty Write를 해결해주더라도, 커밋 시점 사이의 순서에 따라 Update값이 소실되는 위험이 발생할 수 있다.
  • 애플리케이션 로직 상의 문제라고 보는 것이 타당하다.
    • 같은 값 덮어씌우기라도, 예상한 값의 덮어씌움이라면 단순 Update가 되지만
    • 동시에 Update 요청이 들어왔을 때, 이전 값의 소실을 예상하지 못했다면 Lost Update 문제가 된다.
    • 이는 보통 Select For Update 로 해결한다.

쓰기 로직은 일반적인 충돌이 발생하지 않는 환경에서는 문제가 되지 않지만, 우측과 같이 매우 높은 동시성 환경에서는 문제가 발생할 수 있다. 이를 Lost Update라고 한다.

다른 종류의 데이터 오염 - 쓰기 스큐(Write Skew)란?
서로 다른 트랜잭션이 동시에 하나의 데이터를 읽고 수정하면서 일관성이 깨지는 형태의 문제를 이야기한다.
사실상 팬텀(팬텀 리드의 그 팬텀이다.)에 의해 발생하며, Materializing Conflict 혹은 직렬성 스냅샷 격리(SSI)로 해결한다.
가장 유명한 예제로는 "좌석 예매 시스템"이며, 다음 예제에서도 해당 내용을 Materializing Conflict 처럼 도메인에 존재하지 않았던 락 개념을 구체화해 해결한 예시를 확인해볼 수 있다.
쓰기 스큐(Write Skew)란? (작성예정)

  • 리소스와 리소스 사용자 개념을 모델링해보자.
    • 사용자는 리소스를 사용한다.
  • 이때, 다른 사용자가 자신이 사용하고 있는 리소스를 접근하지 못하게 막는 방화벽 개념을 추가해보자.
    • 이를 락(Lock) 이라고 개념짓는다.

락은 경쟁 상태 해소 뿐만이 아니라, DB에서는 Conflict의 해소도 담당할 수 있다.

락 경합/데드락

    • 잠금의 설계 접근 방식에는 크게 두가지 방식이 있다.

Coarse Grained Locking

  • 잠금 리소스의 단위를 크게 잡는 방식을 의미한다.
  • 비유를 하자면, 싱글스레드 환경에서는 동시성 문제가 발생할 일이 없다.
  • 약간 관점을 바꿔 리소스 관점에서 바라보면, 하나의 스레드만 참조 가능하게 하면, 데이터 일관성 문제가 발생할 일이 없다.

그럼 이 방식의 단점은 뭘까?

아래 그림을 참고하자.

Lock Contention의 발생 상황.

T1과 T2는 리소스 r1과 r2를 참조해야 했다. 하지만, 두 트랜잭션이 r1, r2 모두를 동시에 원했고, 이로인해 락 경합(Contention) 이 발생하게 되었다.

이 문제는, Fine-Graind Locking 방식으로 어느정도 해소할 수 있다.

Fine Grained Locking

Fine-Grained Locking이란, 락의 크기를 최대한 작게 잡는 설계 방식은 의미한다.
해당 방식은 Coarse-Grained Locking의 락 경합 발생 가능성을 낮춘다.

하지만 이 방식에도 단점은 존재한다.

기존 하나의 잠금으로 관리하던 r1과 r2 의 잠금이 분리되었고, T1과 T2는 각기 다른 순서로 각 리소스의 잠금을 요구하게 되었다. 그 결과, 예상치 못한 데드락이 발생하였고, 충돌의 해결을 요구하는 상황에 직면하게 되었다.

이런 데드락 문제는 두가지 방식으로 어느정도 해소할 수 있는데, 그 주제는 다음과 같다.

  • Coarse Grained Locking
    • 다시 위에서 설명한 Coarse-Grained Locking 방식으로 되돌아가는것을 의미한다.
    • DDD의 Aggregate 개념은 일종의 Coarse Grained Locking 개념을 차용하여, 코드 관리의 복잡도를 낮추는 판단을 수행했다.
  • 락 순서 보장
    • 코딩 컨벤션으로, “r1과 r2를 동시에 참조하는 트랜잭션은, 반드시 r1을 먼저 참조한 후, r2를 참조하라” 라는 규칙을 추가하는 방법을 의미한다.
    • 하지만 이는, 보이지 않는 코드 규칙을 추가하게 되므로 코드의 관리 복잡도를 높이게 된다.
  • 트랜잭션 쪼개기
    • 만약 T1의 r2 참조가 비동기적으로 실행 가능한경우(즉각적인 응답이 필수적이지 않은 경우), 과감하게 트랜잭션을 분리하여 비동기로 수행할 수 있다.’
    • 이렇게 되면, T1이 r1과 r2를 동시에 요구하지 않게 되고, 이는 데드락의 발생 가능 조건인 “순환 대기”를 막을 수 있게 된다.
    • 하지만, 이는 오프라인 트랜잭션 문제와 굉장히 유사한 문제를 갖게 되며, 구현 복잡도를 크게 높이게 된다.

관점 바꾸기 - 비즈니스 트랜잭션과 시스템 트랜잭션의 경계 불일치

위에서 살짝 언급했지만, 트랜잭션의 경계 분리는 분명 이점도 있지만, 단점도 존재한다.

  • 이점
    • 비동기 처리
      • 첫 시스템 트랜잭션만으로 사용자에게 성공 응답 가능
    • 저장소 분리
      • 캐시된 저장소 사용 - 디스크 I/O가 필요한 DB보다 빠른 접근 가능
  • 단점
    • 오프라인 동시성 문제로 인한 구현 복잡도 상승
      • 시스템 트랜잭션과 비즈니스 트랜잭션의 경계 불일치

비즈니스 트랜잭션의 구현을 편히 하기 위해 시스템 트랜잭션 개념을 사용하게 되었지만, 이렇게 보니 “그럼 고생해서 오프라인 트랜잭션이라는 걸로 전부 구현하면, 성능이 최고가 되는 것 아닌가?” 라는 고민이 들 수 있다.

하지만 이는 분명 오버엔지니어링(over-engineering)이고, 우리는 무심코 개발한 코드의 트랜잭션 경계 불일치로 예상치 못한 롤백 및 재시도 처리의 누락을 발생시키고, 수많은 버그들을 만들어내고 있다.

  • 이 주제 또한 상당이 긴 글이 되기 때문에, 해당 글에서 따로 다루도록 하겠다.(작성예정)

참고 자료

  • Database System Concepts, silberschatz(2019)
  • 엔터프라이즈 애플리케이션 아키텍처 패턴 - PoEAA, Martin Fowler, 최민석 역(2021)
  • 데이터 중심 애플리케이션 설계, Martin Kleppmann, 정재부 외 2인 역(2023)