CS 지식/데이터베이스

전통적인 ACID와 분산 시스템에서의 ACID

마늘냄새폴폴 2025. 7. 12. 17:33

데이터베이스 포스팅이 벌써 50개를 넘어섰는데요. 단일 카테고리로는 두번째 양인데요. 첫번째는 Spring인데 조금 허수가 있는게 영한님 영상을 보면서 복사 붙여넣기 했던 포스팅이 15개정도 그리고 본격적으로 다시 보면서 공부한게 또 20몇개정도 되니 허수가 있죠. 

 

하지만 데이터베이스 카테고리는 그런 허수없이 50개를 넘어섰으니 정말 많이 썼습니다. 그만큼 제가 데이터베이스 공부를 정말 좋아합니다. 

 

데이터베이스에 대한 내용을 공부하면서 트랜잭션에 대한 이야기는 정말 많이 썼는데요. "스프링 내부적인 트랜잭션 동작 방식"이나 "PostgreSQL이 @Transactional과 어울리지 않는다"나 "@Transactional로 분산 트랜잭션을 구현할 수 있을까?" 같은 내용이 바로 그러하죠. 

 

근데 이 때 ACID를 빼놓고 이야기하기 정말 힘들어집니다. 트랜잭션과 ACID는 정말 뗄래야 뗄 수 없는 짝꿍같은 느낌이기에 이 둘을 떼고 얘기할 수 없어지는 것이죠. 

 

서론이 길었습니다. 이런 전통적인 ACID가 분산 시스템에선 조금 달라진다는 사실, 알고 계셨나요? 물론 개념적인 부분은 똑같이 돌아가지만 전통적인 ACID는 가만히 냅둬도 알아서 잘 돌아가지만 분산 시스템에선 이 ACID를 직접 구현해줘야 한다는 것이 큰 차이입니다. 

 

그럼 본격적으로 어떤 부분이 다른지 정리해보겠습니다. 

 

전통적인 ACID

