← 모든 글
분산시스템

최종적 일관성을 믿지 않고 결제와 정산을 대조했다

결제 원장과 정산 대상을 같은 기준으로 집계해 차이를 지연·누락·중복·계약 불일치로 나누고, 자동으로 넘길 것과 사람이 봐야 할 것을 가른 기록이다.

결제와 정산을 나눈 뒤에 남은 질문은 하나였다. 두 쪽이 지금 맞는가. 이벤트가 전달됐고 소비자가 처리했다는 사실만으로는 답이 되지 않았다. 처리가 끝났다는 것과 결과가 맞다는 것은 다른 이야기였다.

대사 자체는 이때 처음 만든 것이 아니었다. Java 모놀리스 시절에도 결제와 정산 금액을 비교하는 대사를 안전장치로 두고 있었다. 다만 그때는 두 값이 같은 트랜잭션 안에서 만들어졌으니 대사가 확인할 것은 계산이 맞는지였다. 나누고 나서는 두 값이 서로 다른 시점에 만들어졌고, 대사가 답해야 할 질문이 늘었다.

최종적 일관성은 확인 방법이 아니다

결제와 정산을 분리하면서 최종적 일관성을 전제로 잡았다. 그런데 그 말은 시간이 지나면 맞아진다는 뜻이지, 지금 맞는지 알려 주는 말이 아니다. 운영자가 오늘 정산 금액을 물었을 때 언젠가 맞아진다고 답할 수는 없었다.

필요한 것은 두 가지였다. 지금 차이가 있는지 확인하는 방법, 그리고 차이가 있을 때 그것이 기다리면 되는 것인지 사람이 봐야 하는 것인지 가르는 기준이었다. 앞의 것이 없으면 문제를 모르고, 뒤의 것이 없으면 모든 차이가 장애가 된다.

무엇을 기준으로 삼을지부터 정했다

대사는 두 집계를 비교하는 일이다. 그래서 어느 쪽이 기준인지 먼저 정해야 했다. 돈이 실제로 움직인 근거는 결제 원장이므로 원장을 기준으로 두고, 정산 대상이 그것을 따라왔는지 확인하는 방향으로 잡았다.

기준을 정하지 않으면 차이가 났을 때 어느 쪽을 고쳐야 하는지도 정해지지 않는다. 양쪽을 대등하게 두면 매번 판단이 필요해지고, 그 판단이 사람마다 달라진다.

flowchart TD
  A[("결제 원장")] --> B["기준 집계"]
  C[("정산 대상")] --> D["대상 집계"]
  B --> E{"대조"}
  D --> E
  E --> F["차이 분류"]
  F --> G["자동 처리"]
  F --> H["사람 확인"]

대조 키는 결제 한 건을 가리키는 식별자와 기준일, 금액이었다. 셋 중 하나라도 기준이 어긋나면 맞는 데이터도 틀린 것으로 나온다.

집계 기준을 맞추는 일이 비교보다 먼저였다

대사에서 나오는 차이의 상당수는 데이터가 틀려서가 아니라 세는 방법이 달라서 생긴다. 그래서 비교하는 코드를 짜기 전에 양쪽이 같은 규칙으로 세고 있는지부터 확인했다.

맞춰야 할 기준어긋나면 생기는 일
날짜를 자르는 시각자정을 걸친 거래가 양쪽에서 다른 날에 들어간다
취소와 부분취소를 반영하는 시점금액이 서로 다른 단계의 값으로 비교된다
집계에 포함하는 상태한쪽만 불명확 상태를 세어 건수가 어긋난다
금액의 단위와 반올림합계에서만 아주 작은 차이가 계속 남는다

이 네 가지를 문장으로 먼저 적고 양쪽 구현이 그 문장을 따르는지 확인했다. 규칙이 코드에만 있으면 나중에 어느 쪽이 규칙을 어겼는지 판단할 근거가 없다.

차이를 네 가지로 나눴다

차이가 있다는 사실만으로는 무엇을 해야 할지 정해지지 않는다. 그래서 차이를 성격으로 나누고, 종류마다 다음 행동을 미리 정해 뒀다.

분류무엇인가다음 행동
지연아직 도착하지 않았을 뿐 맞아질 것허용 범위 안이면 기다린다
누락도착했어야 하는데 오지 않았다원본 사건을 찾아 재처리한다
중복같은 사건이 두 번 반영됐다어느 쪽이 유효한지 정하고 정리한다
계약 불일치값 자체가 서로 다른 규칙으로 만들어졌다코드를 고쳐야 한다

이 분류가 있어야 대사 결과를 볼 때 판단이 빨라진다. 숫자가 안 맞는다는 보고와 누락 세 건이라는 보고는 대응이 완전히 다르다.

