7분

ACID 보장이 신뢰할 수 있는 트랜잭션 시스템을 만드는 방식

ACID 보장이 데이터베이스 설계와 애플리케이션 동작에 어떤 영향을 미치는지 알아보세요. 원자성, 일관성, 격리성, 내구성, 트레이드오프와 실제 사례를 살펴봅니다.

ACID 보장이 신뢰할 수 있는 트랜잭션 시스템을 만드는 방식

일상적 트랜잭션에서 “ACID”가 의미하는 바

장보기 결제이든, 항공편 예약이든, 계좌 간 이체든 결과가 명확하길 기대합니다: 성공했거나 실패했거나. 데이터베이스도 많은 사용자가 동시에 쓰고, 서버가 죽고, 네트워크가 불안정한 상황에서도 같은 확실성을 제공하려 합니다.

트랜잭션, 쉽게 말하면

트랜잭션은 데이터베이스가 하나의 "패키지"로 취급하는 작업 단위입니다. 재고 차감, 주문 레코드 생성, 카드 청구, 영수증 기록 등 여러 단계를 포함할 수 있지만 하나의 일관된 동작처럼 행동해야 합니다.

어떤 단계라도 실패하면 시스템은 반쯤 끝난 상태를 남기기보다 안전한 지점으로 되돌려야 합니다.

부분 업데이트가 실제 비즈니스 문제를 일으키는 이유

부분 업데이트는 단순한 기술적 오류가 아니라 고객 지원 티켓과 재무적 위험으로 이어집니다. 예를 들어:

  • 결제는 이루어졌지만 주문이 생성되지 않음 — 고객은 돈을 냈으나 확인을 받지 못함
  • 주문은 생성됐지만 재고가 차감되지 않음 — 사이트가 과판매하고 나중에 취소함
  • 계좌 이체에서 한 쪽만 차감되고 다른 쪽은 입금되지 않음 — 잔액이 맞지 않음

이런 실패는 디버그하기가 어렵습니다. 모든 것이 "대부분 맞는 것처럼" 보이지만 숫자가 맞지 않습니다.

ACID는 제품이 아니라 보장 집합입니다

ACID는 많은 데이터베이스가 트랜잭션에 대해 제공할 수 있는 네 가지 보장을 줄여 부르는 말입니다:

  • 원자성(Atomicity): 전체 혹은 전무 실행
  • 일관성(Consistency): 데이터는 유효한 규칙 내에 머무름
  • 격리성(Isolation): 동시 트랜잭션이 안전하지 않은 방식으로 간섭하지 않음
  • 내구성(Durability): 커밋 후 변경은 지속됨

ACID는 특정 브랜드나 토글 가능한 단일 기능이 아닙니다. 동작에 대한 약속입니다.

얻는 이득과 예상되는 비용

강한 보장은 일반적으로 데이터베이스가 더 많은 작업을 해야 함을 의미합니다: 추가 조정, 잠금 대기, 버전 추적, 로그 기록 등. 과도한 경우 처리량이 떨어지거나 대기 시간이 늘어날 수 있습니다. 목표는 항상 "항상 최대 ACID"가 아니라 실제 비즈니스 위험에 맞는 보장을 선택하는 것입니다.

원자성: 전부 아니면 전무 업데이트

원자성은 트랜잭션을 하나의 작업 단위로 다루어, 완전히 끝나거나 전혀 영향이 없도록 합니다. 데이터베이스에서 "반만 업데이트된" 상태가 보이지 않습니다.

단순한 은행 이체 예시

Alice에서 Bob으로 $50를 이체한다고 가정하면 내부적으로 적어도 두 가지 변경이 필요합니다:

  • Alice 잔액에서 $50 차감
  • Bob 잔액에 $50 추가

원자성이 있으면 두 변경이 함께 성공하거나 함께 실패합니다. 둘 다 안전하게 할 수 없다면 둘 다 하지 않아야 합니다. 이로써 Alice만 차감되고 Bob에게는 돈이 가지 않는 최악의 상황을 막습니다.

커밋 vs 롤백(비전문가 버전)

데이터베이스는 트랜잭션에 두 가지 출구를 제공합니다:

  • 커밋: "모든 단계가 성공했으니 결과를 공식화하라"
  • 롤백: "문제가 생겼으니 이 트랜잭션의 모든 것을 취소하라"

유용한 비유는 "초안 대 공개"입니다. 트랜잭션이 실행되는 동안 변경은 임시적입니다. 커밋만이 이를 공개합니다.

트랜잭션 중간에 무엇이 잘못될 수 있나?

원자성이 중요한 이유는 실패가 일상적이기 때문입니다:

  • 앱 크래시: 서비스가 한 테이블만 업데이트하고 다음을 처리하기 전에 멈춤
  • 네트워크 단절: 앱이 DB에 도달하지 못하거나 클라이언트가 "성공" 응답을 받지 못함
  • 전원 손실: DB 서버가 예기치 않게 종료됨

이런 일이 커밋 전에 발생하면 원자성은 데이터베이스가 롤백하여 부분적인 작업이 실제 잔액으로 유출되지 않도록 보장합니다.

원자성 + 멱등성 및 재시도

원자성은 DB 상태를 보호하지만, 네트워크 단절로 커밋 여부가 불분명할 때 애플리케이션은 여전히 불확실성을 처리해야 합니다.