ACID를 하나하나 다시 상기하면서 전통적인 ACID부터 시작해보도록 하겠습니다. 

 

  • Atomicity : "어떤 작업에 대해 쿼리 하나하나에 대응되는 것이 아닌 하나의 전체적인 작업으로서 모든 작업이 모두 성공하거나 모두 실패해야한다."

    원자성의 정의는 위와 같고 여기서는 쿼리 하나하나에 초점이 맞춰진 것이 아닌 쿼리가 모인 하나의 "작업"으로서의 원자적인 동작을 의미합니다. 

    예를 들어서 주문 -> 결제 -> 알람 이렇게 세 단계의 걸친 "작업"이 동시에 성공하던가 동시에 실패해야한다는 것이죠. 만약 성공했다면 모두 성공하고 중간에 실패했다면 모든 작업이 실패해야한다는 것입니다. 

    이런 상황을 대비해 대부분의 프레임워크에선 트랜잭션에 대한 처리를 도와주고 있고 스프링에서도 @Transactional 어노테이션을 이용해서 원자성을 보장받을 수 있습니다. 
  • Consistency : "데이터를 조작하는 과정에서 충돌이 일어나는 경우 데이터가 꼬이지 않도록 일관성을 보장해야한다"

    일관성의 초점은 데이터를 조작하는 "쓰기" 작업에 대한 내용입니다. 만약 A트랜잭션이 1번 행에 접근해서 데이터를 가져와 가공하는 도중 B트랜잭션이 똑같이 1번 행에 접근해 데이터를 가공하려고 한다면 충돌이 일어나고 이 때 값이 꼬여 일관성이 무너질 수 있기 때문에 이것을 적절히 처리해야한다는 것이죠. 

    전통적인 ACID에선 PCC (Pesimistic Concurrency Control) 와 OCC (Optimistic Concurrency Control) 이렇게 두가지 방법으로 일관성을 보장합니다. 

    PCC는 트랜잭션이 반드시 충돌한다고 가정하기 때문에 충돌 여부를 읽기단계에서 처리하는 높은 수준의 일관성을 보장합니다. 쉽게 얘기해서 읽기 작업이 들어가면 해당 행에 락을 걸어서 뒤에 있는 트랜잭션이 접근하지 못하게 막는다는 의미입니다. 보통 높은 일관성을 유지해야하는 경우에 PCC를 사용합니다. 

    OCC는 이와 반대로 트랜잭션이 충돌하지 않는다고 가정하기 때문에 충돌 여부를 커밋 단계에서 처리하는 낮은 수준의 일관성입니다. 커밋 시점에 충돌 여부를 확인하기 때문에 트랜잭션이 하나의 행을 잡고 동시에 처리할 수는 있지만 커밋 단계에서 충돌이 일어나는 경우 뒤에 있는 트랜잭션에게 예외를 던지면서 작업이 실패하게 만들죠. 때문에 OCC의 경우 예외 상황에 대한 재시도 로직이 반드시 필요하다는 점이 있어 복잡도가 올라가지만 PCC에 비해 성능이 잘 나온다는 장점이 있죠.

    때문에 PCC는 강한 일관성이 보장되어야하는 결제로직과 같은 중요한 연산에서 주로 사용되고 OCC의 경우 그정도의 강한 일관성은 필요 없고 성능이 더 중요한 경우 사용하기도 합니다. 
  • Isolation : "각각의 작업들은 서로 독립적으로 동작하는 것을 보장해야한다"

    격리성은 작업들에 대해 서로 독립적으로 동작하여야하고 각자의 작업에 영향을 주어서는 안된다는 원칙인데요. 보통 데이터베이스 수준에서 격리수준을 제공하고 단계는 Read Uncommited, Read Commited, Repeatable Read, Serializable 이렇게 네 단계가 있습니다. 

    각각의 격리수준마다 생기는 부정합 문제가 있어 정합성을 맞추는 것이 중요하지만 뒤로 갈수록 성능이 떨어져 보통의 데이터베이스의 경우 Read Commited를 채택합니다. 하지만 InnoDB의 경우 Repeatable Read를 채택한다는 특징이 있습니다. 
  • Durability : "만약 작업이 완료 되었다면 그것이 설령 데이터베이스가 내려가더라도 완료된 것을 보장해야한다"

    내구성이 사실 앞선 세개의 원칙에 비해 추상적이어서 이해하기 조금 까다로운 부분이 있습니다. 저도 처음에 ACID를 공부하면서도 이 내구성이 가장 이해가 안되더라구요. 

    내구성은 쉽게 이야기해서 작업이 완료되는 경우 그 작업이 완료된 것을 보장해야한다는 원칙입니다. 만약 데이터베이스에 쓰기 작업을 하던 도중 데이터베이스가 다운됐다고 가정해봅시다. 그럼 두개의 상황을 가정해볼 수 있습니다. 

    1. 데이터베이스에 쓰기 작업이 적용된 경우
    2. 데이터베이스에 쓰기 작업이 적용되지 않은 경우

    쓰기 작업이 적용되지 않았다는 것은 커밋이 되기 전에 데이터베이스가 내려갔다는 의미이겠죠? 그럼 애플리케이션 레벨에서 예외가 발생했을 것이고 재시도를 했을 것입니다. 

    쓰기 작업이 적용되는 경우 커밋이 되었다는 이야기고 커밋이 됐다는 것은 데이터베이스에 쓰기 작업이 적용되었다는 이야기입니다. 

    즉, 내구성의 경우 "커밋"을 기점으로 데이터베이스에 적용이 되었다는 것을 보장한다는 것입니다. 

 

전통적인 ACID를 일부러 길게 설명했습니다. 뒤에 언급할 분산 시스템에서의 ACID는 위의 내용과 본질적으로는 같지만 복잡하기 때문에 한번 상기시키면서 정리할겸 일부러 길게 썼습니다. 

 

이제 분산 시스템에서 ACID를 다룰텐데 선행 지식이 있는 경우 더 이해하기 편하다는 점을 말씀드리고 들어가도록 하겠습니다. 

 

  1. 분산 시스템은 어떻게 생긴 놈인가?
  2. 카프카에 대한 기본적인 지식
  3. 분산 트랜잭션과 보상 트랜잭션에 대한 이해 (Saga 패턴)

분산 시스템에서의 ACID

