View

728x90
반응형

결론부터 쎄게 박고 시작하자

LLM이 다른 언어로 컨버팅 한번에 "딸깍" 안된다. 

 

 

이 글은 실험 후기가 아니라 학계 통계 정리글이다. 누가 LLM에게 "이 Python 코드 Go로 바꿔줘" 한 줄을 던져 깨진 적이 있다면 — 그것은 본인의 프롬프트 문제가 아니라 모델이 본질적으로 이 작업을 잘 하지 못한다는 사실이며, ICSE 2024 학회 논문에서 1700 샘플로 측정된 결과이다. 한 번에 끝내는 번역(one-shot translation)은 평균 정확도가 절반에도 미치지 못한다. 이를 모른 채 계속 시키기 때문에 매번 깨지는 것이다. 비유하자면, 외국어 통역사에게 사전 없이 즉석으로 의학 논문을 통역시키는 것과 비슷하다. 단어는 옮겨지지만 문맥이 박살난다.

 

이 글에서 풀어낼 4가지 질문이다. (1) 왜 본질적으로 어려운가, (2) 어떤 종류로 깨지는가, (3) 어떻게 막을 수 있는가, (4) 자동화는 어디까지 가능한가. 마지막에 Python agent 파이프라인을 Go로 옮기는 절차서를 적용 사례로 붙인다.

 

한 번에 시킨 번역 정확도 47.3% — ICSE 2024가 측정한 LLM 코드 번역의 한계

 

ICSE 2024(소프트웨어공학 분야 최상위 학회)에 발표된 Pan et al. "Lost in Translation" 논문이 이 분야의 가장 단단한 실측 자료다. 7개 LLM × 5개 언어(C·C++·Go·Java·Python, 총 31개 번역 페어) × 1700 샘플 실측에서 한 번에 시킨 번역의 정확도는 2.1%~47.3% 범위로 나타났다.

 

47.3%는 좋아 보이지만 이것이 최고치다. 같은 측정에서 어떤 페어는 2.1%까지 떨어졌다. 평균을 내도 절반을 넘기지 못한다. 즉, "한 줄 프롬프트로 코드 번역" 자체가 이미 fail-by-design인 셈이다. 모델 크기의 문제도 아니고 프롬프트 엔지니어링의 문제도 아니다. GPT-4·StarCoder·CodeGen·Llama 2 모두 비슷하게 깨진다.

 

왜일까? 보통 개발자들은 "LLM이 코드를 잘 짠다더라"는 말을 듣고 번역도 잘할 것이라 가정한다. 그러나 코드 생성(generation)과 코드 번역(translation)은 다른 문제다. 생성은 "이런 기능을 만들어달라"는 요구이고 정답이 여러 개다. 번역은 "이 코드의 의미를 보존해 옮겨달라"는 요구이고 정답이 정확히 하나다. LLM은 후자에 약하다.

 

여기서 "LLM 코드 번역"이라는 키워드 자체가 검색 의도를 둘로 쪼갠다. 하나는 "왜 안 되는가", 다른 하나는 "그럼 어떻게 해야 하는가"이다. 본문은 두 질문에 모두 답한다.

 

동적 → 정적 변환이 가장 어렵다 — Python을 Go로 옮길 때 깨지는 진짜 이유

 

Pan et al. 논문이 또 하나 짚은 발견은 동적 타입 → 정적 타입 번역이 제일 어렵다는 점이다. Python·JavaScript처럼 런타임에 타입이 결정되는 언어를, Java·Go처럼 컴파일 타임에 타입이 박혀야 하는 언어로 옮길 때 정확도가 가장 낮게 나타났다.

 

이를 일상 비유로 풀면 다음과 같다. 한국어는 "주어 생략"이 자연스러운데 영어로 옮길 때는 주어를 채워 넣어야 한다. 누가 한 행동인지 모르는 채로 옮기면 영어 문장이 어색해진다. 동적 → 정적 변환에서 LLM이 헷갈리는 지점도 이와 비슷하다.

 

