
LLM 생성 코드와 5년을 함께하기 위한 3가지 아키텍처 전략
요약
LLM 생성 코드가 가진 문맥 결여와 유지보수성 문제를 해결하기 위한 3가지 아키텍처 전략을 제시합니다. 의도의 외재화, 명확한 경계 설정, 테스트 가독성 확보를 통해 AI 생성 코드의 이해 격차를 줄이는 방법을 다룹니다.
핵심 포인트
- ADR(Architecture Decision Records)을 활용해 코드 구현의 의도를 기록하고 추적할 것
- 인터페이스 퍼스트(Interface-first) 접근법으로 모듈 간 경계를 명확히 유지할 것
- LLM이 기존 설계 패턴과 명명 규칙을 준수하도록 가이드라인을 제공할 것
- 단순히 작동하는 코드를 넘어 유지보수 가능한 설계를 위한 인간의 개입이 필수적임
- LLM이 작성한 코드는 「작동」하지만 「이해하기 쉽다」고는 할 수 없다
- 유지보수성을 유지하려면
의도의 외재화 · 경계의 명확화 · 테스트 가독성 확보의 3개 축이 필요 - 팀 개발에 있어서는 「코드를 읽는 사람이 있다는 전제」의 설계 습관을 LLM 사용 후에도 무너뜨리지 말 것
GitHub Copilot, Cursor, Claude Code 등 LLM을 사용한 코딩 지원 도구가 보급되면서, 개인 개발자부터 엔터프라이즈 팀까지 일상적으로 AI 생성 코드를 리포지토리(Repository)에 머지(Merge)하게 되었다.
생산성은 올라갔다. 하지만 새로운 문제가 부상하고 있다.
「직접 쓰지 않은 코드를, 내가 얼마나 이해하고 있는가?」
Hacker News나 Aha! Engineering Blog 등에서 반복적으로 화제가 되는 이 질문은, 2026년 시점에서도 아직 명확한 답이 나오지 않은 실천적 과제다. LLM이 생성하는 코드는 다음과 같은 특성을 갖기 쉽다.
국소적으로는 맞지만 전체 문맥에서 벗어나 있음: 함수 단체는 작동하지만, 기존의 명명 규칙(Naming convention) · 에러 처리 패턴과 괴리되어 있음 -
왜 그렇게 구현했는지에 대한 코멘트가 없음: LLM은 코멘트를 생략하는 경향이 있어 의도를 알 수 없음 -
테스트가 「통과만 하는」 것을 생성하기 쉬움: 어설션(Assertion)이 느슨하고, 경계값 · 에러 계통이 빈약함
본 기사에서는 이 「이해 격차」를 메우기 위한 3가지 아키텍처적 · 운용적 전략을 해설한다.
LLM에게 「이 함수를 작성해줘」라고 부탁했을 때, 당신의 머릿속에는 「왜 그 구현이 필요한가」라는 문맥이 있다. 하지만 생성된 코드에는 그 문맥이 존재하지 않는다.
3개월 후에 다른 멤버가 그 코드를 보았을 때, 의도를 알 수 없다면 리팩터링(Refactoring)도 확장도 두려워진다.
docs/
adr/
0001-use-exponential-backoff-for-stripe-retry.md
...
ADR의 형식은 Michael Nygard의 제안이 널리 사용되고 있다.
# ADR-0002: 사용자 목록 페이지네이션에 커서 방식을 채택
## 상태: 승인됨 (2026-07-17)
## 컨텍스트
...
포인트: 프롬프트(Prompt)를 ADR에 남기면 「왜 이 코드가 되었는가」를 추적할 수 있다. LLM이 특정 구현 패턴을 선택한 이유도 언어화된다.
LLM에게 큰 태스크를 던지면, 기존 모듈의 책임을 무시하고 「작동하는 코드」를 생성할 때가 있다.
예를 들어 「주문 확인 메일을 보내는 처리를 추가해줘」라고 부탁하면:
// LLM이 생성하기 쉬운 코드 (악례)
async function createOrder(userId: string, items: Item[]) {
// DB 저장
...
createOrder가 메일 전송의 상세 내용을 알게 되어 버린다. 테스트도 어려워진다.
// 먼저 인터페이스(Interface)를 인간이 정의한다
interface OrderService {
createOrder(userId: string, items: Item[]): Promise<Order>;
...
이 타입 정의를 LLM에 대한 지시에 포함한다:
「다음 interface에 따라 createOrder를 구현해줘.
메일 전송은 NotificationService로 위임할 것.
직접 sendgrid를 호출하지 말 것.」
// LLM이 생성한 구현 (경계가 지켜진 버전)
class OrderServiceImpl implements OrderService {
constructor(private notificationService: NotificationService) {}
...
인터페이스 퍼스트(Interface-first)로 LLM에게 전달하면, 생성된 코드가 기존 설계에 녹아들기 쉬워진다.
// trait를 먼저 정의 → LLM에게 「이 trait를 구현해줘」라고 전달
pub trait NotificationPort {
async fn send_order_confirmation(&self, order: &Order) -> Result<(), NotificationError>;
...
Rust의 타입 시스템은 LLM 생성 코드의 「경계 침식」을 컴파일 시점에 검출할 수 있다는 강점이 있다. 타입이 맞지 않으면 컴파일 에러가 발생하므로, 리뷰 비용을 낮출 수 있다.
// LLM이 생성하기 쉬운 테스트 (나쁜 예)
it("works", async () => {
const result = await createOrder("user1", [{ id: "item1", qty: 1 }]);
...
이 테스트는:
- 무엇을 검증하는지 불분명함 (
toBeTruthy는 무엇이든 통과함) - 테스트 이름이 사양(Specification)을 나타내지 않음 (
works는 의미가 없음) - 실패 시 어떤 사양이 깨졌는지 알 수 없음
LLM에게 다음과 같이 요청하십시오:
"다음 사양을 커버하는 테스트를 Given-When-Then 형식으로 작성해줘.
테스트 이름은 '〇〇일 때, △△하면, ××가 된다' 형식으로 작성할 것."
사양:
- 재고가 0인 상품을 주문하려고 할 경우,
InsufficientStockError가 발생한다. - 주문이 성공했을 경우,
NotificationService.sendOrderConfirmation이 1회 호출된다.
// LLM이 생성한 테스트 (가독성을 지정했을 경우)
describe("OrderService.createOrder", () => {
describe("재고가 0인 상품을 주문하려고 할 때", () => {
...
테스트가 '사양서'로서 기능하는 상태를 유지함으로써, LLM 생성 코드를 인간이 리뷰할 때의 비용을 대폭 낮출 수 있다.
LLM 코딩을 팀에서 운용할 때 효과적이었던 프랙티스(Practice)를 정리한다.
| 확인 항목 | 이유 |
|---|---|
| LLM 생성 코드의 프롬프트(Prompt)가 ADR / PR 설명에 기재되어 있는가 | 의도의 추적 가능성 |
| ... |
Cursor에서는 .cursor/rules에, GitHub Copilot에서는 .github/copilot-instructions.md에 프로젝트 고유의 규칙을 작성함으로써, LLM이 생성하는 코드의 품질을 사전에 제어할 수 있다.
# .cursor/rules (예시)
## 에러 핸들링 (Error Handling)
- 도메인 에러는 `src/errors/`에 정의된 클래스를 사용할 것
...
LLM 생성 코드와 장기적으로 함께하기 위해서는 다음의 3가지가 핵심이다.
- 의도를 외재화하기: ADR과 프롬프트 로그를 통해 "왜 이 구현인가"를 남긴다.
- 경계를 타입(Type)으로 선언하기: 인터페이스(Interface)나 트레이트(Trait)를 먼저 정의한 후 LLM에게 구현을 시킨다.
- 테스트를 사양서로 유지하기: Given-When-Then과 의미 있는 테스트 이름을 LLM에 대한 지시 사항에 포함시킨다.
"LLM이 썼으니 어쩔 수 없다"라고 방치하면 기술 부채(Technical Debt)가 쌓일 뿐이다. LLM을 팀의 신입 멤버로 대우하고, 기존의 설계 사상·명명 규칙·테스트 문화에 따르도록 만드는 습관을 들이는 것이 코드베이스의 장기적인 건전성을 지키는 열쇠가 된다.
- Documenting Architecture Decisions - Michael Nygard (2011)
- Staying familiar with the code when it's written by an LLM - Aha! Engineering
- GitHub Copilot custom instructions
- cursor.directory - Cursor Rules Examples
- TypeScript Handbook - Interfaces
- The Rust Programming Language - Traits
✍️ 본 기사의 저자: 합동회사 지모라보 (Jimolab Co., Ltd.) (@locallab_jp)
지모라보는 하치오지를 거점으로 AI를 활용한 SaaS를 다수 개발하고 있습니다. 본 기사의 기술 검증 또한 그러한 개발 과정의 부산물입니다.
- 🌐 공식 사이트: https://locallab.jp
- 🔍 AI SEO 최적화 SaaS: lookupai.jp
- 📺 YouTube: @locallab_llc
- ✉️ 문의: info@locallab.jp
관심이 생기셨다면 각 SNS 팔로우도 꼭 부탁드립니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기