내가 LLM으로 소프트웨어를 만드는 방법: 실전 워크플로우와 함정 피하기

컨텍스트 패키지 설계부터 생성-검토-수정 사이클, TDD-LLM 결합, 환각 코드 같은 실전 함정까지 개인 경험을 바탕으로 한 LLM 기반 개발 워크플로우를 정리한다.

2026-08-14 · 최초 발행 2026-03-17

LLM이 코드를 생성할 수 있다는 사실은 이미 모두가 안다. 그러나 LLM을 활용하여 실제로 동작하는 소프트웨어를 꾸준히, 안정적으로 만들어내는 것은 전혀 다른 이야기다. 지난 1년간 LLM을 개발 파이프라인의 핵심 도구로 사용하면서 발견한 패턴, 실수, 그리고 실질적으로 효과를 발휘한 전략을 공유한다.

왜 LLM 기반 개발로 전환했나

2026년 현재, LLM 보조 개발은 단순한 트렌드를 넘어 생산성의 문제가 되었다. GitHub Copilot, Claude, GPT-4o 등의 도구를 제대로 활용하는 개발자와 그렇지 않은 개발자 사이의 생산성 격차는 연구에 따라 30%에서 최대 55%까지 벌어진다는 보고가 나온다. 하지만 단순히 도구를 쓴다고 생산성이 오르는 것이 아니다. LLM을 워크플로우에 올바르게 통합하는 방법을 배워야 한다.

내가 LLM 기반 개발로 전환한 이유는 단순하다. 반복적인 보일러플레이트 코드 작성, API 문서 해석, 기본적인 알고리즘 구현에 소비되던 시간을 아키텍처 설계와 비즈니스 로직에 집중하는 데 쓸 수 있게 되었기 때문이다.

프롬프트를 컨텍스트 패키지로 설계하기

가장 흔한 실수는 프롬프트를 "무엇을 만들어 달라"는 요청으로만 생각하는 것이다. 효과적인 프롬프트는 컨텍스트 패키지다. 좋은 컨텍스트 패키지에는 언어·프레임워크·주요 라이브러리 버전 같은 기술 스택, 기존 코드의 스타일·명명 규칙·에러 처리 방식 같은 코드베이스 패턴, 성능 요구사항·호환성 제한·금지된 패턴 같은 제약 조건, 함수·클래스·모듈 중 무엇인지의 기대 출력 형태가 포함된다.

잘못된 프롬프트 예시는 "Python으로 파일 읽는 함수 만들어줘"다. 좋은 프롬프트 예시는 "Python 3.11, FastAPI 0.104 기반 코드베이스에서 async/await 패턴 사용 중. 기존 코드는 커스텀 FileReadError 예외를 사용함. UTF-8 인코딩, 파일 없을 때 None 반환하는 비동기 파일 읽기 함수 작성. 타입 힌트 필수."이다.

하나의 LLM 세션을 여러 역할로 나누어 사용하는 것도 효과적이다. 동일한 대화에서 구현자와 리뷰어 역할을 번갈아 요청하면 코드 품질이 크게 향상된다.

[구현 요청] → LLM이 코드 생성
[리뷰 요청] "방금 생성한 코드의 문제점을 시니어 개발자 관점에서 지적해줘"
[수정 요청] "지적된 문제를 수정해줘, 특히 [X] 부분에 집중해서"

이 패턴을 사용하면 LLM이 자신의 첫 번째 출력의 약점을 스스로 발견하고 수정하는 효과가 있다.

생성→검토→수정으로 이어지는 반복 사이클

첫 번째 생성 단계에서는 완벽함을 기대하지 않는다. 목표는 동작하는 뼈대(scaffold)를 얻는 것이다. 전체적인 구조가 의도와 맞는지, 사용된 라이브러리와 패턴이 올바른지, 명백한 논리적 오류가 없는지를 이 단계에서 확인한다.

생성된 코드를 그대로 쓰는 것은 가장 큰 실수다. 검토 단계에서는 인간의 판단이 반드시 개입해야 한다. 내가 쓰는 검토 체크리스트는 NULL·빈 입력·경계값 같은 에지 케이스 처리, 에러 처리의 완결성, SQL 인젝션·입력 검증 누락 같은 보안 취약점, N+1 쿼리·불필요한 루프 같은 성능 문제, 의존성 주입·모킹 가능성 같은 테스트 가능성이다.

수정 단계에서는 구체적인 피드백이 핵심이다. "고쳐줘"보다 "37번째 줄의 데이터베이스 쿼리가 루프 안에 있어서 N+1 문제가 발생한다. 쿼리를 루프 밖으로 빼고 bulk fetch로 바꿔줘"가 훨씬 효과적이다.

아니오아니오요구사항 분석컨텍스트 패키지 구성초기 코드 생성 요청생성된 코드 검토품질 기준 충족?구체적 피드백 작성수정 요청테스트 작성 요청테스트 실행모든 테스트 통과?실패 원인 분석코드 리뷰 통합완료

TDD와 LLM을 결합하는 법

