지난 글에서 Anthropic의 Claude Code를 살펴봤다. 이번엔 현대 AI 코딩 도구들의 출발점이 된 OpenAI Codex와 그 후속 제품들의 흐름을 따라간다. 오늘날 우리가 당연하게 쓰는 AI 코딩 도구 대부분의 뿌리가 여기에 있다.
다만 계보만 훑고 끝내지는 않는다. 실제로 부딪히는 문제는 「어느 모델에 무엇을 맡기는가」, 「모델이 써 준 코드를 어디서 실행하는가」, 「팀에 들일 때 무엇을 확인하는가」 셋이다. 벤치마크 점수를 읽는 법과 실행 격리 설계에 더 많은 자리를 둔 것은 그래서다.
모델 계보
Codex
2021년 8월, OpenAI가 Codex를 발표했다. GPT-3를 GitHub의 공개 코드로 파인튜닝한 모델로, 자연어 설명을 함수 본문으로 바꾸는 일을 처음 대중 앞에 보였다. 여기서 파인튜닝은 학습을 이미 마친 모델에 특정 영역의 데이터를 더 먹여 그 영역 쪽으로 숫자를 조금 더 옮기는 일이다 — 모델을 새로 만드는 것이 아니라 있는 모델을 그쪽으로 기울이는 것이다. HumanEval 벤치마크에서 28.8%를 기록했고, GitHub Copilot의 첫 기반 모델이 됐다.
Codex API는 2023년 3월에 공식적으로 내려갔다. 코드 전용 모델이 범용 모델에 밀려난 것인데, 이 자리가 이후 흐름을 압축해 보여 준다. 코드만 먹인 작은 모델보다 온갖 텍스트를 먹인 큰 모델이 코드도 더 잘 썼다. 지금의 도구들이 코딩 전용 모델을 따로 두지 않고 범용 모델에 도구와 실행 환경을 붙이는 쪽으로 간 이유가 여기 있다. 「코딩 잘하는 모델」을 고르는 일이 사실상 「가장 좋은 범용 모델」을 고르는 일과 같아진 것도 같은 사건의 결과다.
ChatGPT
2022년 11월의 ChatGPT는 GPT-3.5에 RLHF를 적용한 모델이었다. 코딩 전용 제품이 아니었는데도 개발자들 사이에서 먼저 퍼진 이유는 성능이 아니라 형식이었다. 에러 메시지와 코드를 통째로 붙여 넣고 왜 이러느냐고 물을 수 있는 자리가 그때까지 없었기 때문이다. 자동완성은 다음 줄을 채워 주지만 이미 난 오류를 설명해 주지는 못한다.
그때 생긴 습관이 지금도 유효하다. 붙여 넣을 때 에러 메시지의 마지막 줄만 잘라 넣으면 모델은 앞에서 무슨 일이 있었는지를 추측하게 되고, 추측이 틀리면 엉뚱한 곳을 고치라고 한다. 스택 트레이스 전체와 문제가 난 함수, 그리고 입력 값이 실제로 어떤 모양이었는지를 함께 주는 편이 왕복을 줄인다. 값의 모양이란 타입과 길이, 그리고 비어 있을 수 있는지 정도를 말한다.
# 1. 에러 메시지와 코드를 함께 붙여넣기
"""
다음 에러가 발생합니다:
TypeError: unsupported operand type(s) for +: 'int' and 'str'
코드:
def calculate_total(items):
total = 0
for item in items:
total = total + item['price'] # ← 여기서 에러
return total
items는 DB에서 온 dict 리스트이고 price가 문자열로 들어오는 행이 섞여 있습니다.
"""
# 2. 요구사항을 구체적으로 명시
"""
Python으로 다음 기능을 구현해줘:
- 입력: 주문 목록 (order_id, amount, status 포함)
- 처리: status가 'completed'인 주문의 amount 합계
- 출력: 합계와 건수를 딕셔너리로 반환
- 조건: type hint 포함, docstring 포함
"""
GPT-4o
GPT-4o는 텍스트·이미지·음성을 한 모델에서 받는 멀티모달 모델이고, 지금 범용 작업의 기본값이다. 코드 생성에서 따로 신경 쓸 파라미터는 temperature 하나다. 다음 낱말을 고를 때 확률이 낮은 후보를 얼마나 허용할지 정하는 값이라, 문장을 다양하게 쓰고 싶을 때는 올리고 컴파일이 돼야 하는 코드에는 내린다. 0.1 근처면 같은 요청에 거의 같은 코드가 돌아오므로, 결과가 달라졌을 때 그 원인을 모델이 아니라 내 프롬프트에서 찾을 수 있다.
시스템 프롬프트에 무엇을 적을지는 취향이 아니라 팀 규칙이다. 타입 힌트를 쓰는지, 주석을 어느 말로 다는지, 예외를 삼키지 않고 올리는지 같은 항목은 매번 사람이 고쳐 주기보다 시스템 프롬프트에 한 번 못 박아 두는 편이 싸다. 이 문자열을 저장소의 파일 하나로 두고 코드에서 읽어 쓰면, 규칙이 바뀌었을 때 고칠 자리가 하나로 모인다.
from openai import OpenAI
client = OpenAI()
def generate_code(specification: str, language: str = "python") -> str:
"""
자연어 명세를 코드로 변환
"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": f"""당신은 {language} 전문가입니다.
다음 명세에 따라 코드를 생성하세요:
- 타입 힌트 필수
- 에러 처리 포함
- 주석은 한국어
- 테스트 코드도 함께 제공"""
},
{"role": "user", "content": specification}
],
temperature=0.1, # 코드 생성은 낮은 temperature 권장
)
return response.choices[0].message.content
HumanEval 통과율
측정 방식
HumanEval은 164개의 파이썬 함수 문제로 된 벤치마크다. 각 문제는 함수 시그니처와 설명 주석을 주고 본문을 채우게 한 다음, 숨겨 둔 단위 테스트를 돌려 통과하면 1점을 준다. 사람이 읽고 채점하는 것이 아니라 테스트가 채점하므로 채점자에 따라 값이 흔들리지 않고, 그래서 모델 사이를 견주는 자로 오래 쓰였다.
28.8%와 92%가 같은 자로 잰 값이라는 점이 이 수의 전부다. 5년 사이에 무엇이 얼마나 달라졌는지를 한 줄로 보여 주지만, 그 자가 재는 범위 밖의 일은 아무것도 말해 주지 않는다. 벤치마크는 절대적인 실력이 아니라 한 가지 과제에서의 비교 순위를 준다.
사각지대
이 벤치마크의 문제는 하나같이 자기 완결이다. 외부 파일도, 다른 모듈도, 이미 있는 코드베이스의 규칙도 없다. 그런데 업무 코드가 어려운 이유는 대개 알고리즘이 어려워서가 아니라 이미 있는 코드와 맞물려야 해서다. 우리 프로젝트의 로깅 방식, 예외 클래스, 트랜잭션 경계를 지키면서 함수를 하나 끼워 넣는 일이 실제 난이도이고, 그 부분이 통째로 빠져 있다.
두 번째로, 문제와 테스트가 공개돼 있다. 공개된 데이터로 학습한 모델이라면 정답을 이미 본 적이 있을 수 있는데 이것을 오염이라고 부른다 — 시험 문제가 교재에 실려 있는 상태다. 오염이 얼마나 섞였는지는 바깥에서 셀 수 없으므로, 공개된 지 오래된 벤치마크의 높은 점수는 언제나 얼마쯤 할인해 읽어야 한다.
셋째로 92%를 「열 번 중 아홉 번 맞는다」로 읽으면 실무에서 어긋난다. 그 값은 스무 줄짜리 독립 함수에서의 확률이고, 파일 셋을 동시에 고치는 작업의 성공률은 그보다 한참 낮다. 작업이 커질수록 틀릴 자리가 곱으로 늘기 때문이다.
통과율의 쓸모
그래도 쓸모가 없지는 않다. 새 모델이 나왔을 때 후보를 셋 정도로 좁히는 데는 충분하고, 세대 사이의 차이가 클 때는 그 차이가 실무에서도 대체로 느껴진다. 다만 거기까지다. 좁힌 다음에는 자기 표본으로 다시 재는 편이 훨씬 정확하다. 지난 달에 실제로 고친 이슈 스무 개를 골라 같은 프롬프트로 각 모델에 돌리고, 사람이 손대지 않고 통과한 비율을 세면 된다. 스무 개는 하루면 만들고, 그 뒤로 모델이 바뀔 때마다 같은 자로 잴 수 있다.
표본을 고를 때 실패한 작업을 일부러 섞는 것이 중요하다. 성공한 이슈만 모으면 우리 팀이 이미 잘하는 종류만 재게 되고, 그 자는 모델을 바꿀 이유를 알려 주지 않는다. 사람이 두 번 이상 손댔던 이슈, 리뷰에서 되돌아온 변경, 재현이 까다로웠던 버그가 표본에 들어가야 모델 사이의 차이가 드러난다.
추론 모델과 일반 모델
갈림의 기준
o-series는 답을 바로 내지 않는다. 내부에서 단계를 밟아 가며 후보를 세우고 검토한 뒤 최종 답만 돌려준다. 그 중간 단계도 토큰으로 세므로 요금과 지연에 그대로 들어간다. 그래서 가르는 질문은 「이 문제가 어려운가」가 아니라 「틀렸는지 내가 바로 아는가」다.
타입 오류나 문법 실수는 실행하면 바로 드러나므로 일반 모델에 맡기고 틀리면 다시 시키는 편이 싸다. 반대로 경계 조건이 틀린 정렬 로직, 동시성 문제, 복잡한 SQL의 조인 조건처럼 돌아가긴 하는데 틀린 결과가 나오는 종류는 사람이 검토하는 비용이 크다. 추론 모델이 값하는 자리가 여기다. 실패가 조용한 작업일수록 앞단에 돈을 쓰는 편이 싸게 먹힌다.
reasoning_effort
추론 모델에는 생각을 얼마나 길게 할지 정하는 손잡이가 있다. 낮은 단계는 일반 모델과 비슷한 속도로 돌아오고, 높은 단계는 몇 배의 시간과 토큰을 쓴다. 기본값에서 시작해 실패한 문제에만 한 단계 올리는 방식이 무난하다. 처음부터 최고 단계로 두면 쉬운 문제에서도 요금이 그대로 나간다.
response = client.chat.completions.create(
model="o4-mini",
reasoning_effort="high", # low / medium / high
messages=[{
"role": "user",
"content": """
다음 문제를 O(n log n) 이내로 해결하는 Python 코드를 작성해줘:
n개의 정수 배열에서 두 원소의 합이 target과 같은
모든 쌍의 인덱스를 반환하라.
같은 인덱스는 두 번 사용할 수 없으며, 결과의 순서는 상관없다.
예: nums=[2,7,11,15], target=9 → [[0,1]]
"""
}]
)
비용과 지연
추론 모델은 대화에 맞지 않는다. 한 번 부르는 데 수십 초가 걸리면 사람은 기다리지 않고 다른 창으로 가 버리고, 돌아와서 맥락을 다시 잡는 비용이 답을 기다린 시간보다 크다. 그래서 자리를 나누는 편이 낫다. 화면 앞에서 주고받는 대화는 빠른 모델에 맡기고, 추론 모델은 사람이 자리를 비운 사이에 돌아도 되는 일 — 실패한 테스트 묶음 분석, 대량 리팩터링 후보 검토, 야간 배치 — 에 건다.
지연을 재는 자리도 하나로 두는 편이 낫다. 요청을 보낸 시각과 첫 글자가 온 시각, 마지막 글자가 온 시각 셋을 같이 로그에 남기면 모델을 바꿨을 때 체감이 왜 달라졌는지를 수로 확인할 수 있다. 추론 모델은 첫 글자까지가 길고 그 뒤가 짧으며, 일반 모델은 첫 글자가 빨리 오고 끝까지 길다. 같은 총 시간이라도 사람이 느끼는 답답함은 전자가 크다. 화면에 진행 표시를 둘지 말지는 이 두 수를 보고 정하면 된다.
Code Interpreter 샌드박스
2023년 7월 ChatGPT에 통합된 Code Interpreter는 모델이 실제 파이썬 코드를 실행하고 결과를 보여 주는 기능이다. 데이터 분석과 시각화, 파일 변환에서 즉시 쓸모가 있지만, 이것이 어떤 상자 안에서 도는지를 모르면 되는 일과 안 되는 일이 매번 우연처럼 느껴진다.
네트워크 차단
가장 자주 걸리는 벽이다. 실행 환경에 바깥으로 나가는 통로가 없다. 패키지를 새로 설치하려는 코드, 공개 API를 호출하는 코드, URL에서 데이터를 내려받는 코드는 전부 실패한다. 모델은 이 사실을 잊고 설치 명령을 먼저 쓰는 일이 잦으므로, 필요한 파일은 처음부터 업로드해서 넣는 것이 확실하다. 인터넷에서 가져오라고 시켰는데 그럴듯한 결과가 나왔다면 한 번 의심해 볼 일이다. 실행이 아니라 모델이 기억에서 지어낸 값일 수 있다. 미리 깔린 라이브러리 목록 안에서 해결되는 작업인지를 먼저 묻고 시작하면 왕복이 준다.
파일과 세션
업로드한 파일에는 크기 상한이 있고, 실행 환경은 일정 시간 쓰지 않으면 초기화된다. 초기화되면 올려 둔 파일도, 앞서 만든 변수도 함께 사라진다. 한 시간쯤 다른 일을 하다 돌아와서 「아까 그 데이터프레임에서 이어서」라고 하면 모델이 없는 변수를 참조하는 코드를 써 준다.
그래서 세션이 긴 작업에서는 중간 결과를 파일로 내려받는 자리를 일부러 만든다. 정리한 데이터프레임을 CSV로 저장해 받아 두면 다음에 다시 올려서 이어 갈 수 있다.
결과를 남기는 법
채팅 화면에 뜬 그래프는 기록으로 쓰기 어렵다. 같은 그림을 다시 얻으려면 대화를 처음부터 다시 돌려야 하는데, 세션이 초기화된 뒤에는 그것도 안 된다. 분석이 어느 정도 모양을 갖추면 모델이 쓴 코드 전체를 복사해 로컬 노트북에서 한 번 돌려 보는 편이 낫다. 그 자리에서 결과가 재현되면 그 코드가 분석의 기록이 되고, 재현되지 않으면 어딘가에 환경 의존이 숨어 있다는 뜻이라 어차피 알아야 할 사실이다.
업로드한 데이터에 개인정보나 사내 지표가 들어 있는 경우도 이 자리에서 한 번 멈춰야 한다. 분석을 빨리 보려고 원본 CSV를 통째로 올리는 것이 가장 흔한 사고이고, 컬럼 몇 개만 지우고 올리거나 표본 천 행만 올려도 대개 같은 결론이 나온다.
코드 실행 격리
모델이 써 준 코드를 사람이 읽지 않고 바로 실행하는 구조를 만들 때가 온다. 직접 만드는 경우가 Tool Calling이다 — 모델에 함수 목록을 알려 주고, 모델이 그중 하나를 부르겠다고 하면 우리 서버가 그 함수를 실행해 결과를 돌려주는 방식이다. 문제는 그 함수가 임의의 코드 실행일 때다. 이때는 「모델이 나쁜 코드를 쓸까」보다 「나쁜 코드가 나왔을 때 피해가 어디서 멈추는가」를 설계해야 한다.
컨테이너
아래 예시처럼 호스트에서 곧바로 실행하는 코드는 예제로는 쓸 수 있어도 서비스에 두면 안 된다. 파일 시스템 전체와 환경 변수, 그리고 사내망이 그대로 열린다. 실제로는 요청마다 컨테이너를 새로 띄우고, 네트워크를 끊고, 파일 시스템을 읽기 전용으로 붙이고, 루트가 아닌 사용자로 프로세스를 띄운다. 쓰기가 필요하면 임시 디렉터리 하나만 쓰기 가능으로 열고 작업이 끝나면 컨테이너째 버린다. 한 번 쓰고 버리는 구조라야 앞 요청이 남긴 것이 다음 요청에 섞이지 않는다.
tools = [
{
"type": "function",
"function": {
"name": "run_python",
"description": "Python 코드를 샌드박스에서 실행하고 결과를 반환",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "실행할 Python 코드"}
},
"required": ["code"]
}
}
}
]
def run_python(code: str) -> str:
# 주의: 아래는 최소 예시다. 운영에서는 컨테이너 안에서 실행한다.
result = subprocess.run(
["python", "-c", code],
capture_output=True, text=True, timeout=30
)
out = result.stdout or result.stderr
return out[:8000] # 출력 상한
타임아웃
무한 루프는 모델이 가장 흔하게 만드는 사고다. 악의가 아니라 종료 조건을 하나 빠뜨린 결과이고, 타임아웃이 없으면 그 요청 하나가 워커를 영구히 붙든다. 상한은 두 겹으로 둔다. 프로세스 자체의 실행 시간 제한과, 그것이 듣지 않을 때 컨테이너를 통째로 죽이는 바깥 제한이다. 안쪽 제한만 두면 자식 프로세스를 띄워 놓고 죽은 코드에서 새는 자리가 남는다.
메모리도 같은 이유로 상한을 건다. 큰 배열을 만드는 코드 한 줄이 호스트의 메모리를 다 먹으면 무관한 다른 요청들이 함께 죽는다. 상한에 걸려 죽은 요청은 실패로 끝내지 말고 왜 죽었는지를 모델에 돌려준다. 시간 초과인지 메모리 초과인지만 알려 줘도 다음 시도에서 루프를 고치거나 데이터를 나눠 처리하는 코드가 온다.
출력 크기
의외로 자주 서비스를 세우는 자리다. 실행 결과를 그대로 모델에 되돌려 주는 구조에서, 10만 줄을 출력하는 코드가 한 번 돌면 그 출력이 통째로 다음 요청의 입력이 된다. 요금이 튀고 컨텍스트 창이 넘치고 응답은 실패한다. 표준 출력을 받을 때 앞뒤 일부만 남기고 가운데를 잘라 내는 처리를 넣고, 잘랐다는 사실을 모델에 알려 준다. 몇 줄이 생략됐는지를 같이 적어 주면 모델이 다시 부를 때 범위를 좁혀 부른다. 상한은 문자 수가 아니라 토큰 수로 잡는 편이 정확하지만, 처음에는 몇 킬로바이트 정도의 문자 상한으로 시작해도 사고는 막힌다.
Codex CLI 한 사이클
2025년 OpenAI는 Codex CLI를 내놓았다. 터미널에서 모델을 에이전트로 돌려 저장소의 파일을 직접 고치게 하는 도구다.
저장소 붙이기
# 설치
npm install -g @openai/codex
# 실행
codex
시작하기 전에 확인할 것은 두 가지다. 작업 디렉터리가 고치려는 저장소인지, 그리고 그 저장소가 깨끗한 상태인지다. 커밋하지 않은 변경이 남은 채로 에이전트를 돌리면 내가 쓴 줄과 모델이 쓴 줄이 한 diff에 섞여 되돌릴 때 어느 것이 무엇인지 가릴 수 없다. 시작 전에 커밋하거나 따로 브랜치를 파는 것이 한 사이클의 첫 단계다. 저장소의 규칙을 적어 둔 파일이 있으면 에이전트가 그것을 먼저 읽게 하는 것도 같은 자리에서 한다. 디렉터리 구조, 테스트를 돌리는 명령, 손대면 안 되는 생성 파일 정도를 적어 두면 첫 요청부터 헛도는 일이 줄어든다.
승인 모드
명령마다 사람이 승인하는 모드와 편집을 자동으로 적용하는 모드가 있다. 자동 모드는 익숙해지면 빠르지만, 무엇을 고쳤는지 보지 않고 다음 요청을 보내는 습관이 붙는 것이 진짜 위험이다. 변경이 서너 파일을 넘어가기 시작하면 승인 모드로 돌아오는 편이 낫다.
자동 모드를 쓰더라도 경계를 하나는 둔다. 작업 디렉터리 밖을 건드리지 못하게 하고, 패키지 설치나 배포 명령처럼 되돌리기 어려운 명령은 승인을 받게 하는 식이다. 읽기와 쓰기를 가르는 것이 아니라 되돌릴 수 있는 일과 없는 일을 가르는 것이 기준이다.
변경 검토와 되돌리기
에이전트가 끝났다고 해서 한 사이클이 끝난 것이 아니다. 변경된 파일 목록을 먼저 보고, 예상하지 못한 파일이 섞였는지부터 확인한다. 설정 파일이나 잠금 파일이 함께 바뀌어 있는 경우가 흔하다. 그다음 테스트를 돌리고, 통과하면 사람이 커밋 메시지를 다시 쓴다. 되돌릴 때는 파일 단위로 골라 되돌리기보다 브랜치를 버리고 다시 시작하는 편이 대개 빠르다. 그래서 앞에서 브랜치를 판 것이다.
도입 판단
Canvas와 채팅
ChatGPT의 Canvas는 코드를 옆 패널에 띄워 두고 일부만 골라 고치게 하는 화면이다. 채팅과 갈리는 기준은 취향이 아니라 수정의 크기다. 코드 전체가 다시 출력돼도 눈으로 훑을 수 있는 정도, 대략 오륙십 줄 아래라면 채팅이 빠르다. 그보다 길어지면 매번 전체가 다시 나오는 것 자체가 비용이고, 무엇이 달라졌는지 찾는 데 시간이 든다. 함수 하나만 바꾸는 요청을 반복할 참이면 Canvas가 이득이다. 다만 Canvas 안에서 오래 작업하다 보면 그 코드가 저장소의 어느 파일에서 왔는지를 잊기 쉽다. 붙여 넣을 때 파일 경로를 첫 줄 주석으로 함께 옮겨 두면 되돌아갈 자리가 남는다.
데이터 처리 정책
팀에 들일 때 실제로 막히는 자리는 성능이 아니라 이쪽이다. 확인할 것은 셋이다. 첫째, 무엇이 밖으로 나가는가 — 에디터에 열린 파일만인지, 저장소 전체를 훑어 보내는지. 둘째, 보낸 것이 얼마나 남는가 — 로그 보존 기간과 삭제 요청 경로가 있는지. 셋째, 보낸 것이 모델 학습에 쓰이는가 — 업무용 요금제에서는 대체로 기본이 사용 안 함이지만 개인 요금제와 다를 수 있으므로 계약서에서 확인한다. 이 셋을 문서로 확인하지 않은 채로 사내 코드를 붙이는 일이 가장 자주 나는 사고다.
확인이 끝났으면 그 결과를 사람이 읽을 수 있는 한 쪽짜리 문서로 남긴다. 어떤 요금제를 쓰는지, 어떤 저장소에 붙여도 되는지, 고객 데이터가 든 파일은 어떻게 다루는지를 적어 두면 새로 들어온 사람이 같은 질문을 다시 하지 않는다. 도구를 들이는 비용의 절반은 이 문서를 쓰는 일이다.
선택 가이드
| 작업 | 추천 도구 |
|---|---|
| 빠른 코드 스니펫 | ChatGPT (GPT-4o) |
| 데이터 분석·시각화 | Code Interpreter |
| 조용히 틀리는 로직 | o4-mini / o3 |
| 멀티파일 프로젝트 | Codex CLI |
| 긴 코드의 부분 수정 | Canvas |
| CI/CD 통합 | OpenAI API + Tool Calling |
읽어주셔서 감사합니다. 😊

