LabHub
배우기 러닝패스 코스

은행 현장의 언어 · 누가 언제 무엇을 바꿨는가 · 이론

한도는 올라가 있는데 올린 사람이 없다

LabHub 에서 이어서 보기

한 줄 요약

감사 추적은 값을 저장하는 일이 아니라 나중에 다툴 수 있는 형태로 남기는 일이다. 제자리 UPDATE 대신 추가만 되는 기록을 두고, 정규 직렬화로 같은 사건이 늘 같은 바이트가 되게 만든 다음, 해시 사슬과 열쇠로 "중간에 손대면 드러나는" 성질을 붙인다.

왜 이게 필요했나

은행에서 감사 대응이 어려워지는 순간은 대개 값이 틀렸을 때가 아니다. 값은 맞는데 그 값이 어떻게 거기까지 왔는지 아무도 설명하지 못할 때다. 여신한도 표가 UPDATE customer_limit SET limit_amount = ? WHERE customer_id = ? 한 줄로 갱신되는 시스템을 생각해 보자. 이 문장이 도는 순간 옛 값은 사라진다. 남는 것은 updated_atupdated_by 컬럼뿐인데, 그마저도 마지막 한 번의 흔적이라 그 앞에 몇 번을 거쳐 왔는지는 알 수 없다.

현장에서 이 문제는 이렇게 나타난다. 월말 점검에서 한도 총액이 월초보다 4억 8천만 원 늘어 있다. 변경 신청서를 세어 보니 건수가 맞지 않는다. 몇 건은 신청서가 아예 없고, 몇 건은 신청서에 적힌 승인 금액과 지금 값이 다르다. 이때 담당자에게 물어보면 대개 정직한 답이 돌아온다 — "배치로 일괄 반영했습니다." 그 배치가 무엇을 했는지는 어디에도 없다. 감사에서 가장 비싼 값은 틀린 숫자가 아니라 설명할 수 없는 숫자다.

미국 NIST 의 [SP 800-92 로그 관리 지침](https://csrc.nist.gov/pubs/sp/800/92/final)이 로그를 다루며 반복해서 말하는 것도 같은 것이다([전문 PDF](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-92.pdf)). 로그는 생성하는 것으로 끝나지 않고 보관·보호·검증까지 한 묶음으로 설계해야 하며, 무결성이 지켜지지 않은 로그는 증거로서의 값이 크게 떨어진다. 한국 금융권 실무의 구체적 보존 연한은 기관마다 다르므로 이 실습에서는 다루지 않고, 기록을 어떻게 만들면 나중에 증명할 수 있는가 에만 집중한다.

어떻게 동작하나

첫째, 추가만 되는 표를 둔다. 상태를 덮어쓰는 표(현재 한도)와 사건을 쌓는 표(감사 기록)를 따로 둔다. 현재 값은 빠른 조회를 위한 파생물이고, 진실은 사건의 나열이다.

둘째, 사건을 같은 바이트로 만든다. 해시는 바이트에 대해 계산되므로 {"a":1,"b":2}{"b":2,"a":1} 은 같은 사건인데도 다른 해시가 된다. [RFC 8785 JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)이 이 문제를 규격으로 못박았다. 객체의 키를 정렬하고, 구분자 주변의 공백을 없애고, UTF-8 로 직렬화한다. 파이썬에서는 [json 모듈](https://docs.python.org/3/library/json.html)의 sort_keys=True, separators=(",", ":"), ensure_ascii=False 세 가지를 켜면 대부분의 실무 payload 에서 같은 결과가 나온다. 다만 RFC 8785 는 키 정렬을 UTF-16 코드 단위 기준으로 정의하고 파이썬은 코드 포인트로 정렬하므로, 이모지 같은 보조 평면 문자를 키로 쓰면 어긋난다. 실무에서는 키를 ASCII 로 제한하는 편이 안전하다.

셋째, 사슬로 잇는다. 항목마다 앞 항목의 해시를 prev_hash 로 품고, 자기 내용과 prev_hash 를 함께 해시해 entry_hash 를 만든다([hashlib](https://docs.python.org/3/library/hashlib.html)). 중간 한 줄을 고치면 그 줄의 entry_hash 가 어긋나고, 그 줄의 해시를 다시 계산해 끼워 넣으면 뒤 항목의 prev_hash 가 어긋난다. 사슬은 "고칠 수 없게" 만들지 않는다. 고치면 드러나게 만든다.

넷째, 사슬 위에 열쇠를 얹는다. 해시 사슬만 있으면 기록 전체에 접근할 수 있는 사람이 처음부터 끝까지 다시 계산해 통째로 갈아 끼울 수 있다. [hmac 모듈](https://docs.python.org/3/library/hmac.html)로 항목마다 MAC 을 만들고 그 열쇠를 기록과 다른 자리에 두면, 열쇠를 모르는 사람은 다시 계산할 수 없다. 열쇠가 같은 DB 안에 있으면 이 성질은 통째로 사라진다.

app_log(원본)  ──▶ canon(사건) ──▶ sha256 ──▶ entry_hash  ──┐                                   ▲                        │ prev_hash 로 다음 항목에                                   └──────── prev_hash ◀────┘                    entry_hash + prev_mac ──▶ HMAC(열쇠) ──▶ entry_mac

현장에서 만나는 모습

가장 흔한 실패는 상관 ID 가 없는 로그다. 한 번의 한도 변경이 창구 앱, 한도 서비스, 코어 배치, 감사 릴레이를 차례로 지나가는데 각 시스템이 자기 사건만 남긴다. 나중에 "이 변경이 어디서 시작됐나" 를 물으면 시각이 비슷한 줄들을 눈으로 이어 붙이는 수밖에 없다. 요청 하나에 ID 를 붙여 전 구간에 흘려보내면 조사 시간이 시간 단위에서 분 단위로 줄어든다. 반대로 중간에 한 시스템이 그 ID 를 떨어뜨리면 그 지점 이후는 다시 추측이 된다.

두 번째는 협력사에서 받은 반출본을 그대로 믿는 것이다. 사슬이 붙어 있다고 해서 검증했다는 뜻은 아니다. 받은 파일을 처음부터 다시 계산해 보면 내용만 고쳐 넣은 자리, 항목이 통째로 빠진 자리가 드러난다. 두 가지는 증상이 다르다. 내용을 고치면 그 줄의 해시가 어긋나고, 항목을 빼면 그 다음 줄의 연결이 어긋난다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

한들은행(가상)의 한도 스냅샷을 직접 만들고, 신청서 없이 바뀐 건과 승인값과 다르게 반영된 건을 숫자로 드러냅니다. 그 다음 정규 직렬화 도구를 만들어 채점기가 매번 다르게 섞어 주는 벡터에 맞추고, 로그 400줄을 해시 사슬로 묶습니다. 협력사 반출본에서 손이 탄 자리를 찾아내고, HMAC 으로 재계산 위조를 막고, 상관 ID 로 사건을 요청 단위로 되묶은 뒤, 해시가 붙은 증적 묶음을 냅니다.