소프트웨어 3.0 시대를 맞이하며
소프트웨어 3.0 시대란?
2025년 6월, Andrej Karpathy는 Y Combinator AI Startup School에서 흥미로운 발표를 했습니다. 그는 소프트웨어의 진화를 세 단계로 구분했습니다.
Software 1.0: 우리가 수십 년간 해온 방식입니다. Python, Java, C++로 명시적인 로직을 작성합니다. if-else로 분기하고, for로 반복하고, 함수로 추상화합니다. 어떻게(How) 해야 하는지를 코드로 작성하는 시대입니다.
Software 2.0: 2010년대 딥러닝의 부상과 함께 시작됐습니다. 더 이상 규칙을 직접 작성하지 않습니다. 데이터를 모으고, 모델을 학습시키면, 신경망의 가중치가 곧 프로그램이 됩니다. Tesla Autopilot에서 수많은 C++ 코드가 신경망으로 대체된 것처럼요.
Software 3.0: 지금 우리가 진입하고 있는 시대입니다. LLM에게 자연어로 무엇을(What) 원하는지 말하면 됩니다. 프롬프트가 곧 프로그램입니다.

Karpathy는 이렇게 말합니다: "Software 3.0 is eating 1.0/2.0." 새로운 패러다임이 기존의 것을 집어삼키고 있다고요.
📺 Andrej Karpathy: Software Is Changing (Again) — Y Combinator AI Startup School 발표
Harness: LLM을 쓸모있게 만드는 것
그런데 현실은 좀 다릅니다.
ChatGPT에 "우리 서비스의 버그를 고쳐줘"라고 말한다고 마법처럼 해결되지 않습니다. LLM은 강력하지만, 혼자서는 파일을 읽을 수도, API를 호출할 수도, 데이터베이스에 접근할 수도 없습니다.
여기서 Harness의 개념이 등장합니다.
Harness는 원래 '마구(馬具)'를 뜻합니다. 말의 힘을 인간이 활용할 수 있게 해준 도구죠. 말이 아무리 빠르고 강해도, 마구 없이는 그 힘을 제어하거나 활용할 수 없었습니다.

LLM도 마찬가지입니다. 그 자체만으로는 제어하거나 활용하기 어렵습니다. LLM의 한계를 보완하고, 실제 업무에 연결해주는 도구와 환경이 필요합니다.
Claude Code도 Harness다
Claude Code는 Anthropic이 만든 CLI 기반 코딩 에이전트입니다. 그리고 이것은 본질적으로 Claude 모델을 위한 Harness라고 할 수 있습니다.
Claude Code가 제공하는 것들을 보면:
이 모든 것이 Claude라는 LLM 엔진을 실제로 일할 수 있는 에이전트로 만들어주는 Harness입니다. 그런데 이 구조, 어디서 많이 본 것 같지 않나요?
소프트웨어 1.0의 눈으로 바라보기
MCP, Skills, Sub-agent, Slash Command...
새로운 용어들이 쏟아지면 인지 부하가 생깁니다. 하지만 이 구조를 자세히 들여다보면, 우리가 오랜기간 사용해 온 레이어드 아키텍처와 놀라울 정도로 유사합니다.

