Skip to content

27만 건 무중단 데이터 이관, 대사 스케줄러로 정합성 지키기

목차

이전 회사에서 외부 업체가 운영하던 자산관리 시스템에 기대고 있던 데이터를 우리 쪽 시스템으로 옮기는 일을 맡았습니다. 건수로는 약 27만 건이었고, 서비스는 멈추지 않아야 했습니다.

이관은 회사 차원에서 정한 일이었고, 저는 그걸 받아 실행을 맡았습니다. 그래서 이 글은 “왜 옮기기로 했나”보다 “정해진 일을 어떻게 안전하게 끝냈나”에 대한 이야기입니다. 업체 이름이나 내부 시스템 이름, 세부 수치는 쓰지 않고 “외부 자산관리 시스템”, “외부 API” 정도로 부르겠습니다.

무중단이라는 조건

데이터 이관은 대부분 “옮기고 나서 맞는지 확인하는” 일로 끝나는 것처럼 보입니다. 서비스를 잠깐 세울 수 있다면 실제로 그렇게 해도 됩니다. 점검 시간을 잡고, 그 안에 전부 옮기고, 건수와 합계를 맞춰 보고, 이상이 없으면 문을 엽니다.

이번에는 그럴 수 없었습니다. 이관하는 동안에도 사용자는 서비스를 쓰고, 그 요청은 계속 외부 시스템이나 우리 시스템으로 들어옵니다. 그러면 이관하는 동안에도 데이터가 계속 바뀔 수 있다는 뜻이 됩니다. 어제 복사한 사본은 오늘 기준으로 일부가 틀려 있을 수 있습니다.

이 조건 때문에 문제의 성격이 달라진다고 생각했습니다. 한 번에 정확히 옮기는 일이 아니라, 옮기는 도중과 옮긴 직후에 생기는 어긋남을 계속 찾아서 바로잡는 일이 됩니다. 그래서 이번 일의 중심은 데이터를 옮기는 스크립트보다, 정합성을 검증하고 어긋난 건을 자동으로 복구하는 장치 쪽에 있었습니다.

한 번에 옮기지 않았습니다

첫 번째로 한 일은 순서를 나눈 것입니다. 1차로 위험이 낮은 소규모 사이트를 먼저 옮기고, 2차로 규모가 크고 실패 비용이 큰 사이트를 옮겼습니다. 전체 이관에는 1주일이 걸렸고, 그 사이 서비스는 멈추지 않았습니다.

이렇게 나누는 이유는 단순합니다. 작은 곳에서 처음 터지는 문제는 작게 터집니다. 이관 로직이나 대조 로직에 빈틈이 있다면 그것을 큰 사이트에서 처음 만나고 싶지 않다는 것이 이런 순서의 일반적인 논리입니다. 롤아웃을 위험이 낮은 쪽에서 높은 쪽으로 가는 순서로 잡는 것은 새로운 아이디어는 아니고, 제가 평소 변경을 적용할 때 쓰는 기본 순서이기도 합니다.

대사 스케줄러가 한 일

핵심은 대사 스케줄러였습니다. 대사(Reconciliation)는 원래 회계에서 쓰는 말로, 내 장부와 상대방 장부를 나란히 놓고 한 줄씩 맞춰 보는 일입니다. 은행 통장 정리와 같습니다. 내 가계부에는 3만 원 지출이라고 적혀 있는데 통장에는 찍혀 있지 않다면, 둘 중 하나가 틀린 것이고 그 이유를 찾아야 합니다.

이관 상황에 대입하면 이렇습니다. 우리 시스템이 처리한 결과가 우리 장부입니다. 외부 시스템이 알고 있는 상태가 상대방 장부입니다. 둘이 같으면 넘어가고, 다르면 조사합니다.

스케줄러가 하는 일의 흐름은 다음과 같았습니다.

작업이 한 번 돌고 나면 처리되지 않고 남은 건이 생깁니다. 스케줄러는 이 미처리 건을 먼저 자동으로 다시 시도합니다. 재시도로도 처리되지 않는 건은 DLQ 테이블로 옮깁니다. DLQ는 Dead Letter Queue의 약자로, 처리하지 못한 건을 버리지 않고 따로 모아 두는 보관함이라고 보시면 됩니다. 그다음에 외부 API를 다시 호출해서 외부의 응답을 받고, 그 응답을 우리 장부와 비교합니다.

이 비교의 목적은 “실패했다”는 사실 하나로 뭉뚱그려진 건을 세 갈래로 나누는 것이었습니다. 진짜 실패, 타임아웃, 일시적인 오류입니다.

이 구분이 왜 필요한지는 외부 API를 다뤄 보신 분이면 바로 공감하실 겁니다. 호출이 타임아웃으로 끝났다고 해서 상대방이 그 요청을 처리하지 않았다는 보장이 없습니다. 응답만 늦거나 유실됐을 뿐 상대방에서는 이미 반영됐을 수 있습니다. 이때 실패로 보고 다시 처리하면 중복이 생기고, 반대로 성공으로 보고 넘어가면 누락이 생깁니다. 일시적인 오류는 잠시 뒤 다시 하면 되는 것이고, 진짜 실패는 사람이 봐야 합니다. 외부 응답과 우리 장부를 직접 대조하면 이 셋을 추측이 아니라 근거를 가지고 가를 수 있습니다.

정리하면 스케줄러 하나가 정합성 검증과 자동 복구를 같이 맡은 구조였습니다. 일반적으로 어긋난 것을 찾는 일과 고치는 일이 따로 있으면 그 사이에 틈이 생기고, 그 틈은 결국 사람이 메우게 됩니다.

같은 모양을 일반화하면

여기서부터는 대사 로직을 짤 때 보통 신경 쓰는 부분을 일반화해서 적어 보겠습니다.