두 가지 실무 보완책:

  • 재시도: 응답이 없을 때 요청을 반복
  • 멱등성: 동일 요청 반복을 안전하게 만들기(예: 멱등성 키를 사용해 "transfer #123"이 최대 한 번만 적용되도록)

원자적 트랜잭션과 멱등한 재시도를 함께 쓰면 부분 업데이트와 실수로 인한 중복 청구를 모두 피할 수 있습니다.

일관성: 유효한 규칙 내에서 데이터 유지

ACID에서의 일관성은 "데이터가 그럴듯해 보인다"거나 "모든 복제본이 일치한다"는 뜻이 아닙니다. 트랜잭션은 당신이 정의한 규칙에 따라 데이터베이스를 한 유효한 상태에서 다른 유효한 상태로만 이동시켜야 한다는 뜻입니다.

일관성은 당신이 정한 규칙으로 정의됩니다

데이터베이스는 제약, 트리거, 불변식으로 표현된 명시적 규칙을 기준으로만 데이터를 일관되게 유지할 수 있습니다. ACID는 이 규칙들을 트랜잭션 동안 강제합니다.

일반적인 예:

  • 외래 키: 모든 order.customer_id 는 존재하는 고객을 가리켜야 함
  • 고유 제약: 두 사용자가 같은 이메일을 가질 수 없음
  • 체크 제약/불변식: 계좌 잔액은 음수가 될 수 없거나 상품 수량은 음수가 될 수 없음

이 규칙들이 있으면 데이터베이스는 이를 위반하는 트랜잭션을 거부하므로 “반쪽 유효한” 데이터가 나오지 않습니다.

애플리케이션 검증 vs 데이터베이스 제약

앱 수준 검증은 사용자 경험을 개선하고 복잡한 비즈니스 규칙을 적용하는 데 중요하지만 단독으로는 충분하지 않습니다.

  • 앱 검증: 명확한 에러 메시지와 조기 피드백 제공, 복잡한 로직 검증에 유용
  • DB 제약: 여러 서비스나 백그라운드 작업이 동일 테이블을 쓸 때 최종 방어선

고전적 실패모드는 앱에서 "이메일 사용 가능"을 확인한 뒤 행을 삽입할 때 두 요청이 동시에 확인을 통과하는 경우입니다. 데이터베이스의 고유 제약이 한 번만 삽입되도록 보장합니다.

실무에서의 일관성 모습

"잔액은 음수가 될 수 없다"를 제약이나 단일 트랜잭션 내에서 안정적으로 강제하면, 초과 인출을 초래하는 전송은 전체가 실패해야 합니다. 규칙을 아무 데도 명시하지 않으면 ACID도 보호할 수 없습니다—강제할 수단이 없기 때문입니다.

일관성은 결국 명확성을 요구합니다: 규칙을 정의하고, 트랜잭션이 그 규칙을 결코 깨지 않도록 하세요.

격리성: 동시성 하에서 안전하게 작업하기

격리성은 트랜잭션들이 서로 간섭하지 않도록 합니다. 한 트랜잭션이 진행되는 동안 다른 트랜잭션은 반쯤 끝난 작업을 보거나 실수로 덮어쓰지 않아야 합니다. 목표는 간단합니다: 많은 사용자가 활동하더라도 각 트랜잭션이 마치 혼자 실행된 것처럼 행동해야 합니다.

동시성이 이 문제를 어렵게 만드는 이유

실제 시스템은 바쁩니다: 고객 주문, 지원 담당자 프로필 수정, 백그라운드 결제 정산 작업 등이 동시에 일어납니다. 이런 작업들은 시간이 겹치고 종종 같은 행(계좌 잔액, 재고 수량, 예약 슬롯)에 접근합니다.

격리성이 없으면 타이밍이 비즈니스 로직의 일부가 됩니다. "재고 차감" 업데이트가 다른 체크아웃과 경쟁하거나, 보고서가 변경 도중의 데이터를 읽어 존재하지 않는 수치를 보여줄 수 있습니다.

격리성은 보통 설정 가능

완전한 "혼자 실행한 것처럼" 격리하면 비용이 높습니다. 처리량이 줄고 대기(락)가 늘거나 트랜잭션 재시도가 발생할 수 있습니다. 많은 워크플로우는 가장 엄격한 보호가 필요하지 않습니다(예: 어제의 분석을 읽는 작업은 약간의 불일치를 허용할 수 있음).

그래서 데이터베이스는 선택 가능한 격리 수준을 제공합니다: 어느 정도의 동시성 위험을 감수하고 성능/충돌을 줄일지 선택하세요.

미리보기: 격리가 방지(혹은 허용)하는 이상 현상

격리가 너무 약하면 다음과 같은 고전적 이상이 발생합니다:

  • 더티 리드(Dirty reads): 다른 트랜잭션이 커밋하지 않은 변경을 읽음
  • 로스트 업데이트(Lost updates): 두 트랜잭션이 서로 덮어써 한 세트의 변경이 사라짐
  • 팬텀 리드(Phantom reads): 같은 쿼리를 다시 실행했을 때 다른 행 집합이 반환됨

이러한 실패 모드를 이해하면 제품의 약속에 맞는 격리 수준을 고르기 쉽습니다.

격리가 예방(또는 허용)하는 일반적 이상 현상

