
하나의 COBOL 파일이 AI에게 중요한 그 사실만 빼고 전부 알려 줬기에, 나는 먼저 지도를 만들었다.
이 모든 일의 시작이 된 COBOL 한 줄은 세 단어였고, 그 세 단어가 전부 나를 속였다.
COMPUTE WS-RESULT = WS-WIRE-AMOUNT - TRN-LIMIT. 나는 중견 은행의 전신 송금 하위 시스템을 위한 데모 에스테이트를 만들고 있었고, 이것은 WIRETXN이라는 프로그램 안의 운명적인 한 줄이었다. 1학년생도 포팅할 수 있을 법한 산술처럼 보인다. 금액에서 한도를 빼고 결과를 쓴다. 그 단일 파일을 어떤 최신 모델에든 건네 Java를 요청하면, 약 4초 만에 깨끗하고 컴파일되며 단위 테스트를 통과하는 Java를 내놓을 것이다. 그리고 TRN-LIMIT을 long으로 타입 지정할 것이다. 그리고 첫 번째 실제 전신 송금에서, 프로덕션 데이터베이스에 손상된 바이트를 써넣을 것이다.
내가 그걸 아는 이유는 TRN-LIMIT이 long이 아니기 때문이다. 그것은 packed-decimal COMP-3 필드이고, 세 파일 떨어진 곳에 정의되어 있으며, 그 살아 있는 해석은 전혀 다른 프로그램에서 설정된 플래그에 의해 선택되고, 새벽 2시에 돌아가는 배치 작업이 그 순서를 잡는다. 그 어느 것도 WIRETXN에서는 보이지 않는다. 위험한 COMPUTE가 들어 있는 파일에는, 그것을 위험하게 만드는 사실들이 하나도 없다.
그 간극이 내가 CodeGraph를 만든 전부 이유이고, 이 에세이는 그곳에 이르는 길에서 내가 무엇을 잘못 잡았는지에 대한 이야기다. 처음에는 문제가 번역 품질이라고 확신했다. 틀렸다. 문제는 모델이 봐야 할 것을 볼 수 없다는 것이고, "더 많은 맥락을 줘라"로는 절대 고쳐지지 않는다는 것을 나는 한동안 스스로에게 증명했다.
안전해 보였지만 그렇지 않았던 COMPUTE
나는 어떤 도구도 신뢰하기 전에 전신 송금 변경을 먼저 손으로 매핑했고, 올바른 마이그레이션이 알아야 할 사실은 정확히 아홉 가지였다.
그중 세 가지는 WIRETXN 안에 있고, 단일 파일 독자에게 정말로 보인다. WIRETXN은 그 TRN-LIMIT을 그 COMPUTE에서 33번 줄에 사용한다. CBACCT이라는 카피북을, 이름만으로, 13번 줄에서 가져온다. UPDATE를 DB2 테이블 ACCOUNTS에 대해 37번 줄에서 실행한다. 텍스트 윈도우 도구는 이 세 가지를 모두 본다. 사실이 그것뿐이었다면, 나이브한 포팅도 문제없었을 것이다.
나머지 여섯이 아픈 것들이다. TRN-LIMIT은 PIC S9(9)V99 COMP-3으로 CBACCT.cpy 11번 줄에 선언되어 있다. 이는 packed decimal을 뜻하고, Java에서는 BigDecimal을 뜻하며 절대 long이 아니다. 바로 그 아래에 TRN-LIMIT-ALPHA REDEFINES TRN-LIMIT가 있어, 같은 여섯 바이트를 원시 텍스트로 그 필드 위에 겹쳐 둔다. 세 번째 필드인 LIMIT-TYPE-FLAG가 런타임에 그 두 해석 중 어느 살아 있는 쪽인지 결정한다. 그 플래그는 LIMITSET이라는 프로그램이 쓰고, 야간 배치 작업 BATCHUPD도 다시 쓴다. 그리고 JCL 작업 NIGHTLY가 02:00에 전신 송금 작업의 선행 작업으로 돌아가는데, 에스테이트 전체에서 "플래그를 설정한다"와 "송금을 실행한다" 사이의 순서가 기록된 곳은 오직 그곳뿐이다.