LLM이 동적 → 정적 번역에서 자꾸 망치는 4가지 지점

 

  • 타입 추론: Python의 def add(a, b): return a + b를 Go로 옮길 때 a, b가 int인지 string인지 generic인지 LLM이 추측한다. 추측이 틀리면 컴파일이 깨진다.
  • nil/None 처리: Python의 None 체크는 단순히 if x is None이지만 Go는 zero value, nil pointer, empty interface가 모두 다르다. LLM이 무지성으로 nil로 옮기다가 panic을 터뜨린다.
  • generic vs interface{}: Python의 duck typing을 Go 1.18 이전 코드처럼 interface{}로 박아버리면 타입 안전성이 박살난다. 1.18 이후 generic이 있는데도 LLM이 옛날 패턴으로 출력하는 빈도가 높다.
  • error 반환 패턴: Python은 try/except, Go는 if err != nil이다. LLM이 try/except를 panic/recover로 옮기는 실수가 잦다. Go 컨벤션상 panic은 거의 사용하지 않는다.

 

이 4가지 지점은 단순한 syntax 변환이 아니라 언어 철학의 차이다. "Neural Code Translation Taxonomy" 논문(ASE 2023)이 코드 번역을 4가지 타입(token-level, syntactic-level, library-level, algorithm-level)으로 분류했는데, type-3 라이브러리 레벨type-4 알고리즘 레벨 같은 지식 의존적 번역이 LLM이 가장 취약한 영역이다. Python → Go는 type-3에서 type-4 사이에 걸쳐 있어 어려운 것이다. HumanEval-X 같은 멀티언어 벤치마크가 Go를 포함하는 것도 이 어려움 때문이다.

 

깨지는 패턴은 무작위가 아니다 — 학계가 분류한 LLM 코드 번역 버그 15종

 

Pan et al.는 1700 샘플의 실패를 분석하여 15개 버그 카테고리로 분류했다. 무작위가 아니라 패턴이 있다는 점이 이 분류의 의의다. 일부만 추리면 다음과 같다.

 

  • Syntax 직역: 소스 언어 문법을 그대로 옮기다가 타겟 언어에서 무효한 문법이 된다. Python [x for x in xs]를 Go에 그대로 옮기는 경우다.
  • API 의미 mismatch: 함수 이름은 비슷하지만 동작이 다른 API로 매핑한다. Python list.append를 Go slice = append(slice, x) 대신 다른 패턴으로 옮기다가 깨진다.
  • Target language requirement 위반: 타겟 언어가 요구하는 패턴(Go의 명시적 error 반환 등)을 누락한다.
  • Dead code 생성: 의미가 없는 코드 블록을 끼워 넣는다.
  • Incorrect output format: 출력 형식이 미묘하게 다르다.

 

여기에 후속 논문 TransLibEval이 200 task(Python·Java·C++ 라이브러리 의존 코드)로 측정한 syntactic 에러 내부 비율은 더 구체적이다.

 

syntactic 에러 하위 카테고리 비율
Syntax structure 38.4%
Language feature 21.86%
Object definition 39.73%

 

Object definition 에러 39.73%가 핵심 시사점이다(syntactic 에러 카테고리 내부 비중 기준). numpy·pandas처럼 외부 라이브러리에 의존하는 코드를 옮길 때, LLM이 타겟 언어의 동등한 객체·자료구조 정의를 잘못 만들어내는 비중이 syntactic 실패 중 약 40%로 가장 크다. Python pandas.DataFrame을 Go로 옮길 때 동등한 추상화가 없기 때문에 LLM이 어설프게 struct로 흉내 내다가 의미가 깨지는 것이다.

 