두려움 없이 실험하세요
스키마와 격리 수준을 반복 수정하고, 필요할 때 스냅샷으로 되돌리세요.

격리는 트랜잭션이 다른 트랜잭션을 실행 중에 무엇을 "볼 수 있는지"를 결정합니다. 격리가 약하면 이상(사용자에게 놀라운 동작)이 발생할 수 있습니다.

읽기 관련 이상

더티 리드는 다른 트랜잭션이 쓴(하지만 커밋하지 않은) 데이터를 읽을 때 발생합니다.

시나리오: Alex가 계좌에서 $500를 이체해 잔액이 임시로 $200가 되었고, 당신이 Alex의 이체가 실패해 롤백되기 전 그 $200를 읽음.

사용자 관점: 고객이 잘못된 낮은 잔액을 보거나, 사기 탐지 규칙이 잘못 동작하거나, 지원 담당자가 잘못된 정보를 제공함.

**비반복 가능 리드(Non-repeatable read)**는 같은 행을 두 번 읽었을 때 다른 값을 얻는 경우입니다.

시나리오: 주문 합계 $49.00을 읽고 잠깐 후 새로고침했더니 할인 항목이 제거되어 $54.00으로 바뀜.

사용자 관점: "결제 중 내 총액이 바뀌었다"는 불신으로 연결될 수 있습니다.

**팬텀 리드(Phantom read)**는 비슷하지만 행 집합 자체가 달라지는 경우입니다: 다른 트랜잭션이 매칭되는 레코드를 삽입/삭제함.

시나리오: 호텔 검색에서 "3개 이용 가능"으로 표시되었다가 예약 과정에서 다시 확인하니 없음(다른 예약이 들어옴).

사용자 관점: 예약 중복이나 일관성 없는 가용성 화면, 과판매 발생.

쓰기 관련 이상(현실에서 흔한 버그)

로스트 업데이트는 두 트랜잭션이 같은 값을 읽고 각각 업데이트를 쓰는데, 나중 쓰기가 앞선 변경을 덮어써서 하나의 변경이 사라지는 경우입니다.

시나리오: 두 관리자 모두 같은 상품 가격을 편집. 둘 다 $10에서 시작해 한 명은 $12로 저장, 다른 한 명은 나중에 $11로 저장해 앞선 변경이 사라짐.

**라이트 스큐(Write skew)**는 두 트랜잭션이 각각 개별적으로는 유효한 변경을 하지만 둘을 합치면 규칙을 위반하는 경우입니다.

시나리오: 규칙: "항상 최소 한 명의 당직 의사가 배치되어 있어야 한다." 두 의사가 서로 상대가 당직인 것을 확인하고 동시에 오프를 표시하면 함께 적용되어 결과적으로 당직자가 0명이 됨.

왜 항상 가장 엄격한 격리를 사용하지 않나요?

강한 격리는 이상을 줄이지만 대기, 재시도, 비용을 증가시킵니다. 읽기 중심의 분석에는 약한 격리가 적절할 수 있고, 돈 이동이나 예약처럼 정확성이 중요한 흐름에는 더 엄격한 설정을 쓰는 것이 낫습니다.

격리 수준: 적절한 안전 설정 선택하기

격리는 트랜잭션이 다른 트랜잭션이 실행되는 동안 무엇을 "볼 수 있는지"에 관한 것입니다. DB는 이를 격리 수준으로 노출합니다. 높은 수준은 놀라운 동작을 줄이지만 처리량과 응답성에 비용을 지불해야 합니다.

일반적인 격리 수준

  • Read Uncommitted: 커밋되지 않은 변경을 읽을 수 있음(더티 리드). 거의 아무것도 방지하지 않음.
  • Read Committed: 커밋된 데이터만 읽음으로 더티 리드를 방지. 그러나 같은 쿼리를 두 번 실행하면 누군가 중간에 커밋했을 수 있어 결과가 달라질 수 있음(비반복 가능 리드).
  • Repeatable Read: 트랜잭션 내에서 이미 읽은 내용은 안정적으로 유지되는 경향이 있어 비반복 가능 리드를 일반적으로 방지. 엔진에 따라 팬텀을 막지 못할 수도 있음.
  • Serializable: 트랜잭션이 순차적으로 실행된 것처럼 행동함. 가장 강력한 설정으로 더티 리드, 비반복 가능 리드, 팬텀을 방지하고 많은 미묘한 쓰기 이상도 줄여줌.

수준 선택: 처리량 vs 정확성

많은 팀은 사용자 대상 앱에서 기본으로 Read Committed를 선택합니다: 성능이 좋고 "더티 리드 없음"은 대부분의 기대에 부합합니다.

  • Repeatable Read는 트랜잭션 내부에서 안정적인 결과가 필요할 때(예: 명세서 생성)
  • Serializable는 작은 크리티컬 섹션에서 정확성이 가장 중요할 때(예: 과판매 금지)

Read Uncommitted는 OLTP 시스템에서는 드뭅니다. 모니터링이나 대략적인 리포팅에 간혹 쓰입니다.

중요한 경고: 엔진별 동작 차이

이름은 표준화되어 있지만 정확한 보장은 DB 엔진마다 다릅니다(때로는 설정에 따라 달라짐). 문서를 확인하고 비즈니스에 중요한 이상을 테스트하세요.

내구성: 커밋을 영구화하기