TDD(테스트 주도 개발)의 원칙을 LLM 개발에 적용하면 강력한 시너지가 발생한다. 내가 쓰는 변형된 TDD-LLM 사이클은 인간이 실패하는 테스트를 먼저 작성하고, LLM에게 테스트를 통과하는 구현 코드를 요청하고, 테스트 실행으로 구현을 검증하고, LLM에게 엣지 케이스 추가 테스트를 요청하는 과정을 반복하는 순서다. 이 접근법의 장점은 LLM이 "무엇을 만들어야 하는가"를 테스트 코드라는 형식화된 명세로 전달받기 때문에 모호함이 줄어든다는 점이다.

구현 코드가 완성된 후, LLM을 활용해 엣지 케이스를 발굴하는 것도 매우 효과적이다. "아래 함수에 대한 pytest 테스트를 작성해줘. 특히 다음 케이스에 집중해: 경계값, None 입력, 타입 불일치, 동시성 문제, 리소스 정리. 각 테스트에 명확한 docstring 포함." 같은 프롬프트 패턴으로 인간이 놓치기 쉬운 엣지 케이스를 체계적으로 커버할 수 있다.

실전에서 마주치는 함정들

LLM이 생성하는 코드의 가장 위험한 특성은 틀렸지만 그럴싸하게 보인다는 것이다. LLM이 라이브러리의 존재하지 않는 메서드를 자신 있게 사용하는 존재하지 않는 API, 오래된 학습 데이터로 인해 deprecated된 API를 쓰는 버전 불일치, 안전해 보이지만 실제로는 취약한 암호화 구현인 보안 패턴 혼동에 특히 주의해야 한다. 모든 외부 API 호출은 공식 문서에서 직접 확인하고, 보안 관련 코드는 LLM 출력을 출발점으로만 쓰고 반드시 전문가 검토를 거쳐야 한다.

긴 대화가 진행될수록 LLM은 초기 컨텍스트를 "잊기" 시작한다. 갑자기 다른 코딩 스타일을 사용하거나, 초기에 합의한 제약 조건을 무시하거나, 이전에 정의한 함수를 다시 정의하는 것이 이 문제의 징후다. 중요한 컨텍스트(기술 스택, 핵심 제약 조건)를 담은 "컨텍스트 카드"를 만들어 긴 대화에서 주기적으로 다시 주입하는 것으로 대응한다.

LLM에 지나치게 의존하면 정작 중요한 순간에 문제가 생긴다. LLM이 생성한 코드를 이해하지 못하면 디버깅, 최적화, 유지보수가 모두 블랙박스가 된다. LLM은 구현을 가속화하는 도구이지, 이해를 대체하는 도구가 아니라는 것이 핵심 원칙이다. LLM이 생성한 코드라도 왜 그렇게 작동하는지 설명할 수 있어야 한다.

내가 운영하는 워크플로우 전체 구조

실제로 내가 운영하는 LLM 보조 개발 시스템의 전체 구조는 다음과 같다.

실패통합 단계PR 생성CI/CD 파이프라인배포검증 단계자동화 테스트LLM 코드 리뷰보안 스캔구현 단계컨텍스트 패키지 구성코드 생성 요청인간 검토수정 사이클계획 단계요구사항 정제아키텍처 초안(LLM 보조)태스크 분해

최근 RESTful API 서버를 처음부터 구축하는 프로젝트에서 이 워크플로우를 전면 적용했다. 프로젝트 규모는 Python FastAPI, PostgreSQL, Redis 캐싱, JWT 인증을 쓴 약 8,000 LOC였다. LLM은 초안을 작성하고 인간이 관계를 검토·인덱스를 최적화한 데이터베이스 스키마 설계, LLM이 패턴 코드를 생성하고 인간이 비즈니스 로직을 검토한 CRUD 엔드포인트, LLM이 엣지 케이스를 포함해 생성하고 실행해 검증한 단위 테스트, LLM이 docstring과 OpenAPI 스펙을 작성한 문서화 영역에 활용됐다.

측정된 효과는 예상 개발 기간 대비 40% 단축, 초기 코드 커버리지 72%에서 시작(LLM 생성 테스트 포함), 주요 버그 발생 총 3건(모두 환각 API 사용 관련)이었다. LLM 관련 버그 3건 모두 코드 리뷰 단계에서 발견되었다. LLM이 생성한 코드를 직접 실행하지 않고 검토 단계를 거친 것이 효과적이었다.

LLM 기반 소프트웨어 개발은 단순히 코드 생성 도구를 사용하는 것이 아니라, 인간의 판단과 기계의 속도를 결합하는 새로운 개발 방법론이다. 프롬프트를 컨텍스트 패키지로 설계하고, 생성→검토→수정의 반복 사이클을 체계화하며, 테스트를 개발 루프에 통합하면 LLM의 잠재력을 최대한 활용할 수 있다. 가장 중요한 원칙은 LLM이 생성한 코드를 이해하는 인간의 역할을 절대 포기하지 않는 것이다.

Sources

LLM개발프롬프트설계AI코딩워크플로우TDD생성검토수정사이클