감사 추적의 문제: 어떤 규칙이 어떤 결정을 내렸는지 증명하기
막힌 이체 하나가 로그에 남기는 건 한 줄뿐입니다. 몇 달 뒤 감사관이 이유를 묻지만 git은 답하지 못합니다. 코드의 이력은 남겨도 결정의 이력은 남기지 않기 때문입니다. 제대로 된 결정 감사 추적이 무엇을 기록하고, 왜 불변이어야 하는지 설명합니다.
이체 한 건이 막혔습니다. 보험 청구 한 건이 거절됐습니다. 주문에 붙어야 할 할인이 붙지 않았습니다. 도메인도 팀도 다른 세 사건이지만 구조는 같습니다. 시스템이 고장 난 게 아니라 지시받은 대로 동작한 결과입니다. 어딘가에 있는 규칙이 조건을 보고 판정을 내렸고, 그 판정이 그대로 나갔습니다.
그리고 남은 산출물은 blocked: rule failed 한 줄입니다. 사정이 더 나쁘면 이것도 없이 에러 코드와 타임스탬프만 찍혀 있습니다.
사건 당일에는 이게 문제가 되지 않습니다. 당직을 서던 엔지니어의 머릿속에 어떤 규칙이 걸렸는지, 임계값이 얼마였는지, 요청에 무엇이 실려 있었는지가 다 들어 있으니까요. 물어보면 답이 나옵니다. 문제는 그 지식이 사람에게만 있고 기록에는 없다는 것입니다.
시간이 지나면 상황이 뒤집힙니다. 몇 달 뒤에 같은 질문이 다시 들어오면 남아 있는 건 그 한 줄뿐이고, 그 한 줄로는 답할 수 없습니다. 당직이었던 사람은 다른 팀으로 옮겼거나, 자리에 있어도 그날 자기가 무엇을 봤는지 기억하지 못합니다.
이걸 모르는 팀은 없습니다. 다만 순서에서 늘 밀립니다. 그런 일이 생기면 그때 처리하자고 미루고, 그리고 그 일은 실제로 생깁니다.
감사관이 실제로 던지는 질문
질문은 대체로 세 경로로 옵니다. 내부 규제 준수 점검, 고객이 제기한 분쟁, 규제 기관의 자료 요구입니다. 어느 쪽이든 질문은 두루뭉술하지 않습니다. 이런 식으로 들어옵니다.
- 그날 이 이체가 왜 막혔는지 설명해 주세요
- 그날 적용된 규칙이 정확히 무엇이었는지 알려 주세요
- 이 거절의 근거를 보여 주세요
- 이 규칙을 누가 언제 바꿨는지 알려 주세요
네 질문 모두 답은 원리적으로 이미 정해져 있습니다. 결정은 이유가 있어서 내려졌고, 그 이유는 실행 시점에 기록됐거나 기록되지 않았거나 둘 중 하나입니다. 답할 수 있느냐를 가르는 변수는 하나입니다. 결정을 실행하는 그 순간에 이유를 함께 적어 뒀는지, 아니면 1년 뒤에 사람이 앉아서 되짚어 맞추기를 기대했는지입니다.
여기에 기한이 붙습니다. 규제 기관은 며칠 안에 근거를 요구하고 몇 달을 기다려 주지 않습니다. 고객 분쟁도 같습니다. 기한 안에 내놓을 말이 아직 짜 맞추는 중이라는 것뿐이라면 그 시점에 이미 진 겁니다. 한도와 심사, 승인 판정을 규칙으로 다루는 금융 도메인에서는 이 기한이 더 짧습니다.
git과 코드로는 답할 수 없는 이유
엔지니어의 첫 반응은 대체로 같습니다. git에 다 있으니 이력을 보면 된다는 것입니다.
여기서 갈립니다. git이 쥐고 있는 것은 코드의 이력이고, 감사관이 요구하는 것은 결정의 이력입니다. 이름이 비슷할 뿐 서로 다른 기록입니다.
규칙이 코드베이스 안에 있었던 경우부터 봅니다. 이때 git이 말해 줄 수 있는 건 그날 규칙이 아마 이랬을 것이라는 데까지입니다. 그것도 맞는 브랜치와 맞는 커밋을 제대로 찾아냈을 때 이야기입니다. 그리고 다음 세 가지는 git 어디에도 없습니다. 그 이체가 실제로 들고 있던 입력, 그 요청에서 실제로 실행된 갈래, 규칙이 그 요청에 돌려준 값입니다.
코드는 틀입니다. 결정은 그 틀을 지나가며 그 순간 벌어진 사건입니다. git은 틀의 버전을 관리하고 사건에는 아무 말도 하지 않습니다.
규칙이 코드 밖에 있었던 경우는 더 나쁩니다. config 테이블, 관리자 화면의 설정값, 기능 플래그처럼 배포 없이 바꾸는 자리에 비즈니스 로직이 꽤 많이 들어가 있습니다. 여기서는 git이 보여 줄 게 아예 없습니다. 그날 살아 있던 값은 다음 변경이 덮어썼고, 이전 값은 그대로 사라졌습니다.
두 경우의 결말은 같습니다. 원인 규명이 발굴 작업이 됩니다. 커밋을 파고, 그날의 입력을 추측하고, 로그 한 줄에 기댑니다. 발굴이 내놓는 결론에는 언제나 아마도가 붙습니다. 감사관은 아마도를 받지 않습니다.
여기까지 오면 반론이 두 개 나옵니다.
첫째는 로그를 더 남기면 되지 않느냐는 것입니다. 방향 자체는 맞습니다. 결정 감사 추적의 뼈대가 실제로 그것입니다. 다만 필요해 보일 때마다 찍는 방식으로는 버티지 못합니다. 입력과 발동한 규칙만으로는 부족하고, 평가는 됐지만 발동하지 않은 규칙과 그 이유, 그 순간 살아 있던 로직의 정확한 버전까지 실행하는 그날 함께 남겼어야 합니다. 이 중 하나라도 당일에 빠뜨리면 그 항목은 영영 사라집니다. 나중에 되살릴 방법이 없습니다. 게다가 로그는 자유 형식 텍스트입니다. 검색할 수 있다는 것과 증명할 수 있다는 것은 다른 말입니다.
둘째는 애플리케이션이 결정을 내리면서 옆에 별도 감사 테이블을 함께 쓰는 방식입니다. 이 구조는 로직과 테이블이 어긋나기 전까지만 동작합니다. 어긋나는 방식은 뻔합니다. 새 코드 경로를 추가하면서 기록을 빠뜨리거나, 리팩토링이 규칙이라는 말의 의미를 바꿔 놓았는데 옆에 붙은 로깅은 예전 그대로 남습니다. 그 순간부터 감사 기록과 실제 결정은 서로 어긋날 수 있는 두 출처가 됩니다. 어긋나는 순간 어느 쪽도 증거가 아닙니다.
믿을 수 있는 기록에는 조건이 하나 있습니다. 결정이 내려지는 순간에 그 부산물로 저절로 남아야 합니다. 부산물이 되느냐 마느냐는 팀이 얼마나 성실한지가 아니라 결정이 실행되는 기반이 어떤 성질인지에 달려 있습니다. 비즈니스 로직을 애플리케이션 밖으로 꺼내 운영 대상으로 두는 접근이 나온 이유가 여기에 있습니다.
결정 감사 추적이 기록하는 것
방법은 하나뿐입니다. 결과만 남기지 말고 그 결과를 낳은 결정 자체를 함께 남기는 것입니다. 결정 하나에 이를 때마다 거기까지 온 경로를 트레이스로 온전히 적습니다.
트레이스에 들어가야 하는 건 네 가지입니다.
- 발동한 규칙, 그리고 평가는 됐지만 발동하지 않은 규칙. 각각 왜 조건이 맞았고 왜 어긋났는지까지
- 결정이 본 입력을, 본 그대로
- 그 순간 살아 있던 규칙 버전
- 실행 시각과 그 실행이 속한 배포
이게 있으면 질문의 성격이 바뀝니다. 이 건은 왜 막혔는지 알아내는 일이 재구성이 아니라 조회가 됩니다. 막힌 이체 기록을 열면 어떤 임계값이 막았고 그때 무엇을 봤는지가 함께 나옵니다. 거절된 청구를 열면 어떤 규칙이 거절했고 그 규칙이 어느 버전에 속해 있었는지가 나옵니다.
앞에서 말한 고객 분쟁으로 돌아가 봅니다. 재구성에 일주일씩 매달리던 일이 트레이스 하나를 여는 일로 바뀝니다. 답의 형태는 이렇습니다. 이 이체는 그 시점에 살아 있던 규칙 버전으로 평가됐고, 막은 것은 계좌별 한도 규칙이며, 그때 본 입력은 이렇습니다. 걸리는 시간은 기록 하나를 여는 시간입니다.
감사 추적은 이미 내려진 결정에 사후로 답하는 쪽입니다. 그 반대편에는 규칙을 바꾸기 전에 결과가 어떻게 달라지는지 미리 재보는 일이 있습니다. 둘은 한 쌍입니다.
두 번째 요건은 설계에서 자주 빠집니다. 트레이스가 담는 입력에는 민감한 항목이 섞여 있습니다. 이름, 계좌번호, 각종 식별자입니다. 열 때마다 이런 값이 그대로 드러나는 추적은 규제 준수 문제 하나를 다른 규제 준수 문제로 바꿔 놓을 뿐입니다.
그래서 두 가지가 함께 필요합니다. 민감한 항목은 기본으로 마스킹돼 있어야 하고, 마스킹을 해제하는 행위 자체가 다시 기록으로 남아야 합니다. 누가 무엇을 언제 봤는지 말입니다. 감사 추적은 자기가 강제하려는 기준을 자기 자신에게도 적용해야 합니다.
기록은 고칠 수 없어야 하고 오래 남아야 합니다
조용히 고칠 수 있는 기록은 증거가 아닙니다. 감사 추적의 가치는 이 한 가지에 걸려 있습니다. 일이 끝난 뒤에 누구도 입맛에 맞게 손댈 수 없다는 것입니다.
형태는 단순합니다. 한 번 쓰면 수정할 수 없고, 추가만 되고 변경되지 않는(append-only) 기록입니다. 불변은 증거가 되기 위한 전제 조건입니다. 감사관이 가장 먼저 의심하는 것도 여기입니다. 나중에 결과에 맞춰 고쳐 놓은 기록이 아니냐는 의심인데, 애초에 고칠 수 없는 구조라면 그 의심 자체가 성립하지 않습니다.
두 번째 축은 보관 기간입니다. 기록은 질문보다 오래 남아 있어야 합니다. 분쟁은 몇 달 뒤에 오고, 규제 점검은 그보다 더 뒤에 옵니다. 몇 주 만에 잘려 나가는 추적은 정작 필요한 시점에 없습니다.
LexQ는 요금제와 무관하게 모든 결정을 불변 감사 로그에 남깁니다. 요금제에 따라 달라지는 것은 보관 기간뿐이고, 최대 730일, 2년입니다. 1년 반 전에 내린 결정도 오늘 아침 결정과 같은 정밀도로 답할 수 있다는 뜻입니다.
불변과 보관이 맞물릴 때 트레이스는 디버깅에 편한 도구를 넘어섭니다. 그때부터 밖에 내놓을 수 있는 답이 됩니다.
언제 제값을 하고 언제 과한 장치가 되는지
모든 팀에 필요한 장치는 아닙니다. 제값을 하는 조건은 네 가지입니다.
- 결정이 규제나 감사의 대상입니다. 대출, 결제, 보험, 의료처럼 기관이 결과의 근거를 요구할 수 있는 영역입니다
- 결정에 분쟁이 붙고, 왜 그렇게 됐는지 상대에게 보여 줘야 합니다
- 로직을 바꾸는 사람이 둘 이상입니다. 이때부터 누가 언제 바꿨느냐가 실제로 답해야 하는 질문이 됩니다
- 나중에 증명해 달라는 요구가 오늘 내리는 결정을 두고 이미 눈에 보입니다
세 번째는 요즘 무게가 달라졌습니다. 로직을 바꾸는 쪽에 사람만 있는 게 아니라 MCP로 규칙을 직접 고치는 AI 에이전트가 섞이기 시작하면, 누가 언제 무엇을 바꿨는지가 훨씬 무거운 질문이 됩니다.
반대로 과한 경우도 분명합니다.
- 감사나 규제에 노출될 일이 없고 앞으로도 없다면 굳이 필요하지 않습니다
- 결정이 한 사람 머릿속에 다 들어갈 만큼 작을 때도 그렇습니다. 거의 바뀌지 않고 처음 쓴 사람이 끝까지 쥐고 있는 로직이라면 로그 한 줄과 본인 기억으로 충분합니다. 그 위에 감사 추적을 얹으면 쓰지도 않을 장치를 하나 더 유지하는 셈입니다
결정 감사 추적은 언젠가 누군가 책임을 물을 결정을 위한 것입니다. 그런 결정이 아니라면 필요 없습니다.
LexQ는 이 문제를 이렇게 다룹니다
LexQ는 모든 결정을 완전한 트레이스와 함께 불변 감사 로그에 기록합니다. 어떤 규칙이, 어떤 버전에서, 어떤 입력으로 그 결과를 냈는지까지입니다. 실행 한 번마다 발동한 규칙과 발동하지 않은 규칙, 결정이 본 입력, 실행 시점에 살아 있던 버전과 배포가 함께 남습니다.
봉인되는 시점은 실행되는 그 순간입니다. 나중에 배포 이력이 바뀌어도 과거 결정의 출처는 흔들리지 않습니다.
콘솔에서는 이걸 의사결정 출처(Decision Provenance)라고 부릅니다. 결정 하나를 열면 그 결정과 입력, 규칙 버전, 그리고 누가 작성하고 누가 발행하고 누가 배포했는지의 책임 이력까지 한 화면에서 따라갈 수 있습니다.
민감한 입력은 기본으로 마스킹되고, 마스킹을 해제하면 그 행위가 다시 감사에 남습니다. 이력은 불변이고 최대 730일 보관됩니다. 오늘 내린 결정을 2년 뒤에도 증명할 수 있다는 뜻입니다.
핵심은 로그를 더 남기는 것이 아닙니다. 누가 묻기 전에 이미 준비된 답이 모든 결정에 붙어 있다는 것입니다.