콘텐츠로 이동

컬럼이 있다고 값이 채워지는 건 아니다 — 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 지점마다 직접 작성
누락 위험개발자가 실수로 빠뜨릴 방법이 구조적으로 없음도메인이 늘어날수록 반복적으로 놓치기 쉬움

검증 방법론 — 조사 순서 자체가 자산이다

섹션 제목: “검증 방법론 — 조사 순서 자체가 자산이다”

새 도메인·에이전트가 추가될 때마다, 또는 기존 도메인을 재검증할 때마다 아래 순서를 그대로 따릅니다.

  1. 전역 자동화 장치가 있는지 먼저 확인합니다. 비교 기준이 되는 자동 기록 장치(감사 인터셉터)를 찾고, 검증 대상 컬럼에 동등한 장치가 있는지 봅니다. 없다는 사실 자체가 “이건 개별 코드가 책임진다 → 개별 write 지점을 전수 조사해야 한다”는 다음 방향을 결정합니다.
  2. “실제 에이전트를 보유한 도메인” 목록을 먼저 확정합니다. 이전 감사가 만들어둔 도메인 목록이 있으면 처음부터 다시 찾지 말고 그것을 재검증 대상으로 재사용합니다. 기존 자산을 다시 만들지 않는 것이 조사 비용을 크게 줄입니다.
  3. 각 도메인에서 “컬럼을 실제로 write하는 코드”를 grep으로 전수 확인합니다. 스펙 파일·스키마 선언을 제외하고, 실제 data: 절에 값을 대입하는 지점만 셉니다. 예: grep -rl "aiSuggested\s*:" --include="*.service.ts". 결과를 양호/부분/전무로 분류합니다.
  4. “전무”가 곧 “결함”은 아님을 검증합니다 — 가장 중요한 단계입니다. write가 0건인 도메인을 곧바로 버그로 단정하지 않습니다. 그 에이전트가 애초에 업무테이블을 직접 쓰는 성격인지, 아니면 제안 큐·로그만 만들고 사람이 별도 정상 경로로 반영하는 순수 advisory형인지 코드를 끝까지 추적합니다. 이 단계를 생략하면 존재하지도 않는 코드에 억지로 컬럼을 끼워넣게 됩니다.
  5. 조사 중 발견한 “더 큰 문제”는 범위를 넓히지 말고 별도 기록합니다. 원래 스코프와 리스크 프로파일이 다른 문제(예: 신규 실행 로직 구현)를 그 자리에서 즉흥적으로 고치지 않습니다. 별도 이슈로 문서화하고, 원래 작업의 경계를 지킵니다.
  6. 모든 수정은 라이브로 검증합니다 — 유닛테스트로 끝내지 않습니다. 코드가 실제 서비스 계층에 도달하는지, 가능한 곳은 실제 호출 경로로, 시간창이 필요한 배치는 합성 데이터를 과거 시점으로 backdate해 실제 트리거를 통과시켜 확인합니다.

표준 구현 패턴 — 새로 설계하지 말고 복제하라

섹션 제목: “표준 구현 패턴 — 새로 설계하지 말고 복제하라”

검증 결과 “보완 필요”로 판정된 지점은 아래 두 패턴 중 하나로 처리합니다. 세 번째 경우는 아무것도 하지 않습니다.

패턴 A — 전용 write 지점 (에이전트만 호출)

섹션 제목: “패턴 A — 전용 write 지점 (에이전트만 호출)”

해당 서비스 메서드를 오직 에이전트만 호출하고 사람 REST 경로가 공유하지 않는다면, 그 create/updatedata 절에 거버넌스 컬럼을 직접 포함시킵니다. 가장 단순하고 안전합니다.

패턴 B — 공유 write 지점 (사람 경로와 서비스 메서드 공유)

섹션 제목: “패턴 B — 공유 write 지점 (사람 경로와 서비스 메서드 공유)”

같은 서비스 메서드를 사람 REST 경로도 호출한다면, 메서드 내부를 침습하지 않습니다. 대신 호출부(에이전트 도구 핸들러)에서 본 업무를 완료한 뒤, 얇은 후속 태깅 update를 비차단(best-effort)으로 덧붙입니다. 이렇게 하면 사람 경로를 오염시키지 않으면서 AI 경로만 메타데이터를 남깁니다.

”값이 없음”이 곧 “결함”은 아니다 — advisory 판정 절차

섹션 제목: “”값이 없음”이 곧 “결함”은 아니다 — advisory 판정 절차”