CodeMEnv는 한 발 더 들어가서, 같은 언어 안에서 라이브러리 버전만 바뀌어도 LLM이 약하다는 점을 보였다. 19개 Python·Java 패키지의 922개 실제 함수 변경 데이터에서, LLM이 신·구 버전 API 호출을 정확히 매핑하는 비율이 낮게 나타났다. 즉, 언어 페어 번역 이전에 의존성 관리 자체가 LLM의 약점이다.

 

반전 — Go가 사실은 코드 번역의 lingua franca다

 

여기서 학계가 제시한 반전이 있다. "Unraveling the Potential of LLMs in Code Translation: How Far Are We?" 논문14개 언어 × 4 LLM 패밀리로 측정한 결과, source → target 직접 번역보다 source → Go → target 2단 번역의 정확도가 더 높게 나타났다.

 

이것이 lingua franca 발견이다. 라틴어가 중세 유럽 학술어로 쓰였던 것처럼, Go가 LLM 코드 번역의 중간 허브 역할을 더 잘 수행한다는 의미다. 직관적으로는 이상하다 — 단계가 늘면 오차도 늘 것 같다. 그러나 측정 결과는 반대였다.

 

논문이 제시한 설명은 다음과 같다. Go는 문법이 단순하고 패턴이 일관적이다. 키워드 25개, 강제되는 컨벤션, 명시적 error 처리, 늦게 들어온 generic으로 인해 얕은 추상화 레이어를 갖는다. LLM이 Go로 옮긴 중간 결과물은 의미가 명확하게 정렬된 상태가 된다. 그 상태에서 다른 언어로 한 번 더 옮기면 모호함이 줄어든 채로 출발하므로 정확도가 올라간다.

 

실무 시사점은 분명하다. Java를 Rust로 옮기고 싶다면, Java → Rust로 직접 가는 대신 Java → Go → Rust로 가는 것이 통계적으로 유리하다. Python을 TypeScript로 옮길 때도 마찬가지다. Go로 일단 갈아두면 다른 데로 옮기기가 쉬워진다 — 이것이 14개 언어 측정으로 확인된 사실이다.

 

이 발견은 글 후반의 "Python → Go 적용 사례"가 단순한 예시가 아니라 권장 전략의 첫 단계라는 의미이기도 하다. Python agent 파이프라인을 Go로 옮기는 작업은 그 자체로 가치가 있고, 이후 Rust로 또 옮기든 어디로 옮기든 출발점으로 쓸 만하다.

 

Google 12개월 사내 사례 — 환각율 25.55%를 막은 6단 validation gate

 

학계 통계가 절망적이었다면, Google 2025 논문 "Migrating Code At Scale With LLMs At Google"(FSE 2025)는 그래도 어떻게 운영하는지를 보여주는 사례다. 12개월 사내 운영, 595개 변경, 그중 74.45%가 LLM 자동 생성, 환각율 25.55%, 파일당 3회 시도 예산, 시간 50% 절감이다.

 

여기서 핵심은 환각율이 25.55%인데도 시간 50%를 줄였다는 점이다. 4번에 1번꼴로 깨지는 모델로도 운영이 가능했던 이유가 6단 validation gate다. 게이트 하나하나가 환각을 거른다.

 

게이트 무엇을 검증하나 통과 못하면
1. Success LLM이 응답 자체를 줬나 (timeout, 빈 응답) 재요청
2. Whitespace 공백 외에 실질 변경이 있나 스킵 처리
3. AST parser 결과물이 파싱 가능한가 (혹은 AST 동일) 재시도
4. Punt (LLM self-check) 모델이 변경 자체가 필요했는지 직접 판정 재시도
5. Build 빌드 시스템이 통과하는가 재시도
6. Test 기존 테스트가 통과하는가 사람 리뷰

 

이 게이트의 핵심은 계단식이라는 점이다. 1단에서 떨어지면 2단을 보지 않는다. 6단까지 모두 통과하면 사람이 더 보지 않고 머지한다. 6단에서 떨어지면 사람에게 넘긴다. 이렇게 하면 LLM이 환각을 일으켜도 — 25.55%는 환각하지만 — 머지되기 전에 잡힌다.

 