내구성은 트랜잭션이 커밋되면 그 결과가 크래시(전원 손실, 프로세스 재시작, 머신 재부팅)를 견뎌야 한다는 의미입니다. 앱이 고객에게 "결제 성공"이라고 말하면, 내구성은 DB가 다음 장애 이후에도 그 사실을 잊지 않을 것이라는 약속입니다.

DB가 커밋을 생존시키는 방법

대부분의 관계형 DB는 **WAL(Write-Ahead Logging)**으로 내구성을 달성합니다. 요약하면, DB는 트랜잭션을 커밋으로 간주하기 전에 변경의 순차적 "영수증"을 디스크의 로그에 씁니다. DB가 크래시하면 시작 시 로그를 재생해 커밋된 변경을 복원합니다.

복구 시간을 합리적으로 유지하기 위해 DB는 또한 체크포인트를 만듭니다. 체크포인트는 최근 변경 중 충분한 부분을 메인 데이터 파일에 써서 복구 시 재생해야 하는 로그 양을 제한합니다.

내구성은 스토리지와 구성에 따라 달라집니다

내구성은 단순한 on/off가 아닙니다. 데이터가 안정적 저장소에 얼마나 강제로 기록되는지에 달려 있습니다.

  • 동기 설정에서는 DB가 로그가 플러시(대개 OS fsync 호출)될 때까지 기다린 뒤 커밋을 응답합니다. 안전하지만 지연이 늘어납니다.
  • 비동기 설정에서는 로그가 완전히 영구 저장되기 전에 커밋을 응답할 수 있습니다. 성능은 좋아지지만 크래시 시 최근 커밋을 잃을 수 있습니다.

기본 하드웨어도 중요합니다: SSD, RAID 컨트롤러의 쓰기 캐시, 클라우드 볼륨은 장애 시 다르게 동작할 수 있습니다.

백업과 복제는 관련 있지만 다름

백업과 복제는 복구와 가용성 축소에 도움을 주지만, 내구성과 동일하지 않습니다. 트랜잭션이 주(primary)에 내구적일 수 있지만 복제본에 아직 도달하지 않았을 수 있고, 백업은 보통 시점별 스냅샷이지 커밋 단위 보장은 아닙니다.

데이터베이스가 내부적으로 ACID를 어떻게 강제하는가

초기에 동시성 테스트
실행 가능한 프로젝트에서 교착상태, 재시도, 짧은 트랜잭션을 빠르게 모델링하세요.

BEGIN으로 트랜잭션을 시작하고 나중에 COMMIT할 때, DB는 누가 어떤 행을 읽고 쓸 수 있는지, 동시에 누가 같은 레코드를 변경하려고 할 때 무슨 일이 일어나는지 등을 조정합니다.

비관적 vs 낙관적 동시성 제어

충돌을 다루는 핵심 선택은 다음과 같습니다:

  • 비관적 잠금: 충돌이 일어날 가능성이 높다고 가정. 업데이트 시 행을 잠가 다른 트랜잭션은 기다리게 함. 많은 이상을 예방하지만 블로킹이 발생.
  • 낙관적 접근: 충돌이 드물다고 보고 잠금을 적게 사용. 트랜잭션은 덜 차단되고 커밋 시 충돌을 감지해 한 쪽을 거부하고 재시도하게 함.

많은 시스템은 워크로드와 격리 수준에 따라 두 접근을 혼합합니다.

MVCC: 읽기가 쓰기를 막지 않음

현대 DB는 종종 **MVCC(다중 버전 동시성 제어)**를 사용합니다. 한 행의 단일 복사본만 유지하는 대신 여러 버전을 유지합니다.

  • 읽기는 일관된 스냅샷(과거 버전)을 기다리지 않고 볼 수 있고
  • 쓰기는 새로운 버전을 만들며 읽기는 계속 진행됩니다.

이 덕분에 일부 DB는 많은 읽기와 쓰기를 동시에 처리하면서 블로킹을 줄일 수 있습니다. 다만 쓰기-쓰기 충돌은 여전히 해결해야 합니다.

데드락: 대기가 순환할 때

잠금은 데드락을 유발할 수 있습니다: 트랜잭션 A가 B가 가진 잠금을 기다리고 있고 B는 A가 가진 잠금을 기다리는 상황. DB는 보통 사이클을 감지해 한 트랜잭션을 중단(데드락 희생자) 시키고 애플리케이션이 재시도하도록 오류를 반환합니다.

문제가 있음을 알려주는 실무 신호

ACID 강제가 마찰을 일으키면 보통 다음을 관찰합니다:

  • 피크 사용 시 잠금 대기 증가
  • 타임아웃(오랜 대기 후 쿼리 실패)
  • 특정 행/테이블(카운터, last seen 필드 등)에 대한 집중적 경합

이 신호들은 트랜잭션 크기, 인덱스, 또는 어떤 격리/잠금 전략이 적합한지 재검토해야 할 때임을 알려줍니다.

ACID가 애플리케이션 설계 결정에 미치는 영향

ACID 보장은 단순한 데이터베이스 이론이 아니라 API, 백그라운드 작업, UI 흐름 설계에 영향을 줍니다. 핵심 아이디어는 어떤 단계들이 함께 성공해야 하는지 결정하고, 그 단계들만 트랜잭션으로 감싸는 것입니다.

“하나의 비즈니스 변경” 중심의 API 설계