여섯 가지 사실. 하나하나가 참이고, 하나하나가 결정적이며, 하나하나가 실제로 연산을 수행하는 파일에서는 구조적으로 보이지 않는다. 그것들을 그렇게 늘어놓았을 때 나를 불안하게 한 것은 나이브한 포팅이 틀렸다는 점이 아니었다. 나이브한 포팅은 자신이 틀렸다는 것을 알 방법이 없었다. 주어진 단 하나의 파일을 읽었고, 그 단 하나의 파일은 중요한 여섯 사실에 대해 침묵했다.
위험한 줄이 들어 있는 파일에는, 그것을 위험하게 만드는 사실들이 하나도 없다. 그것은 번역 버그가 아니다. 번역 버그의 옷을 입은 검색 실패다.
컨텍스트 윈도우를 더 키우려는 시도를 멈춘 이유
나의 첫 본능은 지금 모두가 가진 바로 그 본능이었고, 한동안 그걸 쫓았다는 것을 솔직히 말하고 싶다: 그냥 모델에 더 많이 줘라.
그 논리는 빈틈없어 보였다. 실패가 모델이 파일 하나만 봤기 때문이라면, 카피북도 먹여라. 플래그를 건드리는 프로그램도 먹여라. JCL도 먹여라. 컨텍스트 윈도우는 이미 거대하고 분기마다 더 커지니, 인색함을 버리고 코드 이웃 전체를 프롬프트에 쏟아붓는 것이 답일 터였다. 나는 진심으로 이게 통할 줄 알았고, 장난감 예제에서는 어느 정도 통한다. 왜냐하면 붙여넣을 여섯 파일이 무엇인지 이미 알고 있다면, 실제 문제는 손으로 이미 풀어 둔 것이기 때문이다.
그게 균열이었다. 모델에 올바른 맥락을 먹이려면, 먼저 어느 올바른 맥락인지 알아야 했다. 그리고 TRN-LIMIT의 타입이 BATCHUPD에서 쓰인 플래그에 의해 결정되고 02:00 JCL 작업에 의해 순서가 잡힌다는 것은 WIRETXN을 더 열심히 읽어서 뽑아내는 것이 아니다. 이미 의존성 그래프를 추적해 두었을 때만 얻을 수 있는 것이다. 컨텍스트 윈도우는 컨텍스트 윈도우에 무엇을 넣어야 하는지 알려주지 않는다. 나는 그 질문에 답으로 답하려 하고 있었다.
그다음 숫자가 그 점을 영구적으로 못 박았다. 이런 은행들이 실제로 운영하는 에스테이트는 여섯 파일이 아니다. COBOL 100만에서 1,000만 줄, 때로는 그 이상이고, 업계 전체에서 여전히 2,200억 줄이 프로덕션에서 살아 있다(업계 메타 분석, 2025). 실제 전신 송금 변경의 이행적 클로저는 마흔 개 파일이거나 사백 개일 수 있다. 그것은 오늘도, 3년 뒤 출시될 모델 버전에서도 컨텍스트 윈도우에 절대 들어가지 않는다. 에스테이트가 윈도우보다 빠르게 커지고, 애초에 윈도우가 제약이 아니었기 때문이다. 제약은 1,000만 줄 가운데 이 변경이 건드리는 마흔 개가 어느 것인지 아는 것이고, 서른여덟이 아니라 마흔 모두를 찾았음을 증명하는 것이다.
더 큰 컨텍스트 윈도우는 내가 묻기를 그만둔 질문에 대한 더 나은 답이다. 질문은 "모델이 더 많은 코드를 담을 수 있는가"가 아니라 "어느 코드인가, 그리고 그것이 전부임을 어떻게 증명하는가"다.
그 재구성이 CodeGraph가 번역기가 아닌 이유의 전부다. 나는 의도적으로 COBOL을 붙여넣고 Java를 돌려주지 않는다. 지도가 제품이다, 그리고 번역은 지도가 존재한 뒤에야 어떤 도구든 할 수 있는 하류 유스케이스다. 내가 만드는 것은 그 아래의 이해 계층이다. 배포된 픽스처에서 에스테이트는 47개 노드와 70개 엣지의 타입 있는 지식 그래프로 파싱되고, "변경의 영향"은 그래프 순회 — 변경이 건드리는 모든 것의 이행적 클로저 — 이며, 각 엣지는 그것이 나온 file:line을 담는다. 의도적으로 지루한 순수 Python 그래프 작업이고, 핫 패스에 모델은 없다. 내가 필요로 하는 것은 영리함이 아니기 때문이다. 완전하고 재현 가능해야 한다. 같은 픽스처를 넣으면, 매번 같은 클로저가 나온다.
나는 스스로에게 규칙처럼 되뇌인다. 에이전트는 조언하고, 코드가 결정한다. 데모의 선택적 언어 계층 — 클로저에 대해 평범한 영어로 질문에 답하는 부분 — 은 기본적으로 꺼져 있고 키 뒤에 잠겨 있다. 가치는 그것에 의존하지 않는다. 가치는 검색과 증명이고, 둘 다 모델의 능력이 아니다.
나이브한 뷰는 실제로 무엇을 지우는가?
나는 여섯 사실이 사라지는 것을 지켜보기 위해 데모에 토글을 일부러 넣었다. 실패를 눈으로 보기 전까지는 완전히 믿지 못했기 때문이다.
"Naive AI context view"를 켜면 그래프가 단일 소스 파일과 변경 주변의 줄 윈도우로 붕괴하는데, 그것이 바로 텍스트 윈도우 도구가 모델에 먹이는 것이다. 9 of 9를 읽던 패널이 3 of 9로 떨어진다. 파일 안의 세 사실은 계속 켜져 있다. 나머지 여섯은 회색으로 흐려지고 조용해진다: COMP-3 타입, REDEFINES 오버레이, 제어 플래그, 그 두 개의 교차 모듈 작성자, 그리고 02:00 JCL 선행 작업. 빨간 배너가 앱 자신의 말로 결과를 적어 둔다 — 보이는 세 사실만 건네받으면 모델은 long TRN_LIMIT을 내보내고 데이터베이스를 손상시킨다.