분산 시스템에서 고려해야하는 가장 중요한 부분은 각각의 서비스들이 물리적으로 분리되어있다는 것입니다. 물리적으로 분리되어있기 때문에 원자성, 일관성을 지키기 굉장히 어려워졌고 때문에 분산 시스템에서 가장 중요하게 봐야할 ACID 속성은 A와 C입니다. 

 

이번 섹션에선 A와 C에 초점을 두고 깊이있게 이야기해보도록 하겠습니다. 

 

분산 시스템에서의 Atomicity, Consistency

아까의 예시를 그대로 가져와서 사용해보겠습니다. 주문 -> 결제 -> 알람 이렇게 세개의 로직을 하나의 원자적인 "작업"으로 묶어야합니다. 그런데 문제가 있습니다. 지금 분산 시스템에선 주문, 결제, 알람이 각각 다른 서버로 이루어져있습니다. 

 

보통 이 로직의 경우 카프카를 사용해서 비동기로 처리하는데요. 만약 동기방식으로 처리했다면 물론 하나의 작업으로 묶기는 편했겠지만 지금처럼 세단계의 뎁스로 이루어져있는게 아니라 여러개의 뎁스로 이어져있는 경우 끝까지 갔다가 돌아오는 시간동안 애플리케이션이 아무것도 못하고 기다려야하기 때문에 비동기로 처리하곤합니다. 

 

그럼 카프카를 이용해서 비동기로 처리해야하는데 비동기로 처리하겠다는 의미는 던져놓고 자기가 할일 한다는 의미잖아요? 그럼 주문 처리하고 결제로 던지고 주문 테이블에 데이터는 커밋되어 처리되죠. 

 

이 때 카프카가 커밋된 것을 확인하고 이어서 결제 서버에 메세지를 보냈습니다. 근데 만약 카프카의 메세지가 처리되지 않는다면 어떻게 해야할까요? 메세지가 처리되지 않는다는 것은 두가지 관점에서 바라볼 수 있습니다. 

 

  1. 데이터베이스에는 작업이 완료되었지만 네트워크 전송 문제로 카프카 메세지가 전송되지 않은 경우
  2. 데이터베이스에서 작업이 실패하여 예외가 터진 경우

1번이 Automicity와 연관있는 내용이고 2번은 Automicity, Consistency와 모두 관련있는 내용입니다. 

 

데이터베이스 작업은 완료, 카프카는 실패

이 경우 트랜잭션의 실패로 롤백이 되어야하는 경우는 아닙니다. 데이터베이스는 완료되었고 뒤로 이어져야할 로직이 이어지지 않은 것이죠. 카프카에선 여러개의 메세지를 원자적으로 묶는 솔루션인 EOS를 지원하긴 하지만 조금 부족한 부분이 있습니다. 

 

바로 데이터베이스의 연산과는 묶을 수 없다는 것입니다. 그래서 나온 개념이 바로 Outbox Pattern입니다. Outbox Pattern은 데이터베이스 연산과 카프카의 메세지를 하나의 연산으로 묶어서 둘 다 안전하게 성공하는 것을 목표로합니다. 

 

Outbox Pattern의 흐름을 정리하면 다음과 같습니다. 이 상황은 데이터베이스 작업은 성공이고 카프카는 실패했다는 것을 가정합니다. 

 

  1. 데이터베이스 연산은 성공
  2. Outbox 테이블에 카프카 연산에 대한 성공 실패 여부를 저장
  3. 정해둔 시간에 따라 지속적으로 스케줄링해서 Outbox 테이블의 내용이 실패했다면 재시도하는 로직을 추가
  4. 성공하면 성공을, 실패하면 실패를 적고 이후 반복

이에 대해서는 제가 쓴 포스팅 글이 있으니 해당 포스팅을 참고해주세요!

 

https://coding-review.tistory.com/566

 

메세지 브로커의 근심과 걱정

마이크로 서비스와 메세지 브로커는 뗄 수 없는 사이입니다. 서로 다른 도메인이 여러개의 서버로 나눠져 있는 상황에서 모든 서버에 동일하게 데이터를 전달해야 하는 경우에 메세지 브로커만

coding-review.tistory.com

 