좋은 트랜잭션 API는 보통 하나의 비즈니스 동작에 매핑됩니다. 예를 들어 /checkout 작업은 주문 생성, 재고 예약, 결제 의도 기록을 포함할 수 있습니다. 이러한 DB 쓰기들은 보통 하나의 트랜잭션에 넣어 검증 실패 시 함께 롤백되도록 합니다.

일반 패턴:

  • 트랜잭션을 열기 전에 입력 검증 수행
  • 트랜잭션 열기
  • 필요한 최소한의 읽기/쓰기 수행
  • 커밋

이렇게 하면 원자성과 일관성을 지키면서 느리고 취약한 트랜잭션을 피할 수 있습니다.

요청, 서비스, 작업(job)에서의 트랜잭션 경계

트랜잭션 경계를 어디에 두느냐는 "하나의 작업 단위"가 무엇인지에 달려 있습니다:

  • 사용자 요청: 트랜잭션을 짧게 유지—몇 개의 쿼리 정도. 뷰 렌더링이나 외부 응답 대기 중 잠금을 유지하지 마세요.
  • 백그라운드 작업: 각 작업 시도를 하나의 작업 단위로 간주. 예를 들어 10,000개 레코드를 처리하면 배치별로 커밋해 안전하게 재시작할 수 있도록 함.
  • 서비스 경계: 한 서비스의 DB 안에서 트랜잭션을 유지하는 것이 바람직합니다. 서비스 간에는 아웃박스 같은 다른 접근이 필요합니다. 하나의 ACID 트랜잭션이 여러 DB를 포괄하기 어렵습니다.

오류 처리: 롤백, 재시도, 안전한 재실행

ACID가 돕지만 애플리케이션은 여전히 실패를 올바르게 처리해야 합니다:

  • 오류 시 롤백: 어떤 단계라도 실패하면 트랜잭션을 중단
  • 일시적 오류 재시도: 직렬화 실패나 데드락은 동시성에서 흔함. 전체 트랜잭션 재시도가 정답일 때가 많음
  • 멱등성 보장: 요청이 재시도될 때(클라이언트나 작업 러너가 하는 경우) 중복 청구/중복 발송 없이 안전하게 재실행 가능하게 만듬—멱등성 키나 고유 제약 사용

흔한 안티패턴

긴 트랜잭션, 트랜잭션 안에서 외부 API 호출, 사용자 생각 시간 동안의 잠금(예: 장바구니 행을 잠근 채 사용자 확인 대기)은 피하세요. 이들은 경합을 키우고 격리 충돌을 훨씬 더 자주 발생시킵니다.

도구가 도움을 줄 수 있는 곳(본질을 바꾸지 않고)

빠르게 트랜잭션 시스템을 만들 때 가장 큰 위험은 보통 ACID를 몰라서가 아니라 하나의 비즈니스 동작을 여러 엔드포인트, 작업, 테이블에 흩어놓아 트랜잭션 경계를 명확히 하지 않는 것입니다.

예: Koder.ai 같은 플랫폼은 ACID를 중심으로 설계하면서도 개발 속도를 높이는 데 도움을 줄 수 있습니다. 워크플로(예: "재고 예약과 결제 의도를 포함한 체크아웃")를 설명하면 React UI와 Go + PostgreSQL 백엔드를 생성하고 스냅샷/롤백으로 스키마나 트랜잭션 경계를 바꾸며 반복할 수 있게 해 줍니다. 데이터베이스는 여전히 보장을 강제하고, 도구의 가치는 올바른 설계에서 작동하는 구현으로 가는 경로를 단축하는 데 있습니다.

분산 및 다중 서비스 시스템에서의 ACID

트랜잭션을 명확하게 설계하세요
코딩 전에 플래닝 우선 채팅으로 불변 조건, 테이블, 트랜잭션 경계를 정의하세요.

단일 데이터베이스는 보통 하나의 트랜잭션 경계 내에서 ACID 보장을 제공할 수 있습니다. 작업을 여러 서비스(종종 여러 DB)에 분산하면 같은 보장을 유지하기가 더 어렵고, 시도하면 비용이 커집니다.

일관성 vs 가용성: 운영에서 체감하는 트레이드오프

엄격한 일관성은 모든 읽기가 "최신의 커밋된 진실"을 보게 하는 것입니다. 높은 가용성은 일부 구성 요소가 느리거나 도달 불가능할 때도 시스템이 응답을 계속하도록 하는 것입니다.

다중 서비스 구성에서는 임시 네트워크 문제로 선택을 강요당할 수 있습니다: 모든 참가자가 동의할 때까지 요청을 블록하거나 실패시키기(더 일관성 있지만 덜 가용함), 아니면 서비스들이 잠깐 불일치해도 요청을 허용하기(더 가용하지만 덜 일관성 있음). 어느 쪽도 항상 옳지 않습니다—비즈니스가 어떤 실수를 감내할 수 있는지에 달려 있습니다.

분산 트랜잭션이 어려운 이유

분산 트랜잭션은 네트워크 지연, 재시도, 타임아웃, 서비스 크래시, 부분 실패 등 당신이 완전히 통제할 수 없는 경계들 간의 조정을 필요로 합니다.