순수 advisory형 에이전트는 업무테이블을 직접 쓰지 않습니다. 이걸 결함으로 오판하면 없는 문제를 만듭니다.

  1. 에이전트가 만드는 큐/로그 타입을 찾습니다. 해당 에이전트가 생성하는 작업 큐(HITL task, decision log 등)의 타입 식별자를 확인합니다.
  2. 그 타입을 실제로 “소비/적용”하는 코드가 있는지 전수 grep합니다. grep -rn "TASK_TYPE_NAME" src/로 정의부 외에 참조가 있는지 봅니다.
  3. 소비 코드가 없다면 → advisory 확정. 사람이 그 제안을 참고만 하고, 완전히 별개의 정상 CRUD 화면에서 직접 처리한다는 뜻입니다. 코드 변경이 불필요합니다. 단, 이 결론을 문서로 남깁니다.
  4. 소비 코드가 있는데 미완성(dead-end)이라면 → 컬럼 문제가 아니라 더 심각한 별도 기능갭입니다. (아래 “더 큰 발견” 참조)

라이브 검증 — 유닛테스트로 끝내지 않는다

섹션 제목: “라이브 검증 — 유닛테스트로 끝내지 않는다”

DI가 성립하고 타입 컴파일이 통과해도, 실제 실행 흐름에서 값이 채워지는지는 다른 문제입니다.

상황검증 방법
즉시 실행되는 도구실제 자연어 지시 → 에이전트가 실제 도구를 호출하는지, DB에 반영되는지 쿼리로 직접 확인
승인(HITL) 후 실행되는 도구승인 큐 생성까지 확인 + 승인 후 실행 경로가 실제 디스패처에 매핑되는지 코드로 확인
관찰기간이 필요한 배치합성 레코드를 관찰기간 이전 시점으로 backdate해 만들고, 수동 트리거로 실제 배치 로직을 통과시킨 뒤 결과 확인 → 테스트 데이터 정리
순수 관측/알림 배치동일 집계 SQL을 직접 실행해 코드의 집계 결과와 일치하는지 대조 + 설정 화면에 정상 노출되는지 확인

더 큰 발견 — 얕은 조사였다면 놓쳤을 것

섹션 제목: “더 큰 발견 — 얕은 조사였다면 놓쳤을 것”

“컬럼이 채워지는가”를 확인하려고 코드를 끝까지 추적하다 보면, 더 근본적인 질문에 자연스럽게 도달합니다.

표면 질문 이 컬럼에 값이 채워지는가? 추적 값을 쓰는 실행 코드가 어디 있나? 근본 질문 애초에 이 동작이 실행은 되는가?
컬럼 확인이라는 표면 질문이, 코드를 끝까지 추적하는 과정에서 "이 동작이 실행은 되는가"라는 근본 질문으로 내려갑니다.

HITL(사람 승인 개입) 구조에서 흔히 발생하는 위험 패턴이 있습니다 — AI가 제안하고 승인 큐까지는 정상 작동하는데, 승인 이후 실제로 그 작업을 실행하는 코드가 없는 dead-end입니다. 새 도구를 추가할 때마다 “제안 → 승인 큐”까지만 만들고 실행 디스패처 등록을 빠뜨리기 쉽습니다. 사람이 처리하는 정상 업무 화면이 AI 경로와 무관하게 따로 존재하면, 이 결함은 겉으로 드러나지 않은 채 오래 방치됩니다.

최종 체크리스트 — 신규 에이전트·도구를 추가할 때

섹션 제목: “최종 체크리스트 — 신규 에이전트·도구를 추가할 때”
  1. 이 에이전트가 업무테이블을 직접 쓰는가, 순수 advisory인가를 먼저 결정하고 설계 문서에 명시합니다. 나중에 다른 사람이 추측하지 않도록.
  2. 직접 쓴다면, 그 write 지점에서 최소 aiSuggested + inputActorType을 처음부터 함께 작성합니다. 나중에 “보완”하러 오지 않도록.
  3. 사람 REST와 서비스 메서드를 공유한다면, 메서드 내부를 침습하지 말고 호출부에서 후속 태깅합니다. 메서드가 어디서 호출되는지 모르는 채로 파라미터를 끼워넣으면 인간 경로까지 오염시킬 위험이 있습니다.
  4. HITL이 필요한 도구라면, “승인 후 실제로 무엇을 실행하는가”의 코드가 진짜 존재하는지 처음부터 확인합니다. 새 도구를 추가할 때마다 큐만 만들고 실행 구현을 빠뜨리는 dead-end가 반복되지 않도록.
  5. 배포 후 반드시 라이브로 확인합니다. 가능하면 실제 호출 경로로, 안 되면 합성 데이터로. 컴파일 통과와 실제 실행은 다른 문제입니다.
  6. write할 코드가 없다는 결론(n/a)에 도달했다면, 왜 없는지를 문서로 남깁니다. “없음”과 “확인했는데 없음”은 미래의 재조사 비용에서 천지차이입니다.

이 글은 SL.AIMS를 만들며 겪은 현장 회고 중 하나입니다. 전체 그림은 〈사례연구: SL.AIMS〉에 있습니다.