각 레이어를 자세히 보면
Slash Command = Controller
Spring의 @RestController, Express의 router.get()처럼, Slash Command는 사용자 요청의 진입점입니다. /review를 치면 리뷰 워크플로우가 시작되고, /refactor를 치면 리팩토링 워크플로우가 시작됩니다.
# 사용자 입력
/review PR-1234
# 내부적으로
→ review 워크플로우 트리거
→ 적절한 Sub-agent와 Skill 조합 실행Sub-agent = Service Layer
Service 계층이 여러 Repository와 Domain 객체를 조율하듯이, Sub-agent는 여러 Skill을 조합하여 워크플로우를 완성합니다. 각 Sub-agent는 별도의 Context를 가지고 있어서, 마치 별도의 스레드처럼 독립적으로 동작해요.
Skills = Domain Component (SRP)
Skill은 단일 책임 원칙(SRP)을 따르는 기능 단위입니다. "코드 리뷰하기", "테스트 생성하기", "문서 작성하기"처럼 하나의 명확한 역할만 담당하죠. 클래스가 비대해지면 안 되는 것처럼, Skill도 한 가지 일만 잘해야 합니다.
MCP = Infrastructure / Adapter
MCP(Model Context Protocol)는 외부 시스템과의 연결을 담당합니다. 데이터베이스, API, 파일 시스템 등 외부 세계와의 접점이죠. Repository Pattern이나 Adapter Pattern처럼, 내부 로직이 외부 구현에 의존하지 않도록 추상화를 제공합니다.
CLAUDE.md = package.json
그리고 프로젝트의 설정을 담는 CLAUDE.md는 package.json이나 pom.xml과 같은 역할입니다. 프로젝트의 기술 스택, 코딩 컨벤션, 빌드 명령어 등 잘 변하지 않는 원칙을 담습니다.
# CLAUDE.md 예시
## 기술 스택
- TypeScript + React 18
- Node.js 20+
- pnpm
## 코딩 컨벤션
- 함수형 컴포넌트만 사용
- 테스트는 vitest로 작성
## 빌드 명령어
- `pnpm build` — 프로덕션 빌드
- `pnpm test` — 전체 테스트*중요한 점: CLAUDE.md를 자주 수정하고 있다면, 그 내용은 거기 있으면 안 되는 것일 가능성이 높습니다. 동적으로 변하는 정보(현재 작업 이슈, 오늘의 우선순위 등)는 대화로 전달하거나 Sub-agent의 Context로 넘겨 보세요.
안티패턴도 그대로 적용된다
레이어드 아키텍처의 안티패턴이 에이전트 설계에도 그대로 적용됩니다. 익숙한 이름들이죠.
curl 직접 호출, API 변경 시 전체 수정코드 스멜도 마찬가지예요.
결정적인 차이: 비유가 설명하지 못하는 것
레이어드 아키텍처 비유가 잘 맞지만, 한 가지 설명되지 않는 게 있습니다.
전통적인 서비스 레이어를 떠올려보세요. 주문 처리 중 재고가 부족하면? OutOfStockException을 던지거나, 정책에 따라 백오더 처리를 합니다. 결제 실패하면? 재시도하거나 에러를 반환합니다.
모든 분기가 미리 정의되어 있어야 합니다.
// 전통적인 서비스 레이어
public Order processOrder(OrderRequest request) {
if (inventory.check(request.getItemId()) < request.getQuantity()) {
throw new OutOfStockException(); // 정해진 예외
// 또는
return backOrderPolicy.apply(request); // 정해진 정책
}
// ...
}
그런데 실제 개발하다 보면 이런 순간이 있죠:
"이 케이스는... PM한테 물어봐야 하는데" "스펙에 없는 상황인데 어떻게 하지?"
전통 아키텍처에서는 이런 순간에 코드가 멈출 방법이 없습니다. 예외를 던지거나, 개발자가 임의로 결정하거나, 일단 로그 남기고 넘어가거나 하는 방법 밖에 없어요.
에이전트는 질문할 수 있다
에이전트는 다릅니다. Human-in-the-Loop(HITL)가 가능하죠.
Request → Agent → 작업 진행 중...
↓
🤔 불확실한 상황 발생
↓
"A와 B 중 어떤 걸 원하세요?"
↓
User Answer → "A로 해줘"
↓
계속 진행 → 완료UserAskQuestion 같은 도구를 통해, 에이전트는 실행 중간에 판단을 위임할 수 있어요.
Exception이 Question으로 바뀌는 겁니다.
언제 질문하고, 언제 알아서 할 것인가
HITL이 가능하다고 매번 물어보면 안 됩니다. 그건 그냥 귀찮은 도구예요.
질문해야 할 때:
알아서 해야 할 때:
좋은 에이전트는 "언제 질문할지"를 아는 에이전트입니다.
1.0 개발자가 3.0으로 가는 길
Software 3.0 시대가 왔다고 해서, 저희가 배운 것들이 쓸모없어지는 것은 아닙니다.
버릴 것
가져갈 것
도구는 바뀌었지만, 좋은 설계의 원칙(응집도, 결합도, 추상화)은 그대로입니다.
MCP를 설계할 때 Adapter Pattern을 떠올려 보세요. Skill을 만들 때 SRP를 떠올려 보세요. Sub-agent를 구성할 때 Service Layer를 떠올려 보세요.
여러분이 가진 아키텍처 지식이 곧 최고의 에이전트를 만드는 기반입니다.
한계점: 비유가 숨기는 것들
레이어드 아키텍처 비유가 이해에 도움이 되지만, 숨기는 차이점도 있습니다. 실전에서 주의해야 할 포인트들입니다.
토큰은 메모리다
전통적인 서버에서는 RAM을 걱정했습니다. 에이전트에서는 토큰을 걱정해야 합니다.
Context Window = 작업 메모리
토큰 사용량 = 메모리 점유율
CLAUDE.md, Skills, 대화 히스토리, MCP 응답... 이 모든 것이 Context Window에 쌓입니다. 200K 토큰이 많아 보여도, 대규모 코드베이스를 다루다 보면 순식간에 차오릅니다.
OOM을 예방하듯, 토큰 폭발도 미리 감지할 수 있습니다. CLAUDE.md에 "모든 테스트 파일을 분석하라"고 쓰기 전에, 테스트가 50개일 때 어떻게 될지 상상해보세요. 정확한 토큰 수를 계산할 필요는 없습니다. 파일 수와 라인 수만 대략 파악하면 충분합니다.
지침을 작성한 뒤, Claude에게 "이 워크플로우를 실행하면 어떤 파일들을 읽게 될 것 같아?"라고 물어보세요. 예상보다 많은 파일이 나온다면, 지침을 좁히거나 단계를 나눠야 한다는 신호입니다.
토큰을 아끼는 또 다른 방법은 결정적 로직을 scripts로 분리하는 것입니다.
# 안티패턴: LLM이 컨벤션을 매번 해석
"브랜치명은 feature/JIRA-{티켓번호}-{설명} 형식으로 만들어줘.
설명은 kebab-case로, 한글이면 영어로 변환하고..."
# 권장: scripts가 컨벤션을 캡슐화
./scripts/create-branch.sh JIRA-1234 "로그인 기능"
→ feature/JIRA-1234-login-featureLLM 입장에서는 스크립트를 실행하고 결과를 활용하면 끝입니다. 컨벤션을 이해할 필요도, 매번 토큰을 소비할 필요도 없죠. 판단이 필요 없는 작업은 도구로 만들어 제공해 보세요.
Skill 분리의 딜레마: 클래스 폭발과 디미터의 법칙
전통 아키텍처에서 SRP를 맹목적으로 따르다 보면 클래스 폭발(Class Explosion)이 발생합니다. 수백 개의 작은 클래스가 난립하고, 이들 간의 관계를 파악하는 것만으로도 인지 부하가 생기죠.
Skill도 마찬가지입니다. Claude는 시작 시 모든 Skill의 메타데이터(name/description)를 시스템 프롬프트에 로드합니다. Skill이 20개면 20개의 Description이 항상 Context를 점유합니다.
# 안티패턴: Skill 폭발
.claude/skills/
├── review-naming/
│ └── SKILL.md
├── review-types/
│ └── SKILL.md
├── review-complexity/
│ └── SKILL.md
├── review-security/
│ └── SKILL.md
└── ... (15개 더)이건 마치 이런 코드와 같습니다:
// 클래스 폭발 안티패턴
class NamingValidator { ... }
class TypeValidator { ... }
class ComplexityValidator { ... }
class SecurityValidator { ... }
// ... 15개 더
// 사용하는 쪽
new NamingValidator().validate(code);
new TypeValidator().validate(code);
// 매번 어떤 Validator를 써야 하는지 기억해야 함
```디미터의 법칙을 떠올려보세요. "친구의 친구에게 말하지 마라." 객체는 직접 관련된 객체만 알아야 합니다. Skill 설계에 적용하면: SKILL.md는 진입점만 제공하고, 세부 지식은 `references/`에 위임하라.
# 권장: Progressive Disclosure 구조
.claude/skills/
└── code-review/
├── SKILL.md # "코드 리뷰해줘" → 여기만 로드
├── references/ # 필요할 때만 로드
│ ├── naming-rules.md # "네이밍 컨벤션은?" → 그때 로드
│ ├── security-checklist.md
│ └── performance-guide.md
└── scripts/
└── lint-check.sh이건 Facade 패턴과 유사합니다:
// Facade 패턴: 진입점은 하나, 내부는 위임
class CodeReviewer {
private NamingRules namingRules; // 필요할 때 로드
private SecurityChecklist security; // 필요할 때 로드
public Review review(Code code) {
// 상황에 따라 필요한 것만 사용
if (needsNamingCheck) namingRules.check(code);
if (needsSecurityCheck) security.check(code);
}
}Claude도 이렇게 동작합니다. SKILL.md가 Facade 역할을 하고, references/ 안의 파일들은 Claude가 필요하다고 판단할 때만 Context에 로드됩니다.
균형점 찾기:
references/ 파일scripts/ 또는 MCP실전 팁: Setup & Config 패턴
이론은 충분합니다. 실제로 어떻게 적용할까요?
Slash Command를 활용하면 HITL과 자동화를 자연스럽게 조합할 수 있습니다. 익숙한 CLI 패턴과 비교해보면:
# 전통적인 CLI
npm init # 최초 구조 생성
npm config set # 이후 설정 변경
# 에이전트 명령어
/setup # 레포 분석 → 구조 생성
/config # 기존 설정 조정
HITL이 빛나는 순간은 setup 과정입니다:
/setup
→ 감지된 환경: TypeScript + React, pnpm
→ 테스트 프레임워크가 vitest와 jest 둘 다 있네요.
어떤 걸 기본으로 사용할까요? [vitest / jest]
> vitest
→ CLAUDE.md 생성 완료에이전트가 환경을 자동 감지하되, 애매한 부분은 질문합니다. 모든 걸 미리 정의할 필요 없이, 불확실한 순간에만 사용자 판단을 요청하는 거죠.
오픈소스 프로젝트인 claude-hud 플러그인이 이 패턴을 잘 보여줍니다:
# 1. 플러그인 설치
/plugin install claude-hud
# 2. 레포에 맞게 설정 — 여기가 setup!
/claude-hud:setup/claude-hud:setup이 하는 일:
핵심은 사용자의 수동 설정을 최소화하면서, 정말 필요한 순간에만 질문하는 것입니다.
마치며
Software 3.0 시대의 개발은, 코드를 작성하는 것에서 코드를 조립하고 지시하는 쪽으로 점차 무게중심이 옮겨가고 있습니다.
다만 그 조립의 원칙 자체는, 우리가 이미 익숙하게 다뤄온 개념들과 크게 다르지 않습니다.
Claude Code의 MCP, Skills, Sub-agent, Slash Command가 다소 낯설게 느껴진다면, 이를 익숙한 레이어드 아키텍처의 관점에서 바라보는 것도 하나의 방법입니다. 새로운 기술이라 하더라도 기존의 엔지니어링 원칙에 비춰 해석해 보면, 그 안에서 자연스럽게 설계 패턴이 드러나곤 합니다.
또 하나 기억해둘 점은, 애플리케이션이 이제 질문할 수 있는 존재가 되었다는 점입니다. 모든 것을 처음부터 완벽히 정의하려 하기보다는, 애매한 부분은 묻게 두는 접근도 고려해볼 수 있습니다.
Start building by refactoring your mindset.