지연은 시간이 지나면 사라진다 그래서 범위를 먼저 정했다

지연은 대사에서 가장 흔하게 나오는 차이다. 이벤트 기반으로 나눠 두었으니 두 쪽 사이에는 항상 시차가 있다. 문제는 어느 정도의 시차까지가 정상인가였다.

이 값은 기술로 정할 수 없었다. 정산 업무가 기다릴 수 있는 시간이 얼마인지가 기준이었고, 그건 운영에 물어야 하는 값이었다. 그 범위를 정하고 나서야 지연과 누락을 구분할 수 있게 됐다.

범위를 넘은 지연은 지연으로 두지 않았다. 최종적 일관성이라는 이름으로 계속 기다리면 문제가 조용히 쌓인다. 넘어간 건은 누락과 같은 절차로 다뤘다.

누락과 중복은 재처리로 이어졌다

누락은 원본 사건을 찾아 다시 처리하면 되는 경우가 많았다. 여기서 중요한 것은 새로 만들지 않고 원래 사건의 식별자로 다시 처리하는 것이었다. 새 식별자로 만들면 그 순간 중복이 된다.

누락 처리
1. 대사에서 없는 건을 찾는다
2. 원본 사건과 실패 원인을 확인한다
3. 현재 비즈니스 상태가 재처리해도 되는 상태인지 본다
4. 같은 식별자로 다시 처리한다
5. 다음 대사에서 사라졌는지 확인한다

중복은 반대로 어느 쪽이 유효한지 정하는 판단이 필요했다. 자동으로 하나를 지우는 방식은 쓰지 않았다. 돈에 영향을 주는 값이라 잘못 지우면 되돌리기가 더 어렵다.

계약 불일치는 코드를 고쳐야 했다

가장 다루기 까다로운 것이 계약 불일치였다. 값이 늦게 오거나 빠진 것이 아니라, 양쪽이 서로 다른 규칙으로 값을 만들고 있는 상태다. 이건 재처리해도 같은 차이가 다시 나온다.

그래서 계약 불일치로 분류된 건은 재처리 대상에서 빼고 따로 모았다. 대사를 반복해서 돌려도 사라지지 않는 차이가 있다면 그건 운영 문제가 아니라 구현 문제라는 신호였다.

자동으로 넘길 것과 사람이 볼 것을 갈랐다

모든 차이를 자동으로 해결하려고 하지 않았다. 자동 처리는 규칙이 분명한 경우에만 두고, 나머지는 사람이 확인하는 목록으로 보냈다.

flowchart TD
  A["대사 결과"] --> B{"허용 범위 안의 지연인가?"}
  B -- "그렇다" --> C["다음 대사까지 기다린다"]
  B -- "아니다" --> D{"자동 재처리 조건을 만족하나?"}
  D -- "그렇다" --> E["같은 식별자로 재처리"]
  D -- "아니다" --> F["사람 확인 목록"]
  E --> G["다음 대사에서 확인"]
  F --> G

사람이 봐야 하는 경우는 정해져 있었다. 계약에 없는 상태로 들어온 건, 운영 보정으로 원장이 바뀐 건, 외부 결과가 끝까지 불명확한 건이었다. 이 세 가지는 자동으로 판단하면 오히려 원장을 틀리게 만든다.

대사 결과 자체를 기록으로 남겼다

대사를 돌리고 차이를 고치는 것으로 끝내지 않았다. 언제 돌렸고, 무엇이 몇 건 나왔고, 어떻게 처리했는지를 남겼다. 이 기록이 없으면 지금 맞다는 사실은 알아도 그동안 맞았는지는 알 수 없다.

기록이 쌓이자 다른 것도 보이기 시작했다. 특정 시간대에 지연이 몰리는지, 어떤 유형의 차이가 반복되는지가 드러났다. 반복되는 차이는 대사로 잡을 것이 아니라 원인을 고쳐야 하는 것이었다.

남은 한계

대사는 차이를 발견하는 장치지 차이를 만들지 않는 장치가 아니다. 대사가 잘 돌수록 안심하게 되는데, 실제로 필요한 것은 대사에서 걸리는 건수가 줄어드는 쪽이었다.

대사 자체의 정확성도 계속 확인해야 했다. 집계 기준이 한쪽만 바뀌면 대사가 멀쩡한 데이터를 차이로 보고하거나, 반대로 진짜 차이를 놓친다. 기준을 바꾸는 변경은 대사 코드와 함께 봐야 했다.

결제와 정산을 나누면서 얻은 것은 독립적인 배포와 확장이었고, 대신 맞는지 확인하는 일을 훨씬 넓혀야 했다. 대사는 그 비용에 해당한다. 나누기로 한 이상 피할 수 있는 비용이 아니라 처음부터 같이 설계해야 하는 부분이었다.