모든 서비스가 올바르더라도 네트워크 때문에 모호성이 생길 수 있습니다: 결제 서비스는 커밋했지만 주문 서비스가 확인을 받지 못했을 수 있습니다. 이를 안전하게 해결하려면 2단계 커밋 같은 조정 프로토콜이 필요하고, 이는 느리고 장애 시 가용성을 낮추며 운영 복잡성을 더합니다.

"하나의 큰 트랜잭션"을 대체하는 실무 패턴

사가(Saga): 워크플로를 단계로 나누고 각 단계는 로컬 커밋. 이후 단계가 실패하면 앞선 단계들을 보상(예: 환불)으로 되돌림.

아웃박스/인박스: 서비스는 비즈니스 데이터와 "발행할 이벤트" 레코드를 같은 로컬 트랜잭션에 씁니다(아웃박스). 소비자는 처리된 메시지 ID를 기록(인박스)해 재시도 시 중복 효과를 방지합니다.

최종적 일관성(Eventual consistency): 서비스 간 데이터가 잠시 다를 수 있음을 허용하고 조정을 위한 계획을 둠.

언제 보장을 완화하고 어떻게 위험을 통제할지

보장을 완화할 때:

  • 일시적 불일치를 감내할 수 있을 때(예: 배송 상태가 주문 생성 뒤 약간 지연)
  • 오류를 보상으로 정정할 수 있을 때(환불, 취소)
  • 지연과 가용성이 즉각적 전역 정확성보다 중요할 때

위험 통제 방법:

  • 반드시 지켜야 할 불변식 정의(무조건 위반해서는 안 되는 규칙)
  • 멱등성 연산 설계
  • 타임아웃과 백오프를 포함한 재시도 정책
  • 드리프트 감시(멈춘 사가, 반복되는 보상, 증가하는 아웃박스 테이블) 모니터링

정말 중요한 불변식(예: "계좌를 절대 초과 출금하지 않음")은 가능하면 하나의 서비스와 하나의 DB 트랜잭션 내에 유지하세요.

실용 체크리스트: ACID 시스템 설계·테스트·모니터링

유닛 테스트에서 "정상"인 트랜잭션도 실제 트래픽, 재시작, 동시성에서는 실패할 수 있습니다. ACID 보장이 운영 환경에서 기대대로 작동하도록 다음 체크리스트를 사용하세요.

1) 설계: 불변식과 트랜잭션 경계 정의

먼저 항상 이어야 하는 것을 적으세요(데이터 불변식). 예: "계좌 잔액은 음수가 될 수 없다", "주문 합계는 라인 아이템 합과 같다", "재고는 0 아래로 내려갈 수 없다", "결제는 정확히 하나의 주문에 연결된다". 이들을 제품 규칙으로 취급하세요.

그다음 어떤 것이 반드시 하나의 트랜잭션 안에 있어야 하고 어떤 것은 지연 가능한지 결정하세요.

  • 데이터 불변식: 관련 테이블/행과 정확한 규칙 나열
  • 실패 시나리오: 프로세스 크래시 중간, 커밋 후 네트워크 타임아웃, 재시도로 인한 중복, 복제본 장애 전환, 디스크 부족 등
  • 동시성 프로필: 어떤 작업이 병렬로 실행되는지(체크아웃 스파이크, 배치 업데이트, 예약 작업), 예상 경합 지점, 읽기가 "지금 시점"이어야 하는지 아니면 약간 오래된 데이터로 괜찮은지

트랜잭션을 작게 유지하세요: 적은 행을 건드리고, 외부 API 호출을 포함하지 말고, 빠르게 커밋하세요.

2) 테스트: 경쟁과 장애에서 동작 증명

동시성을 테스트의 주요 축으로 만드세요.

  • 레ース 컨디션 테스트: 동일한 중요한 작업을 동시 실행(예: 마지막 물건에 대한 두 체크아웃)하고 불변식이 깨지지 않음을 검증
  • 장애 주입(Fault injection): 트랜잭션 중간에 앱 프로세스를 죽이고, 타임아웃을 주입하고, 재시도하고, DB를 재시작해 결과가 커밋되었거나 안전하게 롤백되었는지 확인
  • 부하 테스트와 정확성 검사: 피크 처리량 하에서 지연뿐 아니라 합계, 카운트, 중복 없음 등의 정확성도 검증

재시도를 지원하면 명시적 멱등성 키를 추가하고 "성공 후 요청 반복" 시나리오도 테스트하세요.

3) 모니터: 문제가 사용자에게 가기 전에 포착

보장을 유지하는 데 비용이 커지거나 취약해질 징후를 감시하세요:

  • 잠금 대기와 큐 시간(경쟁 증가)
  • 데드락(빈도, 희생자 쿼리)
  • 장기 실행 트랜잭션(근본 원인인 경우 많음)
  • 복제 지연(읽기 지연 및 장애 전환 지연)
  • 커밋/fsync 시간(스토리지 압박; 내구성 비용)

스파이크뿐 아니라 추세에 경고를 걸고, 지표를 해당 엔드포인트나 작업과 연동하세요.

실무 규칙 요약: 격리와 트랜잭션 범위

불변식을 보호하는 데 필요한 가장 약한(즉, 비용이 적은) 격리 수준을 사용하세요. 기본값으로 무턱대고 최고 수준을 올리지 마세요. 작은 중요 구간(자금 이동, 재고 차감)에는 엄격한 설정을 적용하고, 나머지는 외부로 빼서 성능을 확보하세요.

