YAGNI: 지금 필요하지 않은 기능은 만들지 않는다는 설계 원칙
YAGNI(You Aren't Gonna Need It)는 현재 요구사항에 필요하지 않은 기능이나 추상화를 미리 구현하지 말라는 소프트웨어 공학 원칙이다. 익스트림 프로그래밍(XP)에서 유래했으며, 추측에 기반한 설계가 만드는 비용을 줄이는 데 목적이 있다. 이 글에서는 원리와 비용 구조, 코드 리팩터링 예제, 실무 적용 기준과 오용 사례를 다룬다.
By 송민성
1. 개념
YAGNI(You Aren't Gonna Need It, 필요하지 않을 것이다)는 "실제로 필요해지기 전까지는 기능을 구현하지 않는다"는 원칙이다. 익스트림 프로그래밍(XP, eXtreme Programming)의 실천 방법 중 하나로 알려져 있으며, Ron Jeffries 등이 대중화했다.
적용 대상은 기능뿐만이 아니다. 다음과 같은 "미래를 위한 준비"가 모두 포함된다.
- 아직 쓰이지 않는 설정 옵션, 파라미터, 필드
- 구현체가 하나뿐인 인터페이스와 추상 클래스
- 사용처가 없는 확장 포인트(플러그인 구조, 훅)
- "나중에 필요할지 모르는" 범용 라이브러리화
핵심은 현재 요구사항을 만족하는 가장 단순한 구현을 선택하는 것이다. 단순한 구현은 나중에 필요가 생겼을 때 바꾸기 쉬운 상태로 남겨 둔다.
2. 왜 사용하는가
미리 만든 기능은 공짜가 아니다. Martin Fowler는 추측성 기능이 만드는 비용을 다음과 같이 정리한다.
- 구축 비용(cost of build): 쓰지 않을 기능을 만드는 데 든 시간
- 지연 비용(cost of delay): 그 시간에 만들 수 있었던 실제 필요 기능이 늦어지는 손실
- 운반 비용(cost of carry): 코드가 늘어나 이해, 테스트, 수정이 어려워지는 부담
- 수리 비용(cost of repair): 예측이 틀렸을 때 잘못된 추상화를 고치는 비용
요구사항 예측은 자주 틀린다. 틀린 예측 위에 쌓은 추상화는 올바른 설계를 오히려 방해한다. 또한 코드량이 적을수록 읽고, 테스트하고, 배포하기 쉽다.
3. 동작 원리
YAGNI는 단독으로 작동하지 않고, 변경 비용을 낮추는 실천 방법들이 받쳐 주어야 성립한다.
- 자동화된 테스트: 나중에 구조를 바꿔도 동작이 유지되는지 검증한다.
- 리팩터링(refactoring): 필요가 생긴 시점에 구조를 일반화한다.
- 지속적 통합(CI): 작은 변경을 자주 통합해 위험을 줄인다.
의사결정 흐름은 단순하다.
- 지금 요구사항에 이 코드가 필요한가? → 아니면 만들지 않는다.
- 필요가 실제로 생겼는가? → 그때 가장 단순한 형태로 구현한다.
- 같은 패턴이 반복되는가? → 그때 추상화한다. (흔히 "세 번 반복되면 추상화" 규칙으로 쓰이지만 엄밀한 법칙은 아닌 경험칙이다.)
4. 코드 예제
알림 전송 기능을 예로 든다. 현재 요구사항은 "이메일 알림 발송"뿐이다.
과잉 설계 (YAGNI 위반)
interface Channel {
send(to: string, message: string): Promise<void>;
}
class ChannelFactory {
private registry = new Map<string, () => Channel>();
register(name: string, creator: () => Channel): void {
this.registry.set(name, creator);
}
create(name: string): Channel {
const creator = this.registry.get(name);
if (!creator) throw new Error(`unknown channel: ${name}`);
return creator();
}
}
class EmailChannel implements Channel {
async send(to: string, message: string): Promise<void> {
console.log(`[email] ${to}: ${message}`);
}
}
const factory = new ChannelFactory();
factory.register("email", () => new EmailChannel());
async function notify(to: string, message: string): Promise<void> {
await factory.create("email").send(to, message);
}
notify("user@example.com", "가입을 환영합니다").catch(console.error);SMS나 푸시는 요구사항에 없는데도 팩토리, 레지스트리, 인터페이스가 먼저 생겼다.
YAGNI 적용
async function sendEmail(to: string, message: string): Promise<void> {
console.log(`[email] ${to}: ${message}`);
}
async function notify(to: string, message: string): Promise<void> {
await sendEmail(to, message);
}
notify("user@example.com", "가입을 환영합니다").catch(console.error);실제로 SMS 요구사항이 생겼을 때 그 시점에 일반화한다. 테스트가 있다면 notify의 호출부는 바뀌지 않는다.
type Channel = "email" | "sms";
async function sendEmail(to: string, message: string): Promise<void> {
console.log(`[email] ${to}: ${message}`);
}
async function sendSms(to: string, message: string): Promise<void> {
console.log(`[sms] ${to}: ${message}`);
}
const senders: Record<Channel, (to: string, message: string) => Promise<void>> = {
email: sendEmail,
sms: sendSms,
};
async function notify(
to: string,
message: string,
channel: Channel = "email",
): Promise<void> {
await senders[channel](to, message);
}
notify("010-0000-0000", "인증번호 123456", "sms").catch(console.error);두 번째 구현이 실제로 생긴 뒤에야 추상화가 등장했고, 그 형태도 요구에 맞는 최소한의 것이다.
5. 시간 복잡도 또는 성능 특성
YAGNI는 알고리즘이 아니므로 시간복잡도 대신 비용 특성으로 본다.
| 항목 | 효과 |
|---|---|
| 코드 양 | 감소: 읽어야 할 코드, 테스트 대상이 줄어든다 |
| 간접 계층(indirection) | 감소: 호출 경로 추적이 쉬워진다 |
| 초기 개발 속도 | 증가: 필요한 기능에 집중한다 |
| 변경 시점 비용 | 일부 증가: 필요해질 때 리팩터링 작업이 생긴다 |
불필요한 추상화는 런타임 성능보다 인지 부하(cognitive load)와 유지보수 비용에 더 큰 영향을 준다.
6. 실무 사용 사례
- MVP 개발: 검증되지 않은 가설에 대해 최소 기능만 구현하고 사용자 반응을 본다.
- 코드 리뷰: "이 파라미터는 누가 쓰나요?", "구현체가 하나뿐인데 인터페이스가 필요한가요?"라는 질문으로 적용한다.
- API 설계: 사용처가 없는 필드와 엔드포인트를 노출하지 않는다. 공개한 API는 제거가 어렵기 때문이다.
- DB 스키마: 쓰이지 않는 컬럼과 테이블을 "혹시 몰라서" 추가하지 않는다. 컬럼 추가는 마이그레이션으로 나중에도 가능하다.
- 마이크로서비스 분리: 규모나 팀 요구가 생기기 전에 서비스를 쪼개지 않고 모듈형 모놀리스로 시작한다.
7. 주의할 점
- 품질 요소를 생략하는 것이 아니다. 테스트, 가독성, 에러 처리, 보안은 현재 기능의 일부이므로 YAGNI 대상이 아니다.
- 변경이 비싼 결정은 예외다. 공개 API 형태, DB 키 설계, 데이터 포맷, 보안 모델처럼 나중에 바꾸기 어려운 부분은 미리 신중히 설계할 가치가 있다. YAGNI는 변경 비용이 낮다는 전제에서 성립한다.
- 테스트 없이 적용하면 위험하다. 리팩터링 안전망이 없으면 "나중에 고치기"가 실제로는 고치지 못하는 상태가 된다.
- 단순함과 임시방편은 다르다. 중복을 방치하거나 구조를 무시하는 것은 YAGNI가 아니라 기술 부채다.
- 현재 요구에 이미 명시된 것은 구현 대상이다. 확정된 로드맵이나 계약된 요구사항은 "추측"이 아니다.
8. 핵심 정리
- YAGNI는 현재 필요하지 않은 기능, 추상화, 확장 포인트를 미리 만들지 않는 원칙이다.
- 추측성 구현은 구축, 지연, 운반, 수리 비용을 발생시키고 예측은 자주 틀린다.
- 테스트와 리팩터링 실천이 있어야 "필요할 때 바꾼다"는 전략이 안전하다.
- 두 번째 사용처가 실제로 생겼을 때 일반화하는 것이 안전한 기본 전략이다.
- 변경 비용이 큰 결정(공개 API, 데이터 모델, 보안)은 예외로 신중히 다룬다.
- 품질을 위한 작업(테스트, 가독성)은 YAGNI 때문에 생략하지 않는다.