파일당 3회 시도 예산을 박은 것도 중요하다(논문 그대로 인용하면 "three modification attempts are made per file"). 1회로 끝낸다고 가정하면 정확도가 47%에서 멈추지만, 시도를 늘리고 게이트로 거르면 통과율이 더 올라간다. 재시도가 공짜가 아니라 예산이라는 발상이 운영의 핵심이다.

 

요약하면 Google이 한 일은 "LLM을 똑똑하게 만들기"가 아니라 "LLM은 어차피 깨진다고 가정하고 절차로 거르기"다. 이 발상의 차이가 사내 593건을 돌릴 수 있게 한 것이다.

 

자동화는 어디까지 — translate-then-fix 패러다임의 등장

 

Google 게이트를 사람이 운영했다면, 학계는 이를 자동화하려고 한다. 그 흐름이 translate-then-fix 패러다임이다.

 

대표 논문이 TransAgent다. fine-grained execution alignment라는 발상으로, LLM이 일단 옮긴 결과물을 컴파일러에 직접 넣어서 syntax error를 받고, 그 에러 메시지를 다시 LLM에게 던져 iterative하게 패치한다. "한 번에 끝내는 번역 vs 반복 검증"의 정량 비교에서, 반복 검증이 단번 번역보다 측정 정확도를 유의미하게 올렸다는 결과를 보고한다.

 

비슷한 결의 후속 작업이 여럿이다.

 

  • AgentCoder: generator + tester + executor 3개 에이전트를 분리한다. generator가 만들고, tester가 케이스를 만들고, executor가 돌린다. 한 모델이 다 하는 것보다 분리했을 때 정확도가 올라간다.
  • RLEF: 실행 결과를 강화학습 보상으로 써서 코드 LLM SOTA를 갱신한다. 컴파일·테스트 통과 여부를 RL signal로 사용하는 것이다.
  • Many-Shot Fails 보고: 흥미로운 반전인데, 코드 번역에서는 many-shot prompting이 오히려 역효과라는 측정 결과다. 일반 자연어 태스크에서는 예시를 많이 주면 잘하는데, 코드 번역은 예시가 늘면 정확도가 떨어졌다. 즉 "프롬프트에 예시 잔뜩 박기" 전략이 이 도메인에서는 통하지 않는다.

 

자동화 흐름의 공통점은 "LLM 출력 → 외부 검증 도구 → 다시 LLM" 루프다. 게이트를 자동화하든 RL signal로 박든, 외부 도구의 ground truth를 LLM 안으로 가져오는 것이 핵심이다.

 

Python agent 파이프라인을 Go로 옮기는 절차서

 

여기까지가 일반론이다. 이제 Python agent 파이프라인을 Go로 옮긴다고 가정하고 위 학계 발견 6개를 stage-by-stage 절차로 박아본다. 어떤 언어 페어든 응용 가능한 일반 절차이고, Python → Go는 한 토막 사례다.

 

1단계 — 인터페이스·struct 먼저 정의

한 번에 옮기지 말고, 타겟 언어가 요구하는 구조부터 손으로 박는다. Pan et al.가 분류한 "target language requirement 위반" 버그 카테고리를 회피한다. agent 파이프라인이라면 Stage interface, Pipeline struct, Result struct를 사람이 먼저 정의한다. 이것이 LLM의 자유도를 줄여서 환각 면적을 좁힌다.

 

2단계 — stage 단위로 분리 번역

파일을 통째로 옮기지 말고 함수·메서드 단위로 쪼갠다. Google 사례에서 파일 단위가 평균 3회 재시도였던 점을 떠올리면, 더 작은 단위는 1~2회로 끝낼 수 있다. 함수 < 모듈 < 패키지 순서로 작을수록 환각 노출 면적이 작다.

 

