덮어쓰지 말고 쌓아라: 불변 운영 데이터 설계
이력을 선명하게 하고 동기화를 단순화하는 추가 전용 상환 데이터 구조와, 그 대신 늘어나는 저장 및 조회 비용을 살펴본다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
하나의 가변 행은 “현재 상태가 무엇인가?”에 답하는 가장 쉬운 방법인 경우가 많다. 그러나 그 상태에 이른 과정은 지울 수 있다. 운영 이력을 동기화하고 조사하거나 설명해야 한다면 불변 레코드를 계속 추가하는 방식이 더 잘 맞을 수 있다. 단, 늘어나는 데이터 양과 조회 복잡성을 감당할 수 있어야 한다.
이 선택은 상환 흐름에서 잘 드러난다. 8월 2일에 상환이 실패하고 매일 재시도해 8월 7일에 성공한다고 하자. 이 과정을 저장하는 방법은 적어도 두 가지다. 어느 쪽도 모든 상황의 정답은 아니다. 복잡성을 지불하는 위치가 다를 뿐이다.
두 방식의 차이는 저장 공간만 비교해서는 보이지 않는다. 운영자가 실패 과정을 얼마나 자주 조사하는지, 검색이나 데이터 웨어하우스로 어떤 변경을 동기화하는지, API가 현재 상태를 얼마나 자주 요구하는지까지 함께 봐야 한다. 같은 상환 흐름이라도 주된 사용처가 다르면 더 단순한 구조가 달라질 수 있다.
한 행은 현재 상태를 싸게 보여준다
가변 구조에서는 하나의 행에 상환 상태와 미납 시도 횟수를 둘 수 있다. 재시도가 실패할 때마다 상태를 갱신하고 횟수를 늘린다. 마침내 결제에 성공하면 같은 행을 SUCCESS로 바꾼다.
이 표현은 간결하다. 현재 상태를 읽는 데 그 행 하나면 충분할 수 있다. 재시도할 때마다 저장 데이터가 늘어나지 않고, 최신 결과가 주로 필요한 API의 조회도 단순하다.
대신 과정이 사라진다. 횟수를 보고 여러 번 실패했다고 추측할 수는 있지만, 각 시도가 언제 있었고 정확히 언제 성공으로 전환됐는지는 그 행에서 보이지 않는다. 별도의 이력 테이블로 복구할 수 있지만, 그렇게 하면 현재 상태와 운영 이력에 서로 다른 저장 요구가 있음을 이미 인정한 셈이다.
데이터를 다른 곳에 복제할 때는 갱신도 중요해진다. 주 데이터베이스의 내용을 검색 시스템이나 분석용 저장소로 보낸다면 모든 상태 변경이 목적지에 도착해야 한다. 마지막 갱신이 누락되면 주 테이블은 성공인데 복제본은 계속 실패를 가리킬 수 있다. 빠진 갱신을 찾아 복구하는 운영 작업이 뒤따른다.
같은 행이 FAIL인 채로 횟수만 늘다가 마지막에 SUCCESS로 바뀌는 구조에서는 최종 갱신의 의미가 특히 크다. 중간 횟수 하나를 놓친 것과 성공 전환을 놓친 것은 결과가 다르지만, 동기화 쪽에서는 모두 같은 행의 갱신 누락으로 보일 수 있다. 어느 버전까지 전달됐는지 확인하고 빠진 변경을 다시 보내는 절차가 필요하다.
일어난 일을 설명하는 레코드를 추가하라
추가 전용 방식은 먼저 상환과 연체를 구분한다. 예정된 상환 시도는 실패로 끝난다. 그 실패로 연체 과정이 시작되고, 연체는 독립된 레코드와 재시도 날짜를 가질 수 있다.
8월 2일 실패 뒤에는 실패 결과와 다음 목표일인 8월 3일을 가진 연체 레코드를 추가할 수 있다. 8월 3일 재시도도 실패하면 다음 시도를 위한 레코드를 또 추가한다. 재시도가 성공할 때까지 레코드는 계속 쌓인다. 기존 행을 새로운 이야기로 덮어쓰지 않고, 새 행 하나가 과정의 한 단계를 보존한다.
다음 목표일은 기록용 날짜에 그치지 않는다. 다음 날 실행되는 연체 배치가 어떤 건을 다시 시도할지 표현한다. 8월 3일 시도도 실패하면 8월 4일을 목표로 한 새 레코드를 넣는 식이다. 시간순으로 쌓인 행에는 실패의 연속과 다음 실행 계획이 함께 남으므로, 누적 횟수만 있는 한 행보다 운영 과정이 직접 보인다.
이 구조에서는 주 데이터 자체에 이력이 드러난다. 운영자는 언제 재시도했고 해결까지 어떻게 진행됐는지 볼 수 있다. 하위 시스템도 같은 행의 반복된 변경을 추적하는 대신 삽입된 데이터를 동기화할 수 있다. 전달해야 할 단위가 불변 레코드이므로 누락을 추론하는 일도 더 직관적일 수 있다.
여기서 “불변”은 이런 운영 사실을 저장하는 방식을 가리킨다. 모든 테이블과 모든 도메인에서 갱신을 금지한다는 뜻은 아니다. 완료된 시도는 과거의 사실이고 다음 예정 시도는 새로운 사실이라는 생명주기에서 유용한 경계가 나온다.
쓰기 모델의 비용을 조회가 지불한다
추가 전용 저장이 복잡성을 없애지는 않는다. 복잡성이 이동한다. 가변 행에는 현재 상태가 바로 들어 있지만, 추가 전용 이력에서는 읽는 쪽이 상태를 계산해야 할 수 있다.
어떤 상환이 여전히 연체 중인지 답하려면 애플리케이션이 상환과 연체 데이터를 모두 읽고, 가장 최근의 유효한 레코드를 찾고, 해결되지 않은 실패와 복구 완료를 구분해야 할 수 있다. 인덱스와 적절한 쿼리로 개선할 수 있지만 상태 열 하나를 읽는 것보다는 구현이 복잡하다. 이력이 늘면 스캔 범위도 커질 수 있다.
시도 횟수에 따라 저장량도 증가한다. 연체가 드물고 대부분의 상환이 바로 성공한다면 추가 행은 감당할 만한 비용일 수 있다. 실패가 흔하거나 재시도가 잦다면 같은 구조가 많은 데이터를 만들 수 있다. 실패 빈도에 대한 가정은 부수적인 메모가 아니라 설계의 일부다.
이 예시에서는 대부분의 상환이 정상적으로 끝나고 연체가 많지 않을 것이라는 가정이 추가 전용 구조를 지탱한다. 정상 상환은 연체 레코드를 여러 개 만들지 않는다. 이 가정이 틀리면 저장량뿐 아니라 최신 상태를 찾기 위한 조회 범위도 함께 커진다. 도입 전에 연체 빈도와 건별 재시도 횟수를 추정하고, 운영 뒤에도 그 가정이 유지되는지 확인해야 한다.
워크로드와 운영 필요에서 선택하라
추가 전용 레코드를 도입하기 전에 절충안을 검증 가능한 질문으로 바꾸자.
- 얼마나 자주 재시도하며 한 건에서 몇 개의 행이 생길 수 있는가?
- 별도 이력을 재구성하지 않고도 운영자가 시도 순서를 봐야 하는가?
- 검색 시스템이나 분석 저장소로 변경 내용을 동기화해야 하는가?
- 어느 조회가 현재 상태를 요구하며, 이를 어떻게 효율적으로 계산할 것인가?
- 누락된 레코드나 끝나지 않은 과정을 팀이 어떻게 발견할 것인가?
조회가 거의 항상 최신 상태만 요구하고 이력의 운영 가치가 작다면 가변 행이 더 작은 선택일 수 있다. 시도 순서 자체가 중요하고 반복 갱신 때문에 동기화나 조사가 취약해진다면 사실을 계속 추가하는 비용을 정당화할 수 있다.
삽입이 갱신보다 우월하다고 선언하려는 것이 아니다. 어떤 정보를 계속 보이게 할지, 시스템이 어느 위치의 복잡성을 감당할 수 있는지 결정해야 한다. 이력을 의도적으로 보존하고, 데이터 양을 추정하고, 현재 상태 조회를 설계한 뒤 서비스의 실제 실패 패턴으로 두 경로를 모두 검증하자.