자주 묻는 질문

데이터베이스에서 ACID가 실제로 뜻하는 바는 무엇인가요?

ACID는 실패와 동시성 상황에서 데이터베이스가 예측 가능하게 동작하도록 돕는 트랜잭션 보장들의 집합입니다:

  • 원자성(Atomicity): 모든 단계가 성공하거나 전혀 적용되지 않음
  • 일관성(Consistency): 커밋된 모든 상태가 정의된 규칙/제약을 만족함
  • 격리성(Isolation): 동시 트랜잭션이 안전하지 않은 방식으로 상호작용하지 않음
  • 내구성(Durability): 커밋된 변경사항은 장애가 발생해도 보존됨
트랜잭션이란 무엇이며 왜 중요한가요?

트랜잭션은 데이터베이스가 하나의 ‘패키지’로 다루는 단위 작업입니다. 여러 SQL 문을 실행하더라도 결과는 두 가지 중 하나로 끝납니다:

  • 커밋(Commit): 모든 변경이 공식적으로 적용됨
  • 롤백(Rollback): 어떤 변경도 적용되지 않음
부분 업데이트가 비즈니스에 왜 큰 문제인가요?

부분 업데이트는 현실 세계에서 모순을 만듭니다. 예를 들어:

  • 고객이 결제되었지만 주문이 기록되지 않음
  • 주문은 생성되었지만 재고가 차감되지 않아 과판매 발생
  • 송금의 한 쪽만 적용되어 잔액 불일치 발생

ACID(특히 원자성과 일관성)는 이런 ‘반쪽짜리’ 상태가 진실로 보이지 않도록 막아줍니다.

원자성은 어떻게 반쯤 완료된 작업을 막나요?

원자성은 데이터베이스가 ‘반쯤 완료된’ 트랜잭션을 노출하지 않도록 보장합니다. 커밋 전에 앱이 크래시하거나 네트워크가 끊기거나 DB가 재시작되면, 트랜잭션은 롤백되어 이전 단계의 변경이 지속되지 않습니다.

실무에서는 두 계정의 잔액을 동시에 바꾸는 이체 같은 다단계 변경을 안전하게 만드는 것이 바로 원자성입니다.

ACID가 안전하다면 왜 멱등성과 재시도가 필요한가요?

클라이언트가 응답을 못 받는 경우(예: 네트워크 타임아웃 직후 커밋된 경우) 커밋이 실제로 되었는지 확신할 수 없습니다. 따라서 ACID와 함께 다음을 사용해야 합니다:

  • 재시도(Retries): 일시적 실패에 대해 요청을 반복
  • 멱등성(Idempotency): 동일 요청을 여러 번 실행해도 한 번만 적용되도록 idempotency 키나 고유 제약 사용

이 조합은 부분 업데이트와 중복 청구/중복 기록을 모두 방지합니다.

ACID에서 ‘일관성’이 의미하는 바는 무엇이며, 무엇을 의미하지 않나요?

ACID의 ‘일관성’은 데이터가 ‘그럴듯해 보이는 것’이 아니라, 트랜잭션이 정의한 규칙들(제약, 트리거, 불변식)에 따라 한 유효한 상태에서 다른 유효한 상태로 이동해야 함을 뜻합니다.

예를 들어 “잔액은 음수가 되어선 안 된다”는 규칙을 어디에도 명시하지 않으면 ACID가 그 규칙을 강제할 수 없습니다. 데이터베이스는 명시된 불변식에 대해서만 일관성을 보장합니다.

앱에서 이미 입력을 검증하는데 데이터베이스 제약이 왜 필요한가요?

앱 레벨 검증은 UX와 초기사전검사에 중요하지만 동시성 상황에서는 부족할 수 있습니다(예: 두 요청이 동시에 ‘이메일 사용 가능’ 체크를 통과). 데이터베이스 제약은 최종 방어선 역할을 합니다:

  • 고유 제약(Unique constraints): 중복 이메일 방지
  • 외래 키(Foreign keys): 고아 레코드 방지
  • 체크 제약/불변식: 잘못된 값 방지

따라서 둘을 함께 사용하세요: 앱에서 미리 검증하고, DB에서 확실히 강제합니다.

격리성은 어떤 동시성 버그를 막아주나요?

격리성은 트랜잭션이 다른 트랜잭션을 ‘보거나’ 영향을 받는 방식을 제어합니다. 약한 격리에서는 다음과 같은 이상 현상이 발생할 수 있습니다:

  • 더티 리드(Dirty read): 아직 커밋되지 않은 데이터를 읽음
  • 비반복 가능 리드(Non-repeatable read): 동일 행을 다시 읽을 때 값이 바뀜
  • 팬텀 리드(Phantom read): 동일 조건의 쿼리에서 행 집합이 달라짐
  • 로스트 업데이트/라이트 스큐(Lost update / Write skew): 동시 쓰기 때문에 일관성이 깨어짐

격리 수준은 이러한 이상을 어느 정도 방지할지 선택하게 해 줍니다.

성능을 크게 떨어뜨리지 않고 격리 수준을 어떻게 선택하나요?

많은 OLTP 애플리케이션에서 기본값으로 Read Committed를 사용하는 것이 실용적입니다(더티 리드 방지, 우수한 성능). 필요하다면 위로 올리세요:

  • Repeatable Read: 트랜잭션 내부에서 읽은 값이 안정적이어야 할 때
  • Serializable: 과판매 방지 등 무결성이 최우선일 때(대기·재시도가 늘어남)