3단계 — 각 stage마다 6단 게이트 적용

Google 6단 게이트를 stage마다 강제한다. AST 파싱(3단)은 go/parser 패키지로 자동화가 가능하고, Build(5단)는 go build, Test(6단)는 go test로 자동화한다. Punt 셀프 체크(4단)만 LLM 호출이 또 들어가는데, 이는 비용 대비 효과가 크므로 유지한다.

 

4단계 — AST diff 검증

TransAgent 패러다임을 차용하여, 옮긴 Go 코드의 AST와 원본 Python AST의 구조적 동등성을 diff로 확인한다. 함수 시그니처 개수, 분기 깊이, 반복 구조가 맞는지 자동 비교한다. 이 단계에서 dead code나 incorrect output format 버그가 잡힌다.

 

5단계 — Go가 끝점이 아니라 lingua franca임을 의식

Unraveling 발견을 떠올린다. 지금 Go로 옮기는 작업은 그 자체로 가치 있지만, 동시에 이후 Rust나 다른 언어로 또 옮길 때 출발점으로 쓸 수 있게 코드를 정돈한다. 즉 Go 컨벤션에 100% 맞춰 두기 — generic, error 처리, package 분리 — 그것이 다음 단계 번역의 정확도까지 올린다.

 

6단계 — 라이브러리 의존부는 별도 처리

TransLibEval의 39.73% Object definition 에러를 의식하여, numpy·pandas 같은 의존 코드는 LLM에 한 번에 던지지 않는다. 동등한 Go 추상화(gonum, 자체 struct 등)를 사람이 먼저 결정한 뒤, 그 결정을 프롬프트에 박아서 LLM이 추측하지 않도록 한다. 의존성 매핑은 LLM의 가장 약한 영역이므로 인간의 결정이 들어가야 한다.

 

직접 옮기지 말고 절차로 환각을 거른다 — 학계 12개월 실측이 확정한 사실

 

정리하면 다음과 같다.

 

  1. ICSE 2024 1700 샘플 실측에서 한 번에 시킨 LLM 코드 번역의 정확도는 2.1~47.3%다. 절반도 맞히지 못한다. 본인의 프롬프트 문제가 아니라 모델의 본질적 한계다.
  2. 깨지는 양상은 무작위가 아니라 15개 버그 카테고리로 분류된다. TransLibEval은 Object definition 에러만 39.73%로 측정했다.
  3. Google 12개월 사내 사례(595 변경)는 환각율 25.55%인데도 운영을 굴렸다. 비결은 6단 validation gate(Success → Whitespace → AST → Punt → Build → Test) + 파일당 3회 시도 예산이다.
  4. Go가 lingua franca라는 반전 발견(Unraveling, 14언어 × 4 LLM)이 있다. source → Go → target 2단이 직접 번역보다 정확하다.
  5. 자동화는 translate-then-fix 패러다임(TransAgent, AgentCoder, RLEF)으로 진행 중이지만, many-shot prompting은 오히려 역효과다.

 

각설하고, LLM 코드 번역에서 깨지지 않으려면 결국 "한 번에 시키지 말 것" 한 줄로 요약된다. Pan et al., Google, Unraveling, TransAgent, TransLibEval, CodeMEnv까지 학계 6편이 12개월 넘게 실측한 사실이다. 직접 옮기지 말고 절차로 환각을 거른다 — 이것이 LLM 코드 마이그레이션의 운영 원칙이다.

 

다음 글에서는 위 6단 게이트를 실제 CI 파이프라인(GitHub Actions + Go 빌드 + 자동 LLM 재시도)에 박는 코드를 풀어볼 예정이다. 이 글은 학계 정리, 다음 글은 구현 — 이렇게 한 세트로 묶어 시리즈로 진행할 것이다.

728x90
반응형
Share Link
reply
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31