이 글은 2026-10-01과 10-02 이틀의 기록이다. 첫날은 AI가 낸 변경안이 서버 검증에서 떨어지는 사례를 하나씩 고쳤다. 둘째 날은 전체 정합성 수정 요청 하나가 세 시간을 쓴 일에서 시작해 요청에 상한을 뒀고, 상한 때문에 드러난 문제를 고쳤고, PlantUML 원본 544개를 구조 정의로 옮겼다. API 테스트는 이틀 사이 226건에서 576건이 됐다.
프로젝트 이름이 artifact-transformer에서 BATON으로 바뀌었다. 저장소도 baton이다. 이 일지는 “artifact-transformer 작업 일지” #1~#47에서 이어지고, 이번 편부터 제목을 “BATON 작업 일지”로 쓴다. 번호는 그대로 잇는다.
배경 — AI 변경안은 서버 규칙을 통과해야 적용된다
BATON의 AI 지시는 LLM이 낸 변경안을 그대로 쓰지 않는다. 서버가 세 가지를 강제한다.
- 원본 보호: 다른 문서가 소유한 필드를 AI가 바꾸면 기존 값으로 되돌린다
- 번호 발급: AI는
NEW:프로젝트-유형-분류로 자리만 요청하고, 실제 번호는 서버가 발급한다 - 원본 버전 확인: 변경안을 만든 뒤 원본이 바뀌었으면 적용을 거부한다
이틀 동안 겪은 문제의 대부분은 이 규칙들이 정상적인 수정까지 막은 경우였다.
문제
- 보호 규칙이 올바른 수정을 되돌리거나 엉뚱한 항목을 덮어쓰는 경우를 어떻게 구분하나
- 길게 도는 AI 요청을 언제, 어떤 기준으로 끊나
- 끊긴 요청이 남긴 오류는 왜 다시 요청해도 줄지 않나
- 다이어그램의 원본이 JSON과 PlantUML 둘인 구조를 어떻게 하나로 줄이나
- 변경안을 만든 뒤 원본이 바뀌면 AI를 다시 불러야 하나
결정 1 — 보호 규칙은 위치가 아니라 식별자로 항목을 찾는다
유스케이스 하나를 지우라는 AI 지시가 계속 실패했다. 지워지지 않거나, 지워진 뒤 다시 나타났다.
원인은 둘이었다.
- 삭제로 목록 순서가 바뀌면, 보호 규칙이 뒤 항목에 앞 항목의 이름과 번호를 덮어썼다. 보호 대상을 목록의 위치로 찾고 있었기 때문이다.
- 다이어그램 원본에는 항목이 남아 있어서, 다음 동기화 때 정의가 다시 생겼다.
보호 규칙이 항목을 식별자로 찾도록 고쳤다. 삭제 변경안에는 정의, 다이어그램 노드, 그 노드에 딸린 관계와 주석, 그리고 이 유스케이스를 참조하던 시퀀스 4개의 연결 해제를 함께 담는다. 시퀀스 내용은 건드리지 않는다. SURAS 자료로 저장 없이 돌려 보니 유스케이스가 28개에서 27개가 됐고, 나머지 항목의 이름과 번호는 그대로였다.
다이어그램 쪽 정리는 한 번에 끝나지 않았다. 여러 줄 주석과 색상·방향이 지정된 화살표가 차례로 걸렸다. 가장 까다로운 것은 INFO-->…처럼 공백 없이 붙여 쓴 연결이었다. 식별자의 마지막 글자를 화살표의 일부로 읽었다. 알려진 식별자를 먼저 매칭하도록 고쳤고, 삭제 결과 다이어그램 23개를 실제로 렌더해 확인했다.
그다음 실패는 방금 넣은 수정이 만들었다. 중복 식별자 검사를 삭제 전 목록 전체에 걸어 둔 탓에, “같은 식별 번호를 가진 항목이 여러 개라 변경 대상을 구분할 수 없습니다”라는 검사가 삭제하려는 중복 자체를 막았다. 삭제 요청은 이 검사보다 먼저 현재 편집본에서 대상을 직접 찾게 했다. AI가 새로 만든 중복은 바로 실패시키지 않고 자동 보완 단계로 넘긴다.
결정 2 — 제한 시간을 둘로 나누고, 진행을 보여 준다
AI 호출이 600초에서 끊겼다.
Codex CLI 실행 실패: Command '['cmd', '/c', '...codex.CMD', 'exec', ...]' timed out after 600 seconds
제한 시간이 하나뿐이라, 정상적으로 응답을 쓰고 있는 호출도 10분에 잘렸다. 중단 전에 받은 응답과 오류 정보도 함께 버리고 있었다. 한도를 둘로 나눴다.
| 설정 | 기본값 | 의미 |
|---|---|---|
BATON_CODEX_TIMEOUT | 1800초 | 진행 중이어도 이 시간을 넘으면 중단 |
BATON_CODEX_IDLE_TIMEOUT | 900초 | 응답이 없는 상태로 기다리는 시간. 진행 이벤트가 오면 다시 센다 |
같이 고친 것이 세 가지 있다.
- 재시도 알림은 진행 이벤트로 치지 않는다. 실패를 반복하는 호출이 무응답 시계를 계속 되돌리면 한도가 의미를 잃는다.
- Windows에서는 래퍼(
cmd /c)만 죽이면 하위 프로세스가 남는다. 중단 시 하위 프로세스까지 정리한다. - 끝까지 오지 않은 응답은 변경안이 될 수 없다. 완료 이벤트를 받은 응답만 쓴다.
한도를 늘리자 다른 문제가 보였다. 전체 정합성 수정 요청이 한 시간째 돌고 있는데, 도는지 멈췄는지 알 방법이 없었다. 프로세스는 살아 있었지만 마지막 호출은 응답 시작 신호만 남기고 그 뒤의 진행 기록이 없었다. 이것만으로는 정상 처리 중인지 판단할 수 없었다. 살아 있다는 것과 일하고 있다는 것은 다른 정보다.
진행 상황을 스트림으로 내보냈다. 현재 단계와 대상 문서, 경과 시간, AI 호출 횟수, 해결된 오류와 남은 오류 수를 보여 주고 중단 버튼을 둔다. 서버 연결 상태와 AI의 마지막 응답 시각은 따로 표시한다. 3분 넘게 AI의 응답이 없으면 지연 안내가 뜬다. 프롬프트와 추론 내용은 내보내지 않는다.
결정 3 — 요청 전체에 시간과 호출 횟수의 상한을 둔다
다음 날, 같은 종류의 요청이 세 시간 걸렸다. 호출 기록을 보니 문서 편집 호출 하나의 입력이 평균 약 30만 토큰이었고, 가장 긴 호출은 10분쯤 걸렸다. 구조적인 원인은 셋이었다.
- 파일 하나를 고치는 동안, 아직 다른 파일에서 해결하지 않은 연결 오류까지 반복해서 AI에 보냈다
- 마지막 정합성 보완 단계가 전체 계획을 처음부터 다시 세웠다
- 호출마다 기존 변경안의 원본과 편집본이 중복으로 실려 갔다
참고 문서는 식별자와 연결만 남기고 긴 설명은 100자에서 자른다. 편집 대상 문서는 전체를 그대로 보낸다. SURAS의 문서 16개로 재 보니, 운영 시나리오 하나를 고칠 때 넘기는 참고 문맥이 약 77만 자에서 27만 자로 줄었다.
수정 대상은 AI에게 묻지 않고 실제 정합성 검사 결과에서 뽑는다. 연관 문서는 한 호출로 묶는다.
- 문서 최대 3개, 본문 합계 최대 90,000자
- 원천 문서가 먼저 온다. 기능은 요구사항 뒤에, 물리 데이터는 논리 데이터 뒤에 온다
- 묶음 응답이 형식에 맞지 않으면 묶음을 다시 시도하지 않고 해당 문서만 개별 처리한다
그리고 요청 전체를 감싸는 한도를 뒀다.
| 설정 | 기본값 | 허용 범위 |
|---|---|---|
BATON_ASSIST_REQUEST_TIMEOUT | 600초 | 60~1800 |
BATON_ASSIST_REQUEST_MAX_CALLS | 24회 | 1~80 |
이 한도는 요청 단위다. 계획, 생성, 자동 보완이 안쪽에서 몇 겹으로 돌든 같은 예산을 나눠 쓴다. 재시도로 초기화되지 않는다. 호출 하나의 제한 시간도 남은 예산을 넘지 못한다.
def timeout(default):
"""A single provider call must fit within the remaining request time."""
execution = _current.get()
return max(0.01, min(default, execution.remaining())) if execution else default
한도가 걸린 요청 안에서는 SDK의 내부 재시도도 끈다. 한도에 닿으면 실패로 끝내지 않고, 완료된 변경안과 남은 오류를 함께 돌려준다. 이것은 완료 시간을 보장하는 장치가 아니라 쓰는 시간을 가두는 장치다.
결정 4 — 보호 규칙이 막는 수정은 서버가 직접 한다
한도를 걸고 다시 돌린 결과는 이랬다.
| 건수 | |
|---|---|
| 시작 시 오류 | 153 |
| 해결 | 58 |
| 남음 | 95 |
남은 95건을 뜯어 보니 원인이 둘이었다.
첫째는 한도 자체다. 파일 5개의 변경안은 만들었지만 나머지 5개는 시작도 못 했다. 물리 데이터 문서가 빠지면서 그 문서의 오류 70건이 그대로 남았다.
둘째가 더 중요했다. AI는 잘못된 시나리오 번호와 단계 번호를 고치려 했는데, 원본 보호 규칙이 35개 필드의 변경을 되돌렸다. 식별자는 보호 대상이기 때문이다. 틀린 번호를 고치라는 요청이, 번호를 지키는 규칙에 막힌 것이다.
이 상태에서는 몇 번을 다시 요청해도 같은 자리에서 멈춘다. 게다가 AI의 설명문은 되돌려진 변경을 “수정했다”고 적고 있었다.
번호 수정을 AI에게서 떼어 냈다. 서버가 먼저 규칙으로 고친다.
"""Deterministic repairs of existing scenario/test numbers, with their references.
No business classification is inferred. Only registered selections, unissued NEW
prefixes and a step's actual parent are used. Ordinary AI ownership stays locked.
"""
업무 분류는 추측하지 않는다. 등록된 분류와 단계의 실제 부모만 써서 번호를 다시 매기고, 그 번호를 쓰는 참조를 함께 고친다. 일반 AI 편집의 보호 규칙은 그대로 둔다.
SURAS 편집본의 복사본에서 LLM 호출 없이 오류가 17건에서 10건으로 줄었고, 번호 오류는 모두 없어졌다. 남은 10건은 업무 의미를 확인해야 연결할 수 있는 것들이다. 같은 번호를 여러 항목이 공유해서 연결 대상을 가릴 수 없는 경우도 추측하지 않고 확인 사항으로 남긴다. 보호 규칙이 되돌린 변경을 완료로 설명하지 않도록 결과 문구도 고쳤다.
보호 규칙 자체에도 어긋난 곳이 있었다. 기능의 “관련 요구사항” 필드는 도출기가 기존 편집 값을 보존하는 필드인데, AI 편집에서는 원본 소유 필드로 잠겨 있었다. 두 곳의 규칙이 달라서 올바른 연결 수정이 되돌려졌다. 잠금 목록을 도출기의 실제 동작에 맞췄다.
남은 오류가 많은 문서부터 처리하도록 순서도 바꿨다. 한도에 닿더라도 가장 큰 덩어리는 먼저 끝난다.
결정 5 — 전체 수정 여부는 LLM이 아니라 서버가 판정한다
“정합성 오류 모두 해결해줘”는 LLM에 그대로 전달되지 않는다. 서버가 먼저 이 지시가 전체 수정 요청인지 판정한다.
- 전체 수정 요청이면 검사기가 현재 편집본을 먼저 검사한다. 오류가 0건이면 LLM을 부르지 않는다
- 실제 오류 목록과 함께 변경 계획을 요청한다
- 계획에 따라 문서별 변경안을 요청한다
- 보호 규칙과 번호 발급을 적용하고 다시 검사한다. 남은 오류는 최대 2회 보완하고, 오류가 줄지 않거나 새로 생기면 직전 변경안을 유지한다
일반 편집 지시는 그 변경이 건드린 부분의 정합성만 보완한다. 일반 편집이 오래된 오류까지 손대면 변경안이 요청과 무관하게 커지기 때문이다.
판정은 정규식 전체 일치로 한다. 그래서 한계가 분명하다.
| 입력 | 전체 수정으로 인식 |
|---|---|
| 정합성 오류 모두 해결해줘 | 인식 |
| 남은 정합성 오류 해결해줘 | 인식 |
| 전체 정합성을 점검하고 발견되는 문제를 전부 고쳐줘 | 미인식 |
| 정합성 오류 모두 해결해줘. 기존 업무 내용은 유지해줘. | 미인식 |
같은 뜻이라도 표현에 따라 처리 경로가 달라진다. 미인식 문장도 일반 지시로 전달되므로 LLM이 의도는 이해하지만, 사전 전체 검사와 전체 보완 경로는 켜지지 않는다. 범위를 좁게 잡으려고 고른 방식이고, 그 대가로 남은 약점이다.
이 약점은 이틀 동안 세 번 드러났다.
- 정합성 화면의 버튼이 만든 지시에만 전용 처리가 붙어 있어서, 같은 내용을 직접 입력하면 일반 편집으로 넘어갔다
- “아직 145건의 오류가 있어. 해결해줘”가 전체 수정으로 인식되지 않아 대부분의 오류가 검토 대상에서 빠졌다
- “항목 간 연결 오류 모두 해결해줘”도 빠져 있었다. 화면이 쓰는 용어인데 판정식에는 없었다
세 번 모두 패턴을 하나씩 더하는 것으로 막았다. 근본 해결은 아니다.
반대 방향의 실수도 있었다. AI 대화의 기본 범위를 프로젝트 전체로 넓히자, 일반 편집 요청이 무관한 기존 오류까지 고치려 들었다. 참고하는 범위와 고치는 범위는 따로 정해야 한다. 프로젝트 전체를 참고하되, 고치는 것은 이번 변경과 관련된 정합성으로 제한했다.
전체 수정이 항목을 지워서 오류 수를 줄이는 길은 막았다.
test_bulk_request_cannot_hide_errors_by_removing_items
결정 6 — PlantUML 544개를 구조 정의로 옮긴다
유스케이스·시퀀스·ERD·흐름도는 JSON 정의와 PlantUML 원본을 따로 갖고 있었다. 결정 1의 “지운 항목이 다시 생기는” 문제도 원본이 둘이라 생긴 일이다.
다이어그램 종류마다 원본이 될 정의 문서를 정하고, 11개 프로젝트의 PlantUML 544개를 구조 데이터로 옮긴 뒤 삭제했다. 원본은 백업했다.
| 다이어그램 | 원본 정의 |
|---|---|
| 유스케이스 | usecase.json |
| 시퀀스 | sequence.json |
| ERD | physical-data.json |
| 데이터·상태·화면 흐름 | flow-diagram.json |
기존 PlantUML은 한 방향으로만 읽어 들인다.
"""Native definitions and one-way import of legacy diagrams. Never executes PUML."""
어려운 쪽은 이미 열려 있던 개인 편집본과 대기 중인 AI 변경안이었다. 편집본마다 3-way 병합을 돌렸다. 충돌이 나면 현재 편집 값을 남기고, 가져온 값은 문서 안의 migrationReview에 기록한다. SURAS에서는 47건이 이렇게 남았고, 화면의 “자료 전환 후 확인할 내용”에서 두 값을 비교할 수 있다.
전환은 다시 실행해도 추가 변경이 생기지 않게 만들었고, 변환 결과를 검증하고 백업을 확인한 뒤에만 기존 파일을 지웠다. 이전 화면에서 PlantUML 편집본을 다시 저장하는 경로도 막았다. 숨김 배치선과 화면 전이 표기는 변환 규칙을 따로 보완해야 했다.
전환 직후 세 가지가 따라 나왔다.
-
편집본에서 빠진 정의를 AI가 “이전 자료에만 있다”고 판단했다. 군집 평가 시퀀스 3개가 저장본에는 있고 개인 편집본에는 없었는데, AI에는 다른 문서의 오류가 건수로만 전달되고 저장본과 편집본의 차이는 전달되지 않았다. 시나리오가 참조하는 번호를 기준으로 저장본에서 정의를 가져오는 복원을 서버 쪽에 넣었다. LLM 호출 없이 약 1.4초 만에 오류 6건이 0건이 됐다.
-
삭제된 PlantUML 경로가 AI 작업 대상으로 남아 있었다. 이전 대화가 개별 흐름도 파일 경로를 기억하고 있어서, AI가 “선택된 경로가 다이어그램 목록에 없다”며 변경안을 만들지 못했다. 이전 대화의 경로를 현재 정의의 해당 항목으로 풀어 주는 변환을 넣었다.
-
내부 프롬프트에 옛 규칙이 남아 있었다. PlantUML을 원본으로 다루라는 지시 뒤에 구조 데이터 규칙을 덧붙여 우선 적용하는 형태였다. 옛 규칙을 지웠다.
원본이 하나가 되면서 AI 지시의 대상 구분도 바꿨다. “데이터 / 다이어그램” 선택을 없애고 정의 대상 + 작업 범위(선택 항목 / 정의 전체 / 프로젝트 전체)로 합쳤다. 이전에는 같은 카드를 골라도 “데이터”를 선택하면 선택 정보가 빠진 채 전달됐다. 배치·필터·접힘은 화면 표현 정보로 따로 두고, 이름·속성·관계는 하나의 원본만 고친다.
결정 7 — 낡은 변경안은 AI를 다시 부르지 않고 갱신한다
변경안을 만든 뒤 원본이 바뀌면 적용이 거부된다.
sequence.json: 변경안을 만든 뒤 원본 버전이 달라졌습니다. 아무 파일도 적용하지 않았습니다.
거부 자체는 맞는 동작이다. 문제는 그다음에 할 수 있는 일이 “다시 지시”뿐이었다는 점이다. AI 호출 한 번이 몇 분씩 걸리는 상황에서 이건 비싸다.
최신 내용과 비교로 기존 변경안을 갱신하게 했다. 생성 당시의 내용을 기준으로 AI 변경과 최신 내용을 3-way 병합한다. 모델 호출도, 문서 쓰기도 없다.
- 서로 다른 항목의 변경은 양쪽 모두 보존한다
- 같은 항목이 충돌하면 묶음 전체를 거부하고 사용자에게 선택을 받는다
- 사용자의 선택은 비교한 그 버전에만 유효하다
3번은 토큰으로 구현했다. 원본 버전, 저장본, 편집본, 변경안을 모두 넣어 해시를 만들고 선택에 묶는다. 어느 한쪽이라도 바뀌면 이전 선택은 만료된다.
token = _token([name, proposal["etag"], etag, base, current, effective, candidate])
실제 SURAS 변경안으로 비교해 보니 업무 데이터 변경은 전부 합쳐졌고, 충돌은 결정 6에서 만든 전환 확인 기록(migrationReview)에서만 났다. 이 기록은 순서 있는 업무 단계가 아니라 서로 독립된 진단 기록이다. 그래서 목록을 통째로 비교하지 않고 기록 단위로 합친다. 같은 규칙을 시퀀스 메시지처럼 순서가 의미인 목록에는 쓰지 않는다.
비교만으로는 편집본도 저장본도 바뀌지 않는다. 적용할 때 모든 파일의 최신 버전을 다시 확인한다.
의도치 않은 함정
바뀌지 않은 원본을 바뀌었다고 판정했다. 파일 3개를 적용하려는데 “requirement.json: 원본이 변경되었습니다”로 거부됐다. 실제로는 바뀐 것이 없었다. 요구사항 조회 API는 제거된 Jira 연결 정보를 숨기는데, AI 변경안의 비교 기준에는 그 정보가 남아 있었다. 같은 원본을 두 가지 모양으로 들고 비교한 것이다. 비교하기 전에 양쪽에서 같은 필드를 걷어 내도록 했다. 원본 버전 확인은 비교 대상을 정규화한 뒤에 해야 한다.
형식 보완 단계의 안내가 서로 달랐다. AI가 낸 파일 변경 계획이 형식에 맞지 않으면 다시 쓰게 하는데, 그 재요청이 계획이 아니라 문서 내용 형식으로 답하라고 안내하고 있었다. 고치라는 요청이 틀린 형식을 요구한 셈이다. 빈 계획도 같은 문제였다. 고칠 것이 없어서 빈 계획과 형식이 틀린 계획을 구분하지 않고 둘 다 형식 오류로 재시도했다.
단계 번호를 하위 분류로 검사했다. LORAS-OSC-DAT-S1의 S1이 “등록된 하위 분류가 아니다”라며 거부됐다. S1은 분류가 아니라 시나리오의 단계 순번이다. 부모 시나리오의 번호를 먼저 발급하고, 그 뒤에 LORAS-OSC-DAT-001-S1 형태로 단계 번호와 참조를 함께 확정하게 했다. 반대로 AI가 실제로 미등록 분류(SRM)를 번호에 붙인 경우는 발급 전에 걸러 다시 쓰게 한다.
오류 건수가 화면마다 달랐다. 상태 표시줄은 저장된 원본의 오류를, 하단 패널은 편집 중인 내용의 오류를 세고 있었다. 검사 결과를 한 곳에서 공급하고, 같은 위치의 같은 오류는 한 번만 센다. 재검사 중에는 이전 건수를 최신 결과처럼 보여 주지 않는다. AI 변경안을 적용했을 때의 예상 건수와 현재 편집본의 건수도 구분해 보여 준다.
적용한 변경안이 계속 미적용으로 떴다. 파일은 이미 반영됐는데 대화 쪽 상태가 갱신되지 않았다. 다시 누르면 “편집본이 변경되었습니다”로 거부됐다. 편집본을 바꾼 것이 바로 그 변경안이었는데도 그랬다. 변경안 내용과 현재 편집본이 일치하면 적용 완료로 정리하고, 일부만 적용된 경우에는 나머지만 처리한다.
쓰이지 않는 번호 예약이 남았다. 변경을 모두 반영한 뒤에도 예약 11건이 남아 있었다. 반영 실패가 아니라, 발급만 받고 저장본·열린 리뷰·편집본·AI 변경안 어디에서도 쓰이지 않는 번호였다. 예약을 해제하는 기능을 넣되, 정의뿐 아니라 참조도 사용으로 보고, 읽을 수 없는 데이터가 하나라도 있으면 해제하지 않는다.
마무리
이틀 동안 확인한 것은 셋이다.
- 끝을 정해야 남는 것이 보인다. 세 시간 걸리던 요청을 10분에 끊자 95건이 남았고, 그제야 35개 필드의 수정이 보호 규칙에 되돌려지고 있다는 것이 드러났다. 한도가 없을 때는 같은 자리를 돌고 있어도 “아직 처리 중”으로만 보였다.
- 보호 규칙은 막는 것과 고치는 것을 같이 설계해야 한다. 식별자를 보호하는 규칙은 옳았지만, 틀린 식별자를 고칠 경로가 없었다. AI에게 예외를 열어 주는 대신 서버가 규칙으로 고치는 좁은 경로를 따로 냈다. 그 경로들은 LLM 없이 끝났다. 번호 정정은 17건을 10건으로, 시퀀스 복원은 6건을 0건으로 줄였다.
- 수정이 다음 실패를 만든다. 중복 검사를 넣자 중복 삭제가 막혔고, 버튼이 만든 지시만 고치자 직접 입력한 지시가 빠졌고, 대화 범위를 넓히자 무관한 오류까지 고치려 들었다. 검사를 하나 더하는 수정은 그 검사가 막으면 안 되는 경우를 같이 적어야 한다.
AI가 틀린 것을 고치려 할 때 그걸 되돌리는 규칙이 있다면, 그 수정은 AI의 일이 아니다.
다음 할 일:
- 전체 수정 판정을 정규식 일치에서 넓힐지, 좁은 채로 두고 화면에서 명시적으로 고르게 할지 결정
- 남은 오류를 이어서 처리하는 요청이 몇 번 만에 0건에 닿는지 실제 공급자 호출로 확인
- SURAS의 전환 확인 47건 검토