대조 기준을 무엇으로 삼을지가 먼저입니다. 건별로 양쪽이 공통으로 가진 식별자가 있어야 하고, 비교하는 필드도 정해야 합니다. 상태만 비교할지, 금액이나 수량까지 볼지는 업무마다 다릅니다. 아래는 개념만 보여 주는 예시입니다.

-- 예시: 우리 장부 기준으로 외부 응답과 어긋난 건을 찾는 개념
SELECT l.ref_id, l.status AS ours, e.status AS theirs
FROM ledger l
LEFT JOIN external_snapshot e ON e.ref_id = l.ref_id
WHERE e.ref_id IS NULL
   OR NOT (l.status <=> e.status); -- MySQL NULL-safe 비교

INNER JOIN만 쓰면 상대편에 없는 행이 비교 대상에서 사라집니다. 위 쿼리는 우리 쪽 기준 누락과 상태 차이를 찾습니다. 반대로 외부에만 있는 행은 방향을 뒤집은 anti-join으로 별도 점검해야 합니다. 스냅샷의 기준 시각도 맞춰야 정상적인 처리 중 상태를 오류로 오인하지 않습니다.

실제로는 외부 응답이 SQL로 오는 일은 거의 없고, API로 받아 온 값을 임시 테이블이나 메모리에서 대조하게 됩니다. 어느 쪽이든 하는 일은 같습니다.

두 번째로 자동 복구의 범위를 좁게 잡는 것이 좋다고 생각합니다. 자동으로 고쳐도 되는 것은 어느 방향으로 고쳐도 위험하지 않다고 확신할 수 있는 종류의 불일치뿐입니다. 근거가 모호한 건은 고치지 말고 DLQ에 남겨서 사람이 보게 하는 편이 낫습니다. 대조 로직이 자신 있게 틀리는 경우가 가장 위험하기 때문입니다.

세 번째는 재시도가 같은 요청을 두 번 만들지 않게 하는 것입니다. 재시도는 중복 처리와 맞닿아 있어서, 재시도할 때마다 이미 반영됐는지 먼저 확인하는 순서가 필요합니다. 조회 후 실행 사이에는 경쟁 조건이 남습니다. 조회만으로 중복 방지가 보장되지는 않습니다. DB의 유일성 제약이나 조건부 상태 전이로 실행 권한을 잡고, 외부 API가 지원한다면 동일한 멱등성 키를 재사용해야 합니다. 외부가 이미 성공했는데 로컬 커밋만 실패한 경우도 별도 대사 대상으로 남깁니다.

다른 곳에서도 쓴 비슷한 패턴

다른 업무에서도 비슷한 모양의 장치를 썼습니다. 외부 가상계좌 입금 신호를 받는 쪽에서, 신호는 받았는데 DB 저장이 실패하는 경우에도 1분 주기 스케줄러가 외부에 미처리 입금을 정기적으로 다시 조회하게 했습니다. 처리에 성공하면 해당 건의 플래그를 “처리됨”으로 바꿉니다. 당시에는 별도의 멱등성 키 대신 DB 플래그로 처리 상태를 관리했습니다. 다만 플래그를 읽고 나중에 바꾸는 구현만으로는 동시 실행을 막을 수 없습니다. 플래그 확인과 선점의 원자성, 외부 성공 후 DB 실패의 복구가 별도로 필요합니다.

이건 이관과는 별개의 업무이고, 대사라는 이름을 붙이면 같은 계열의 발상이라는 정도의 연결입니다. 이 구조는 따로 한 편으로 쓰는 것이 맞아서 여기서는 길게 다루지 않겠습니다. 외부 API 장애를 격리하는 서킷브레이커와 아웃박스 쪽도 마찬가지로 별도 글로 정리하려고 합니다.

전환 완료를 판단하는 기준

이 경험을 다른 시스템에 적용한다면 단순 건수 일치보다 아래 조건을 먼저 정의하겠습니다. 다음은 공개 가능한 설계 기준이며 당시 내부 점검표를 옮긴 것은 아닙니다.

  • 같은 기준 시각에서 양방향 누락, 핵심 필드 차이, 중복 키를 각각 집계합니다.
  • 재시도 대기와 영구 실패를 구분하고, 재시도 횟수·마지막 오류·외부 요청 식별자를 남깁니다.
  • 타임아웃 직후에는 외부 성공 여부부터 조회하고, 모호한 건은 자동 성공 처리하지 않습니다.
  • 전환 후 새 시스템에만 쌓인 변경을 어떻게 복구할지 정하기 전에는 단순히 연결 주소만 되돌리는 것을 롤백으로 보지 않습니다.

결과와 아쉬운 점

약 27만 건을 서비스 중단 없이 옮겼고, 외부 업체에 기대던 구조를 우리 시스템으로 바꿨습니다. 어긋난 건이 몇 건이었고 그중 몇 건을 자동으로 복구했는지 같은 숫자는 공개하기 어려워 적지 않습니다. 이관이 끝난 뒤 대사 스케줄러가 몇 달 동안 어떤 모양으로 돌았는지까지 지켜보지 못한 점은 아쉽습니다.

돌아보며

이 일에서 제가 가져온 기준은 두 가지입니다. 무중단이 조건이면 정확히 옮기는 것만큼 어긋남을 계속 찾아 고치는 장치가 중요하다는 것, 그리고 외부 응답은 추정하지 않고 직접 불러서 우리 장부와 맞대 본다는 것입니다.

비슷한 이관을 앞두고 계신다면 참고만 하시면 됩니다. 저라면 옮기는 스크립트를 쓰기 전에, 어떤 식별자로 짝을 짓고 무엇이 같으면 같다고 볼지부터 적어 두고 시작할 것 같습니다.

읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github