여기서는 조심하고 싶다. 창업자가 과대 포장하고 싶어지는 바로 그 지점이기 때문이다. 9 대 3 결과는 배포된 합성 전신 송금 픽스처에서 측정된 것이고, 이 데모를 위해 내가 손으로 지은 에스테이트다. 진짜 의존성 집합을 알고 회수율 숫자가 감이 아니라 라벨이 붙은 실제 측정이 되도록 일부러 그렇게 했다. 당신의 COBOL에 대한 보장이 아니다. 나이브한 뷰는 시뮬레이션이지 라이브 z/OS 파이프라인이 아니다. 그래프는 SQLite를 밑에 둔 인메모리이지 프로덕션 그래프 플랫폼이 아니다. 내가 합성 은행을 만든 이유는 실제 은행을 윤리적으로 보여 줄 수 없었고, "그래프는 아홉을 전부 잡았고 단일 파일은 셋을 잡았다"고 말할 수 있는 유일한 정직한 방법이 알려진 정답이기 때문이다.
하지만 실패의 형태는 합성이 아니고, 그게 중요한 부분이다. 타입이 다른 곳에서 결정되는 COMP-3 필드, 배치 작업이 설정하는 플래그, JCL에만 존재하는 순서 — 이것들은 40년 된 은행 에스테이트의 평범한 결이지, 이국적인 엣지 케이스가 아니다. 메인프레임 현대화 프로젝트의 대략 70~80%가 목표 달성에 실패할 때(업계 메타 분석, 2025), 나는 더 이상 번역 단계가 나빠서라고 생각하지 않는다. 번역 단계는 괜찮다. 가장 중요한 여섯 사실이 잘려 나간 그림을 먹이는 것이다.
증명하지 못하면 세지 않는다
내가 가장 자랑스러운 기능은 자신이 할 수 없는 것을 인정하는 기능이고, 컴플라이언스 대화가 그걸 다시 잡아주기 전까지는 그 가치를 제대로 느끼지 못했다.
엔지니어는 올바른 마이그레이션을 원한다. 규제 당국은 다르고 더 어려운 것을 원한다: 증거. DORA 아래에서 은행은 ICT 자산 목록을 빚진다. SOC-2 아래에서는 변경 통제 영수증을 빚진다. 어느 쪽도 모델이 "날 믿어, 의존성을 찾았어"라고 말하는 것으로는 충족되지 않는다. 그들은 완전성 증명 — 도구가 코드베이스의 얼마나를 실제로 해석할 수 있었는지에 대한 진술과, 더 중요하게는 해석하지 못한 것에 대한 정직한 표시 — 이 필요하다. 그래서 나는 완전성 게이트를 만들었다. 픽스처의 모든 PERFORM, CALL, COPY, 그리고 DB2 참조는 그래프의 실제 노드로 해석되거나 "검토 필요"로 표시되어야 한다. 아무것도 조용히 사라지게 두지 않는다.
픽스처에서 그 게이트는 34개 참조 중 33개를 해석해 97.1퍼센트 커버리지다. 해석하지 못하는 하나는 DISPATCH라는 프로그램인데, 동적 CALL WS-PROGNAME을 한다 — 런타임에 계산되는 대상이라, 프로그램이 실행되기 전까지 목적지가 알려지지 않으므로 어떤 정적 파서도 따라갈 수 없다. 그리고 거기서 올바른 행동은 추측하는 것이 아니다. "사람이 이걸 봐야 한다"는 플래그를 올리고 보고서에 남기는 것이다.

