
이것저것 시도해도 안 되더니, 결국 문제는 '읽히는 형식'이었다――CSV를 YAML로 바꿨더니 처리 시간 60%·사용량 70% 절감
요약
LLM을 활용한 시스템 연계 정의서 자동 생성 과정에서 데이터 형식을 CSV에서 YAML로 변경하여 비용과 시간을 획기적으로 절감한 사례를 다룹니다. 무조건적인 컨텍스트 축소 시도가 오히려 도구 호출 횟수를 늘려 역효과를 낼 수 있음을 경고합니다.
핵심 포인트
- 데이터 형식을 YAML로 변경하여 계층 구조 명시 및 처리 효율 극대화
- 복잡한 상품 처리 시 시간 60% 및 사용량 70% 절감 달성
- 무분별한 컨텍스트 축소(Grep 활용)는 도구 호출 폭증으로 역효과 유발 가능
- 코드베이스 규모에 맞는 적절한 데이터 읽기 전략 수립의 중요성
최근 시스템 연계의 IF 정의서(Excel)를 LLM으로 자동 생성하는 메커니즘을 만들어 사용하고 있습니다.
(TIPS: AI를 검색 도구로 끝내지 마라: Java 코드 해석 슬래시 명령어로 만든 5가지 TIPS)
꽤 편리하지만, 1회 실행 시 5시간 분량의 사용량을 20~35%나 소비하고 있어서 "이건 분명 어딘가에 낭비가 있다😇"라고 계속 신경 쓰였습니다.
인터넷을 찾아보니 "범위를 좁혀서 읽어라", "서브 에이전트(Sub-agent)로 분할해라"라는 내용이 나와서 시도해 보았습니다. 전부 성공하지는 못했습니다.
최종적으로 효과가 있었던 것은 LLM에게 읽히는 정의 파일의 형식을 바꾸는 것이었습니다. 초보자가 일반론을 시도하다가 모조리 실패하고, 본질적인 문제에 도달한 기록입니다.
최종적인 변경 내용과 결과는 다음과 같습니다.
수행한 작업
- 정의 파일(CSV) 읽기 → 해석 결과 출력용 Python 스크립트 생성 → IF 정의서(Excel)로 이어지는 흐름을 폐지
- 정의 파일을 YAML로 변경 (계층 구조를 명시)
- 해석 결과를 JSON으로 출력
- 해석 결과를 IF 정의서 형식으로 변환하는 고정 제너레이터(Generator)를 준비 (LLM이 매번 생성하던 Python 스크립트를 폐지)
결과
| 단순한 상품 | 복잡한 상품 (상품 정보가 복수) |
|---|---|
| 변경 전 | 처리 시간 10분 / 사용량 10~15% |
| 변경 후 | 처리 시간 7분 / 사용량 5% |
복잡한 상품의 경우 처리 시간은 절반 이하로, 사용량은 70% 이상 절감할 수 있었습니다.
서비스 신청 처리 코드를 읽고, 외부 시스템으로의 요청 정의서(IF 정의)를 Excel로 출력하는 작업입니다. 처리 흐름은 3단계입니다.
STEP 1: Java 소스 코드를 따라 호출 연쇄를 파악한다
STEP 2: 정의 파일(범용 IF 항목 목록)을 참조하면서,
코드에서 설정값을 읽어온다
...
STEP 1에서는 5~9개의 Java 클래스를 연쇄적으로 읽고, STEP 2에서는 350행이 넘는 정의 파일을 LLM에게 읽히고 있었습니다. 처리가 무거운 것이 당연하다면 당연하지만, 어떻게 가볍게 만들지가 문제였습니다.
파일을 전체 행으로 읽고 있는 것이 문제라고 생각하여, "Grep으로 대상 메서드의 행 번호를 특정한 뒤, 그 주변부만 읽는다"라는 개선 사항을 프롬프트(Prompt)에 추가했습니다. 이는 LLM의 비용 최적화 맥락에서 자주 보이는 수법입니다.
【변경 전의 동작】
Read(Resource.java) ← 파일 전체 (수백 행)
→ 해당 메서드를 찾는다
...
의도한 대로 작동한다면 파일당 읽기량이 줄어들 것이었습니다.
변경 전: 처리 시간 10분 / 사용량 11%
변경 후: 처리 시간 30분 / 사용량 35%
역효과였습니다.
읽기량을 줄이려던 의도와 달리, 도구 호출(Tool call) 횟수가 폭증했습니다.
이번 Java 소스는 파일당 300행 전후라는 "LLM이 한 번에 읽기에 딱 적당한 사이즈"였습니다. 전부 읽어버리는 편이 도구 호출 1회로 끝납니다. 그런데 "범위를 좁혀서 읽는다"라고 판단을 거칠 때마다 호출이 늘어났고, 그 출력이 컨텍스트(Context)에 쌓이면서 오히려 더 무거워졌습니다.
해당 메서드의 읽기부터 시작하여, 호출 메서드, 참조 상수의 정의 위치 등 동일 파일 내의 여러 번 읽기가 빈번하게 발생하는 이미지입니다.
전체를 한 번에 읽고 있었다면 수중에 있었을 정보를, 범위를 좁힘으로써 "나중에 다시 가져와야 하는 정보"로 바꿔버린 셈입니다.
"Grep → 범위 좁히기 Read"는 대규모 코드베이스를 위한 수법이었으며, 이번 규모에는 맞지 않았습니다. 일반론으로서 올바르더라도, 코드베이스의 크기나 처리의 성질에 따라서는 오히려 역효과를 냅니다.
"STEP마다 독립된 서브 에이전트(Sub-agent)로 나누고, 인계 정보만 부모에게 전달하면, 단계를 넘나드는 컨텍스트의 축적을 줄일 수 있다"라는 생각으로 프롬프트를 다시 작성했습니다.
【변경 전】
1개의 대화에서 STEP 1 → STEP 2 → STEP 3를 실행
→ 각 STEP의 읽기 결과가 전부 축적됨
...
컨텍스트 분리는 LLM의 긴 처리에서는 유효하다고 알려져 있습니다.
서브 에이전트로의 분할을 프롬프트에 명시적으로 작성해도, LLM이 "이 처리는 서브 에이전트로 나누지 않는 편이 효율적이다"라고 판단하여 취소하는 상황이 있었습니다. 처리의 전후로 정보를 주고받아야 하는 구성이기 때문에, 분할하는 이점보다 오버헤드(Overhead)가 더 크다고 판단된 듯합니다.
이것 또한 「서브 에이전트 분할 (Sub-agent splitting)」이라는 수법 자체는 옳습니다. 다만 이번 처리 구성이 그 방식에 적합하지 않았을 뿐입니다.
두 가지 시책이 헛수고로 돌아간 후, 「읽는 법」이 아니라 「읽히는 것」을 재검토하기로 했습니다.
문제의 STEP 2에서는 LLM이 350행이 넘는 CSV 파일을 읽어 코드의 필드와 대조하고 있었습니다. 이 CSV의 구조가 어떻게 되어 있었냐면, 다음과 같습니다.
데이터 항목명_계층1,데이터 항목명_계층2,데이터 항목명_계층3,데이터 항목명_계층4,데이터 항목명_계층5,데이터 항목명_계층6,레벨,필드명,설정값
(정보역)신청정보,,,,,,01,orderInfo,
,(정보역)신청자정보,,,,,02,customerInfo,
...
「customerName이라는 필드는 신청정보 > 신청자정보의 하위에 있다」라는 정보를 6개의 빈 컬럼으로 표현하고 있습니다.
사람이 계층을 직관적으로 표현한 Excel을 CSV로 변환한 것입니다.
하지만 이를 LLM이 읽으면, 구조를 파악하는 것만으로도 불필요한 토큰을 사용하게 됩니다.
CSV를 YAML로 다시 작성했습니다. 동일한 내용을 YAML로 쓰면 다음과 같습니다.
orderInfo:
_label: (정보역)신청정보
customerInfo:
...
계층 구조가 중첩 (Nest) 구조로 명시되어 있습니다. LLM에게 일어난 변화는 두 가지입니다.
1. 필드의 「위치」를 한눈에 알 수 있음
CSV에서는 「어느 열이 비어 있는가」를 통해 계층을 읽어낼 필요가 있었습니다. YAML은 인덴트 (Indent)가 곧 계층이므로, customerName이 orderInfo.customerInfo 하위에 있다는 것을 즉시 알 수 있습니다.
2. 필드 특정에 키 경로 (Key Path)를 사용할 수 있음
이 YAML의 키 경로 (orderInfo.customerInfo.customerName)가 다음에 설명할 JSON의 키와 그대로 일치합니다. LLM이 「이 필드를 어떤 키로 작성해야 하는가」를 고민할 필요가 없어집니다.
변경 전의 STEP 3에서는 LLM이 Excel 생성 스크립트 (Python)를 매번 통째로 작성하고 있었습니다.
# LLM이 매번 생성하던 코드 (발췌)
import csv, copy, os
def is_container(row):
...
스크립트 하나가 8,000~19,000자입니다. LLM의 출력 토큰은 입력보다 비용이 높기 때문에, 매번 다시 쓰게 하는 것은 효율이 떨어지는 시책이었습니다.
변경 후에는, LLM이 출력하는 것은 설정값인 JSON만으로 제한하고, 스크립트 본체는 고정 파일로 분리했습니다.
{
"_meta": {
"product": "기능명",
...
고정된 generate_from_values.py가 이 JSON을 읽어 CSV → Excel로 변환합니다. LLM은 해석 결과를 구조화하여 전달하기만 하면 됩니다. 스크립트의 로직은 몇 번을 실행해도 동일한 결과를 반환하므로, 재현성도 100% 보장됩니다.
YAML로의 변경과 JSON 출력으로의 변경은 서로 딱 맞아떨어집니다.
YAML의 키 경로
orderInfo.customerInfo.customerName
↕ 그대로 일치
...
LLM이 코드에서 설정값을 읽었을 때, 「이 필드를 JSON의 어떤 키로 작성해야 하는가」를 생각할 필요가 없습니다. YAML의 키 경로를 그대로 사용하면 됩니다. 대조 비용이 제로가 되었습니다.
시도했던 세 가지 접근 방식을 되돌아보면 다음과 같습니다.
| 접근 방식 | 결과 | 이유 |
|---|---|---|
| Grep → 좁히기 Read | 악화 (시간 3배, 사용량 3배) | 파일 크기가 작고, 도구 호출이 연쇄적으로 일어나 역효과 발생 |
| ... |
실패한 두 가지 모두 수법 자체는 틀리지 않았습니다. 이번 처리 구성에 맞지 않았을 뿐입니다.
돌이켜보면 「LLM이 어떻게 움직이는가」의 최적화만을 생각하고 있었습니다. 재검토했어야 할 것은 「LLM에게 무엇을 읽히는가」의 설계였습니다.
LLM에게 정의 파일이나 스키마 (Schema)를 읽히는 상황이 있다면, 그 구조가 「오독하기 어렵게 되어 있는지」, 「불필요한 추론을 하지 않아도 되는지」를 먼저 확인해 보시기 바랍니다. 프롬프트 (Prompt)보다 먼저 그 부분을 의심해 볼 가치가 있습니다.
마지막으로, GMO Connect에서는 서비스 개발 지원 및 기술 지원을 비롯하여
폭넓은 지원을 수행하고 있으므로, 문의 사항이 있으시면 언제든 편하게 연락해 주시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기