물론 Outbot Pattern도 테이블에 적는 방법과 트랜잭션 로그를 추적하는 것 이렇게 두가지 방법이 있지만 일단 가장 간단한 테이블에 적는 방식을 소개했습니다. 

 

데이터베이스 연산 자체가 실패

이 경우에는 롤백까지 처리해줘야하기 때문에 조금 복잡할 수 있습니다. 이 경우 보상 트랜잭션으로 롤백을 진행해야하기 때문에 카프카를 이용합니다. 

 

이 경우 흐름도를 확인해보겠습니다. 

 

  1. 주문 서버에서 주문을 처리하고 결제 서버로 이관
  2. 결제 서버에서 메세지를 받고 데이터를 처리하다가 에러 발생
  3. 어떤 데이터를 넣다가 실패했는지 상태에 대한 메세지를 작성해서 다시 주문 서버로 전송
  4. 주문 서버에서 이 값을 보고 데이터를 다시 가공

이 경우는 롤백이라고 부르기에는 조금 애매하긴 합니다. 우리가 흔히 롤백이라고 부르는건 데이터베이스가 롤백을 진행해주는 것인데 이 경우는 롤백인 것 "처럼" 보이는 것이죠. 

 

이런 것 처럼 실패한 것에 대한 보상을 주는 것을 보상 트랜잭션이라고 하고 이를 Saga패턴중 Choreographed 방식이죠. 이것 말고 Orchestration 방식도 있지만 일단 Choreographed만 소개했습니다. 

 

Saga패턴에 대한 자세한 내용은 아래의 포스팅에서 자세히 다루고 있으니 아래의 포스팅을 참고해주세요!

 

https://coding-review.tistory.com/484

 

@Transactional로 분산 트랜잭션을 구현할 수 있을까?

약 2년전 시작했던 프로젝트인 온라인 쇼핑몰 프로젝트는 제가 취직을 함으로써 종료되었고 더이상 건드리지 않았습니다. 하지만 사이드프로젝트로서 나중에 이직할때 도움이 되고자 코드 리

coding-review.tistory.com

 

그런데 뭔가 이상하죠. 그럼 어떻게 된다는 얘기인가요? 

 

1번에서 커밋이 된 상황에서 2번이 실패하기 전에 만약 어떤 사람이 주문에 대한 정보를 열람하면 어떻게 되나요? 

 

결국 실패 중간에 롤백되기 전에 일관성이 깨진 데이터를 볼 수 있는 가능성이 생깁니다. 하지만 결국 일관성이 맞게 되는데 실패해서 카프카에 메세지를 실어서 주문서버에 되돌려보내 롤백된 것 처럼 보이면 이내 다시 조회하면 일관성이 맞을테니까요. 

 

그래서 이걸 BASE 속성 중 E에 해당하는 Eventual Consistency입니다. 결국 일관성이 맞는다는 이야기죠. 

 

이렇게 분산 시스템에서는 조금 더 복잡하고 느슨한 ACID가 동작하고 이런 느슨한 ACID를 BASE라고 따로 이름붙여 부르기도 합니다. 

 

Basically Available, Soft state, Eventual consistency 이렇게 BASE이죠. 

 

보통 NoSQL에서 다루던 이야기이지만 NoSQL는 분산 시스템에 최적화된 데이터베이스이기 때문에 분산 시스템에서의 RDBMS도 이를 피해갈 수 없었습니다. 

 

마치며

전통적인 ACID와 분산 시스템에서의 ACID는 뭔가 달랐습니다. 공부를 하면서 제가 공부했던 내용들을 상기하면서 쭉 정리하다보니 이런 차이가 있었고 이걸 한번 더 나만의 언어로 정리하기 위해서 포스팅을 작성했습니다. 

 

분산 시스템에서 운영 복잡도, 개발 복잡도가 올라간다는 사실은 누구나 알고 있었지만 이렇게 체감될 정도로 올라가는게 현실을 부정하고 싶을 정도더군요. 

 

이번 포스팅을 계기로 분산 시스템을 설계할 때는 조금 더 신중하게 설계해야할 것 같은, 그리고 개발할 때 조금 더 꼼꼼하게 개발해야할 것 같은 느낌이 들었습니다. 

 

이번 포스팅은 여기서 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~