그 플래그된 DISPATCH 호출이 전체 빌드에서 내가 가장 좋아하는 것이고, 진심이다. 97퍼센트를 해석하고 못 한 3퍼센트가 정확히 무엇인지 알려주는 도구가, 100을 주장하고 간극을 숨기는 도구보다 더 가치가 있다 — 숨겨진 간극이야말로 손상된 전신 송금이 사는 곳이기 때문이다. 완전성 게이트는 내보낼 수 있는 "Codebase Topology and Completeness Report"를 만든다 — 노드·엣지 요약, 모듈별 클로저의 file:line 출처, 회수율 결과, 타임스탬프가 찍힌 플래그 항목이 담긴 JSON과 인쇄 가능한 HTML. 그 산출물이 요점이다. 규제 당국에 건네고, 다음 분기에 다시 돌려, 결정론적이기 때문에 동일한 답을 받을 수 있는 것이다.
나는 구멍을 인정하는 숫자를 출시하는 편이, 구멍을 숨기는 더 둥근 숫자보다 낫다. 플래그된 동적 CALL은 데모의 약점이 아니다. 그것이 데모다.
이것 또한 시간이 지나도 낡지 않는 부분이다. Java 한 줄도 환각하지 않는 완벽한 모델조차, 자신이 검색한 의존성을 규제 당국에게 증명할 수 없다. 런타임에 계산되는 CALL을 정적으로 따라갈 수도 없다. 그것은 코드의 속성이지 독자의 속성이 아니기 때문이다. 출처와 완전성은 모델 주변에 구축하는 시스템의 속성이지, 스케일을 키워 잠금 해제하는 능력이 아니다.
무엇을 어떤 순서로 건드릴지
그래프가 마지막으로 준 것은, 내가 애초에 만들려던 것도 아닌 것이었다: 작업을 안전하게 수행할 순서.
전체 의존성 위상을 갖게 되면, 모든 프로그램을 얽힘 정도에 따라 점수 매길 수 있다. 나는 단순한 공식을 쓴다 — 결합도를 COMP-3 함정, JCL 중요도, 미해석 호출에 가중해 — 그리고 픽스처의 열네 개 프로그램을 스트랭글러 피그 추출 순서로 순위를 매긴다. 위험이 가장 낮은 프로그램이 먼저 추출되고, god-program이 마지막이다. 픽스처에서 AUDITLOG가 순위 1, 위험 점수 0으로 나온다. 결합도가 없고 그것이 올바른지에 의존하는 것도 없기 때문이다. 시작하기에 안전한 지점이다. 우리가 걱정해 온 WIRETXN 프로그램은 순위 11에 자리하며, 하나의 COMP-3 함정과 JCL 중요도를 진다. DISPATCH는 미해석 동적 호출과 함께 순위 12에 있다. 그리고 ACCTMGR는 모든 것이 기대는 god-program으로, 순위 14, 위험 점수 15로 맨 마지막에 추출된다.