항상 사용하는 DB 엔진의 동작을 문서에서 확인하고, 실제로 문제되는 이상 현상을 테스트하세요.

내구성은 무엇을 보장하며 어떤 상황에서 약해지나요?

내구성은 데이터베이스가 커밋을 확인한 이후 그 변경을 장애가 나도 잊지 않겠다는 약속입니다. 대부분의 관계형 DB는 **WAL(Write-Ahead Logging)**을 사용합니다: 변경 기록을 로그에 먼저 순차적으로 기록한 뒤 커밋을 확인합니다. 장애 시 로그를 재생해 커밋된 변경을 복구합니다.

구성에 따라 내구성 강도는 달라집니다:

  • 동기적 커밋/명시적 fsync: 안전하지만 지연이 늘어날 수 있음
  • 비동기 설정: 속도는 빠르지만 크래시 시 최근 커밋을 잃을 수 있음

백업과 복제는 복구/가용성에 도움을 주지만, 내구성과는 별개의 보장입니다.

데이터베이스는 ACID를 내부적으로 어떻게 구현하나요?

트랜잭션을 BEGIN하고 COMMIT할 때 DB는 누가 어떤 행을 읽고 쓰는지, 충돌이 나면 어떻게 처리할지 조율합니다.

주요 기법:

  • 비관적 잠금(Pessimistic locking): 충돌이 자주 난다고 가정하고 업데이트 시 잠금. 차단이 발생하지만 많은 이상을 예방.
  • 낙관적 접근(Optimistic): 충돌이 드물다고 보고 진행하다가 커밋 시 충돌을 검출하고 재시도.
  • MVCC(Multi-Version Concurrency Control): 여러 버전을 유지해 읽기는 블로킹 없이 일관된 스냅샷을 읽게 함. 쓰기 충돌은 별도로 해결해야 함.

잠금이 교착 상태(데드락)를 만들면 DB가 사이클을 감지해 한쪽 트랜잭션을 중단시키고(데드락 희생자) 애플리케이션이 재시도하도록 만드는 것이 일반적입니다.

ACID는 애플리케이션 설계에 어떤 영향을 미치나요?

ACID 보장은 API 설계, 백그라운드 작업, UI 흐름 등 여러 설계 결정을 좌우합니다. 핵심 아이디어는 어떤 단계들이 함께 성공해야 하는지를 정의하고, 그 단계들만 트랜잭션 안에 넣는 것입니다.

실무 권장 패턴:

  • 트랜잭션을 "한 비즈니스 변경"에 매핑하세요(예: /checkout에서 주문 생성·재고 확보·결제 의도 기록을 하나의 트랜잭션 범위로).
  • 트랜잭션 전 입력 검증을 하고, 트랜잭션은 최소한의 읽기/쓰기만 수행한 뒤 커밋하세요.
  • 외부 API 호출이나 사용자 대기 시간은 트랜잭션 외부로 빼세요.

에러 처리:

  • 에러 발생 시 롤백
  • 직렬화 실패/데드락 발생 시 재시도
  • 멱등성 키로 안전한 재실행 보장
ACID는 분산/다중 서비스 환경에서 어떻게 적용되나요?

단일 DB 내에서는 ACID를 지키기 쉽지만, 서비스가 여러 개로 분산되면 같은 수준의 보장을 유지하기가 어렵고 비용이 커집니다.

분산 환경에서의 현실적 패턴:

  • 사가(Sagas): 워크플로를 단계로 나누고, 실패 시 보상(Compensating action)으로 되돌림
  • 아웃박스/인박스(Outbox/Inbox): 비즈니스 데이터와 이벤트 발행 기록을 같은 로컬 트랜잭션에 쓰고, 소비자는 처리 로그로 중복 방지
  • 최종적 일관성(Eventual consistency): 시스템 간 일시적 불일치를 허용하고 조정 프로세스를 둠

중요 불변식(예: “계좌는 절대 초과 인출되지 않아야 함”)이 있다면 가능한 한 하나의 서비스와 하나의 DB 트랜잭션 안에 유지하세요.

ACID 시스템을 설계·테스트·모니터링할 때의 실용적 체크리스트는?

트랜잭션은 유닛 테스트에서는 맞더라도 실제 트래픽과 재시작, 동시성에서는 깨질 수 있습니다. 실무 체크리스트:

  1. 설계: 불변식과 트랜잭션 경계 정의
  • 무엇이 항상 참이어야 하는지 정리하고, 어떤 것이 한 트랜잭션 안에 꼭 있어야 하는지 결정하세요.
  1. 테스트: 경쟁 상태와 장애를 증명
  • 동시성 테스트, 장애 주입(프로세스 중단, DB 재시작 등), 부하 테스트로 정확성 검증
  1. 모니터링: 문제를 사용자보다 먼저 포착
  • 잠금 대기, 데드락 빈도, 장기 실행 트랜잭션, 복제 지연, 커밋/fsync 시간 모니터링

규칙 요약: 불변식을 보호하는 데 필요한 최소한의 격리와 최소 범위의 트랜잭션을 사용하세요. 모든 것을 기본값으로 최고 수준으로 올리지 마세요—성능과 확장성에 큰 비용이 듭니다.

Related posts