컬럼이 있다고 값이 채워지는 건 아니다 — AI 거버넌스 메타데이터 검증법
AI 에이전트가 사람 대신 업무 데이터를 기록하는 시스템에서는 “누가·어떤 근거로 이 값을 썼는가”를 메타데이터로 남겨야 감사·재현·책임 추적이 가능합니다. 그런데 스키마에 컬럼을 추가하는 것과 그 컬럼에 실제로 값이 채워지는 것은 완전히 다른 문장입니다. SL.AIMS를 감사하다가 그 간극을 정면으로 만났습니다. 이 글은 그 간극이 왜 생기고, 어떻게 전수 검증하며, 어떤 표준 패턴으로 구현하는지의 기록입니다.
”스키마 완료”라는 착시
섹션 제목: “”스키마 완료”라는 착시”AI 에이전트가 업무 데이터를 직접 쓰는 시스템을 설계하다 보면 거버넌스 메타데이터 컬럼을 도입하게 됩니다. 예를 들면 이런 필드들입니다 — aiSuggested(AI가 제안했는가), aiConfidence(확신도), aiDecisionId(어떤 판단 로그에 연결되는가), inputActorType(사람/AI/배치 중 누가 썼는가), humanOverridden·humanOverrideReason(사람이 뒤집었는가, 그 이유는). 이 컬럼들이 있어야 나중에 “이 값은 누가, 무슨 근거로 넣었나”를 되짚을 수 있습니다. 이 칸들을 왜 처음부터 박아야 하는지는 〈에이전트 자가학습〉에 적었습니다.
스키마 마이그레이션은 한 번의 배포로 전체 테이블에 안전하게 적용됩니다. 반면 “그 컬럼에 값을 넣는 코드”는 도메인마다·write 지점마다 개별적으로 작성해야 합니다. 감사(audit) 리뷰에서 grep 한 줄이면 “쓰는 코드가 있다”와 “값이 실제로 대입된다”를 구분할 수 있는데, 컬럼 존재 여부만 확인하고 넘어가면 “99% 반영 완료”라는 숫자가 실제 데이터 품질을 전혀 보증하지 못합니다.
왜 간극이 생기는가 — 전역 자동화 vs 개별 책임
섹션 제목: “왜 간극이 생기는가 — 전역 자동화 vs 개별 책임”같은 “자동 기록 컬럼”이라도 구조가 다르면 누락 위험이 근본적으로 다릅니다.
감사 컬럼 (createdBy/updatedBy) | 거버넌스 컬럼 (aiSuggested 등) | |
|---|---|---|
| 누가 채우나 | 전역 인터셉터가 모든 요청에서 자동으로 | 각 서비스 코드가 개별적으로 기억해서 |
| 채우는 방식 | 요청 컨텍스트(AsyncLocalStorage 등)에서 현재 액터를 꺼내 자동 대입 | ”이 write는 AI가 한 거니까 aiSuggested를 true로”라고 write 지점마다 직접 작성 |
| 누락 위험 | 개발자가 실수로 빠뜨릴 방법이 구조적으로 없음 | 도메인이 늘어날수록 반복적으로 놓치기 쉬움 |
검증 방법론 — 조사 순서 자체가 자산이다
섹션 제목: “검증 방법론 — 조사 순서 자체가 자산이다”새 도메인·에이전트가 추가될 때마다, 또는 기존 도메인을 재검증할 때마다 아래 순서를 그대로 따릅니다.
- 전역 자동화 장치가 있는지 먼저 확인합니다. 비교 기준이 되는 자동 기록 장치(감사 인터셉터)를 찾고, 검증 대상 컬럼에 동등한 장치가 있는지 봅니다. 없다는 사실 자체가 “이건 개별 코드가 책임진다 → 개별 write 지점을 전수 조사해야 한다”는 다음 방향을 결정합니다.
- “실제 에이전트를 보유한 도메인” 목록을 먼저 확정합니다. 이전 감사가 만들어둔 도메인 목록이 있으면 처음부터 다시 찾지 말고 그것을 재검증 대상으로 재사용합니다. 기존 자산을 다시 만들지 않는 것이 조사 비용을 크게 줄입니다.
- 각 도메인에서 “컬럼을 실제로 write하는 코드”를 grep으로 전수 확인합니다. 스펙 파일·스키마 선언을 제외하고, 실제
data:절에 값을 대입하는 지점만 셉니다. 예:grep -rl "aiSuggested\s*:" --include="*.service.ts". 결과를 양호/부분/전무로 분류합니다. - “전무”가 곧 “결함”은 아님을 검증합니다 — 가장 중요한 단계입니다. write가 0건인 도메인을 곧바로 버그로 단정하지 않습니다. 그 에이전트가 애초에 업무테이블을 직접 쓰는 성격인지, 아니면 제안 큐·로그만 만들고 사람이 별도 정상 경로로 반영하는 순수 advisory형인지 코드를 끝까지 추적합니다. 이 단계를 생략하면 존재하지도 않는 코드에 억지로 컬럼을 끼워넣게 됩니다.
- 조사 중 발견한 “더 큰 문제”는 범위를 넓히지 말고 별도 기록합니다. 원래 스코프와 리스크 프로파일이 다른 문제(예: 신규 실행 로직 구현)를 그 자리에서 즉흥적으로 고치지 않습니다. 별도 이슈로 문서화하고, 원래 작업의 경계를 지킵니다.
- 모든 수정은 라이브로 검증합니다 — 유닛테스트로 끝내지 않습니다. 코드가 실제 서비스 계층에 도달하는지, 가능한 곳은 실제 호출 경로로, 시간창이 필요한 배치는 합성 데이터를 과거 시점으로 backdate해 실제 트리거를 통과시켜 확인합니다.
표준 구현 패턴 — 새로 설계하지 말고 복제하라
섹션 제목: “표준 구현 패턴 — 새로 설계하지 말고 복제하라”검증 결과 “보완 필요”로 판정된 지점은 아래 두 패턴 중 하나로 처리합니다. 세 번째 경우는 아무것도 하지 않습니다.
패턴 A — 전용 write 지점 (에이전트만 호출)
섹션 제목: “패턴 A — 전용 write 지점 (에이전트만 호출)”해당 서비스 메서드를 오직 에이전트만 호출하고 사람 REST 경로가 공유하지 않는다면, 그 create/update의 data 절에 거버넌스 컬럼을 직접 포함시킵니다. 가장 단순하고 안전합니다.
패턴 B — 공유 write 지점 (사람 경로와 서비스 메서드 공유)
섹션 제목: “패턴 B — 공유 write 지점 (사람 경로와 서비스 메서드 공유)”같은 서비스 메서드를 사람 REST 경로도 호출한다면, 메서드 내부를 침습하지 않습니다. 대신 호출부(에이전트 도구 핸들러)에서 본 업무를 완료한 뒤, 얇은 후속 태깅 update를 비차단(best-effort)으로 덧붙입니다. 이렇게 하면 사람 경로를 오염시키지 않으면서 AI 경로만 메타데이터를 남깁니다.
”값이 없음”이 곧 “결함”은 아니다 — advisory 판정 절차
섹션 제목: “”값이 없음”이 곧 “결함”은 아니다 — advisory 판정 절차”순수 advisory형 에이전트는 업무테이블을 직접 쓰지 않습니다. 이걸 결함으로 오판하면 없는 문제를 만듭니다.
- 에이전트가 만드는 큐/로그 타입을 찾습니다. 해당 에이전트가 생성하는 작업 큐(HITL task, decision log 등)의 타입 식별자를 확인합니다.
- 그 타입을 실제로 “소비/적용”하는 코드가 있는지 전수 grep합니다.
grep -rn "TASK_TYPE_NAME" src/로 정의부 외에 참조가 있는지 봅니다. - 소비 코드가 없다면 → advisory 확정. 사람이 그 제안을 참고만 하고, 완전히 별개의 정상 CRUD 화면에서 직접 처리한다는 뜻입니다. 코드 변경이 불필요합니다. 단, 이 결론을 문서로 남깁니다.
- 소비 코드가 있는데 미완성(dead-end)이라면 → 컬럼 문제가 아니라 더 심각한 별도 기능갭입니다. (아래 “더 큰 발견” 참조)
라이브 검증 — 유닛테스트로 끝내지 않는다
섹션 제목: “라이브 검증 — 유닛테스트로 끝내지 않는다”DI가 성립하고 타입 컴파일이 통과해도, 실제 실행 흐름에서 값이 채워지는지는 다른 문제입니다.
| 상황 | 검증 방법 |
|---|---|
| 즉시 실행되는 도구 | 실제 자연어 지시 → 에이전트가 실제 도구를 호출하는지, DB에 반영되는지 쿼리로 직접 확인 |
| 승인(HITL) 후 실행되는 도구 | 승인 큐 생성까지 확인 + 승인 후 실행 경로가 실제 디스패처에 매핑되는지 코드로 확인 |
| 관찰기간이 필요한 배치 | 합성 레코드를 관찰기간 이전 시점으로 backdate해 만들고, 수동 트리거로 실제 배치 로직을 통과시킨 뒤 결과 확인 → 테스트 데이터 정리 |
| 순수 관측/알림 배치 | 동일 집계 SQL을 직접 실행해 코드의 집계 결과와 일치하는지 대조 + 설정 화면에 정상 노출되는지 확인 |
더 큰 발견 — 얕은 조사였다면 놓쳤을 것
섹션 제목: “더 큰 발견 — 얕은 조사였다면 놓쳤을 것”“컬럼이 채워지는가”를 확인하려고 코드를 끝까지 추적하다 보면, 더 근본적인 질문에 자연스럽게 도달합니다.
HITL(사람 승인 개입) 구조에서 흔히 발생하는 위험 패턴이 있습니다 — AI가 제안하고 승인 큐까지는 정상 작동하는데, 승인 이후 실제로 그 작업을 실행하는 코드가 없는 dead-end입니다. 새 도구를 추가할 때마다 “제안 → 승인 큐”까지만 만들고 실행 디스패처 등록을 빠뜨리기 쉽습니다. 사람이 처리하는 정상 업무 화면이 AI 경로와 무관하게 따로 존재하면, 이 결함은 겉으로 드러나지 않은 채 오래 방치됩니다.
최종 체크리스트 — 신규 에이전트·도구를 추가할 때
섹션 제목: “최종 체크리스트 — 신규 에이전트·도구를 추가할 때”- 이 에이전트가 업무테이블을 직접 쓰는가, 순수 advisory인가를 먼저 결정하고 설계 문서에 명시합니다. 나중에 다른 사람이 추측하지 않도록.
- 직접 쓴다면, 그 write 지점에서 최소
aiSuggested+inputActorType을 처음부터 함께 작성합니다. 나중에 “보완”하러 오지 않도록. - 사람 REST와 서비스 메서드를 공유한다면, 메서드 내부를 침습하지 말고 호출부에서 후속 태깅합니다. 메서드가 어디서 호출되는지 모르는 채로 파라미터를 끼워넣으면 인간 경로까지 오염시킬 위험이 있습니다.
- HITL이 필요한 도구라면, “승인 후 실제로 무엇을 실행하는가”의 코드가 진짜 존재하는지 처음부터 확인합니다. 새 도구를 추가할 때마다 큐만 만들고 실행 구현을 빠뜨리는 dead-end가 반복되지 않도록.
- 배포 후 반드시 라이브로 확인합니다. 가능하면 실제 호출 경로로, 안 되면 합성 데이터로. 컴파일 통과와 실제 실행은 다른 문제입니다.
- write할 코드가 없다는 결론(n/a)에 도달했다면, 왜 없는지를 문서로 남깁니다. “없음”과 “확인했는데 없음”은 미래의 재조사 비용에서 천지차이입니다.
이 글은 SL.AIMS를 만들며 겪은 현장 회고 중 하나입니다. 전체 그림은 〈사례연구: SL.AIMS〉에 있습니다.