나는 시퀀싱을 지금만큼 신경 쓰게 될 줄은 몰랐다. 하지만 같은 교훈이 세 번째다. 어디서 안전하게 시작할 수 있는지는 위상에 대한 사실이지, 기획 회의에서 논쟁하는 의견이 아니다. 백만 줄을 바라보는 팀은 사실 문단을 어떻게 번역할지에서 의견이 갈리지 않는다. 끝없이, 비싸게, 어디서 시작할지와 잘못된 것을 먼저 건드리면 무엇이 깨지는지에서 갈린다. 그것은 그래프 질문이고, 그래프는 매 실행마다 같은 방식으로 답한다.
추출 순서, 완전성 게이트, 영향 클로저 — 모두 같은 대상을 세 각도에서 본 것이다. 진짜 조각을 검색하고, 그것이 전체 조각임을 증명하고, 조각을 위험으로 순위 매긴다. 그 셋 중 어느 것도 번역 문제가 아니며, 어느 것도 더 똑똑한 모델로 풀리지 않는다.
내가 자꾸 돌아오는 질문
나는 내 것 포함, 보는 AI 현대화 피치마다 하나의 질문을 던지기 시작했고, 그것은 조용히 내가 신뢰하는 유일한 질문이 되었다.
"좋은 Java를 쓸 수 있는가"가 아니다. 답은 거의 항상 예이고, 거의 절대 중요하지 않기 때문이다. 더 어려운 질문은 TRN-LIMIT 줄이 내게 가르친 것이다: 지금 당장, 검색한 의존성이 무엇인지 증명할 수 있는가, 그리고 그 증명이 실패하길 원하는 규제 당국 앞에서도 살아남는가. 도구가 file:line 출처와 함께 클로저를 보여 주지 못하고, 해석하지 못한 것을 정직하게 말해 주지 못한다면, 출력이 아무리 유창해 보여도 상관없다. 그것은 좋은 문법으로 추측하는 것이고, 나는 바로 그 추측이 packed-decimal 필드 위에 long을 타입 지정하고 데이터베이스로 손을 뻗는 것을 지켜봤다.
업계는 10년 동안 번역 단계를 더 낫게 만드는 데 썼지만, 프로젝트의 70~80%는 계속 목표를 놓쳤다(업계 메타 분석, 2025). 나는 번역 단계가 애초에 위험이 살던 곳이 아니었기 때문이라고 생각한다. 위험은 위상에, 보이지 않는 여섯 사실에, 새벽 2시에 설정된 플래그에 산다. 그래프가 그 여섯 사실을 복구한 뒤, 정직하게 못 하는 하나를 플래그하는 것을 보고 싶다면 데모는 여기다: veriprajna.com/ko/demos/legacy-cobol-modernization.
그리고 내가 설명하는 글을 읽는 것보다 보는 편이 낫다면, 여기서 처음부터 끝까지 돌아가는 전체를 볼 수 있다.
나는 더 이상 다음 모델 릴리스가 이런 마이그레이션을 풀어 준다고 믿지 않는다. 더 큰 윈도우는 더 많은 코드를 담는다. 어느 코드인지는 알지 못하고, 전부를 찾았음을 증명하지도 못한다. 그건 내가 파서의 첫 줄을 칠 때도 참이었고, 이 작업을 만드는 데 쓴 모델이 은퇴한 한참 뒤에도 참일 것이라고 생각한다. 지도가 언제나 어려운 부분이었다. 우리는 채점할 줄 아는 부분인 번역만 계속 바라봤을 뿐이다.


