DRY: 지식의 단일 출처를 유지해 변경 비용을 줄이는 원칙
DRY(Don't Repeat Yourself)는 하나의 지식이 시스템 안에서 단 하나의 명확한 표현만 갖도록 하라는 원칙이다. 코드 줄이 비슷한지가 아니라 "같은 이유로 바뀌는 지식"이 중복되었는지를 기준으로 삼는다. 잘못 적용하면 오히려 결합도가 높아지므로 중복의 성격을 구분하는 기준이 필요하다.
By 송민성
1. 개념
DRY(Don't Repeat Yourself)는 『실용주의 프로그래머(The Pragmatic Programmer)』에서 제시된 원칙이다. 원문은 다음과 같다.
> 모든 지식은 시스템 안에서 단일하고, 모호하지 않으며, 권위 있는 표현(single, unambiguous, authoritative representation)을 가져야 한다.
핵심은 "코드"가 아니라 "지식(knowledge)"이다. 지식에는 비즈니스 규칙, 검증 조건, 상수, 스키마, API 계약, 설정값 등이 포함된다. 따라서 DRY 위반은 복사-붙여넣기된 코드만 뜻하지 않는다. 다음도 모두 해당한다.
- 코드와 주석·문서가 같은 내용을 따로 설명하는 경우
- DB 스키마와 애플리케이션 모델 타입을 각각 손으로 정의하는 경우
- 서버와 클라이언트가 같은 검증 규칙을 따로 구현하는 경우
반대로 모양이 비슷해도 서로 다른 이유로 바뀌는 코드는 DRY 위반이 아니다.
2. 왜 사용하는가
- 변경 비용 감소: 규칙이 바뀔 때 한 곳만 고치면 된다. 중복이 N곳이면 수정 지점도 N곳이고, 하나라도 놓치면 버그가 된다.
- 불일치(inconsistency) 방지: 같은 규칙의 복사본들이 시간이 지나며 서로 다르게 변형되는 것을 막는다.
- 이해 비용 감소: "이 규칙은 어디에 있는가"에 대한 답이 하나가 된다.
- 테스트 집중: 규칙이 한 곳에 있으면 그 지점만 테스트해도 전체가 보장된다.
3. 동작 원리
3.1 중복의 종류
| 종류 | 예 | 대응 |
|---|---|---|
| 강요된 중복(imposed) | 스키마와 타입을 둘 다 손으로 작성 | 코드 생성, 단일 스키마에서 파생 |
| 부주의한 중복(inadvertent) | 같은 규칙인 줄 모르고 각자 구현 | 공통 함수·모듈로 추출 |
| 성급한 중복(impatient) | 급해서 복사-붙여넣기 | 리팩터링으로 추출 |
| 개발자 간 중복(interdeveloper) | 팀원끼리 같은 유틸 재구현 | 공유 라이브러리, 코드 리뷰 |
3.2 판단 기준: 변경의 이유
두 코드를 합칠지 판단할 때는 "같은 이유로 함께 바뀌는가"를 묻는다.
- 함께 바뀐다 → 같은 지식이므로 하나로 합친다.
- 우연히 지금 같을 뿐이다 → 별개의 지식이므로 합치지 않는다.
이 기준은 단일 책임 원칙(SRP)의 "변경의 이유"와 맞닿아 있다.
3.3 추상화 시점
"세 번 반복되면 추출한다"는 3의 법칙(Rule of Three)이 흔한 경험칙이다. 두 번째까지는 공통점과 차이점을 충분히 알기 어렵기 때문에 세 번째 사례를 보고 추상화의 모양을 정하라는 뜻이다. 엄밀한 규칙이 아니라 휴리스틱이다.
4. 코드 예제
4.1 검증 규칙 중복 제거 (TypeScript)
// Before: 같은 규칙(비밀번호 정책)이 두 곳에 흩어져 있다.
function validateSignup(password: string): boolean {
return password.length >= 8 && /\d/.test(password);
}
function validatePasswordChange(password: string): boolean {
return password.length >= 8 && /\d/.test(password);
}
// After: 규칙의 단일 출처
const PASSWORD_MIN_LENGTH = 8;
export function isValidPassword(password: string): boolean {
return password.length >= PASSWORD_MIN_LENGTH && /\d/.test(password);
}
function validateSignupV2(password: string): boolean {
return isValidPassword(password);
}
function validatePasswordChangeV2(password: string): boolean {
return isValidPassword(password);
}
console.log(validateSignupV2("abc12345")); // true
console.log(validatePasswordChangeV2("short1")); // false4.2 스키마에서 타입 파생 (TypeScript)
타입 선언과 런타임 검증을 따로 쓰면 둘이 어긋날 수 있다. 하나의 정의에서 타입을 파생하면 단일 출처가 유지된다. 아래 예제는 외부 라이브러리 없이 구현했다.
const userFields = {
id: "number",
name: "string",
active: "boolean",
} as const;
type TypeMap = { number: number; string: string; boolean: boolean };
// 스키마에서 타입을 파생
type User = { -readonly [K in keyof typeof userFields]: TypeMap[(typeof userFields)[K]] };
// 같은 스키마로 런타임 검증
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const obj = value as Record<string, unknown>;
return Object.entries(userFields).every(
([key, type]) => typeof obj[key] === type
);
}
const u: unknown = { id: 1, name: "Kim", active: true };
console.log(isUser(u)); // true
console.log(isUser({ id: "1", name: "Kim", active: true })); // false4.3 잘못된 DRY: 우연한 중복의 병합 (TypeScript)
// 지금은 둘 다 "10% 할인"이라 같아 보인다.
function memberDiscount(price: number): number {
return price * 0.9;
}
function couponDiscount(price: number): number {
return price * 0.9;
}
// 이를 applyTenPercent 하나로 합치면, 회원 등급 정책이 바뀔 때
// 쿠폰 로직까지 영향을 받는다. 두 규칙은 변경 이유가 다르다.
console.log(memberDiscount(1000), couponDiscount(1000)); // 900 9004.4 DB 제약의 단일 출처 (SQL, PostgreSQL)
CREATE DOMAIN email_address AS text
CHECK (VALUE ~* '^[^@\s]+@[^@\s]+\.[^@\s]+$');
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email email_address NOT NULL UNIQUE
);
CREATE TABLE newsletter_subscribers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email email_address NOT NULL
);이메일 형식 규칙이 DOMAIN 하나에 있으므로 두 테이블이 같은 제약을 공유한다.
5. 시간 복잡도 또는 성능 특성
DRY는 알고리즘이 아니라 설계 원칙이므로 고유한 시간복잡도가 없다. 대신 다음 특성이 있다.
- 런타임 성능: 공통 함수 호출은 대부분 무시할 수준의 비용이다. 다만 아주 뜨거운 경로(hot path)에서 과도한 추상화 계층이 생기면 측정이 필요하다.
- 유지보수 비용: 규칙 변경 시 수정 지점이 N곳에서 1곳으로 줄어든다. 이는 코드 변경 비용에 대한 이야기이며 실행 속도와는 별개다.
- 결합도 비용: 공통 모듈을 쓰는 곳이 늘수록 그 모듈을 바꿀 때 영향 범위도 커진다.
6. 실무 사용 사례
- API 계약 공유: OpenAPI 명세 하나에서 서버 라우트 검증, 클라이언트 SDK, 문서를 생성한다.
- DB 스키마와 타입: 스키마나 마이그레이션에서 ORM 모델·타입을 생성한다.
- 설정과 상수: 환경별로 달라지는 값을 한 설정 파일에 모으고 코드에서는 참조만 한다.
- UI 컴포넌트: 버튼, 입력창 같은 반복 요소를 디자인 시스템 컴포넌트로 통합한다.
- CI 파이프라인: 여러 저장소가 쓰는 빌드 단계를 재사용 가능한 워크플로로 분리한다.
7. 주의할 점
- 잘못된 추상화의 비용: 잘못 합친 추상화는 중복보다 해롭다는 지적이 널리 공유되어 있다. 합친 뒤 매개변수와 분기(
if (flag))가 계속 늘어난다면 병합이 잘못되었다는 신호다. - 우연한 중복(accidental duplication): 지금 코드가 같아도 변경 이유가 다르면 그대로 둔다.
- 서비스 경계: 마이크로서비스 사이에서 공유 라이브러리로 중복을 없애면 배포 결합이 생긴다. 이 경우 약간의 중복이나 계약 기반 생성이 더 나을 수 있다.
- 테스트 코드: 테스트는 읽기 쉬운 것이 우선이다. 과도하게 공통화하면 각 테스트가 무엇을 검증하는지 파악하기 어려워진다.
- DRY와 가독성의 균형: 중복 제거가 코드를 이해하기 어렵게 만든다면 이득이 없다.
8. 핵심 정리
- DRY의 대상은 코드 모양이 아니라 지식이다.
- 중복 판단 기준은 "같은 이유로 함께 바뀌는가"이다.
- 강요된 중복은 코드 생성이나 단일 스키마로, 부주의한 중복은 공통 모듈 추출로 해결한다.
- 3의 법칙은 성급한 추상화를 피하기 위한 휴리스틱이다.
- 잘못된 추상화는 중복보다 비용이 크므로, 합치기 전에 변경 이유를 먼저 확인한다.