본문으로 건너뛰기

CUPID 프로그래밍

개념

Dan North가 제안한 Composable, Unix philosophy, Predictable, Idiomatic, Domain-based 속성의 개념과 적용 방법

정확한 명칭은 **CUPID 원칙(CUPID principles)**보다 **CUPID 속성(CUPID properties)**이다. CUPID는 준수 여부를 판정하는 규칙이나 완결된 개발 방법론이 아니라, 코드베이스를 평가하고 더 나은 방향으로 점진적으로 이동하기 위한 다섯 가지 설계 속성이다.12

개요

CUPID는 Daniel Terhorst-North(Dan North)가 제안한 소프트웨어 설계 관점이다. 2021년 배경 글과 강연에서 처음 구체화되었고, 2022년 원문 **“CUPID: for joyful coding”**에서 전체 설명이 공개되었다.31

CUPID가 지향하는 것은 저자가 말하는 joyful code, 즉 다음 개발자가 쉽게 탐색하고, 이해하고, 추론하고, 자신 있게 변경할 수 있는 코드다. 여기서 “즐거움”은 단순한 취향이 아니라 코드 사용자의 작업 경험을 가리킨다.1

글자 속성 요약 원문이 제시한 주요 관찰점
C Composable 다른 코드와 잘 조합된다 작은 공개 표면, 의도를 드러내는 이름, 최소한의 의존성
U Unix philosophy 한 가지 목적을 잘 수행한다 단순하고 일관된 모델, 조합 가능한 입출력, 단일 목적
P Predictable 기대한 대로 동작한다 예상 가능한 행위, 결정성, 관찰 가능성
I Idiomatic 언어와 코드베이스 안에서 자연스럽다 언어 생태계의 관용구, 팀·프로젝트의 지역 관용구
D Domain-based 문제 도메인을 언어와 구조에 반영한다 도메인 언어, 도메인 중심 구조, 도메인 경계

CUPID는 특정 프로그래밍 언어, 객체지향 패러다임, 프레임워크 또는 배포 방식에 한정되지 않는다. 애플리케이션, 서비스, 라이브러리, 프레임워크와 코드베이스 전반에 적용할 수 있는 평가 렌즈로 제안되었다.14

원칙보다 속성

Dan North는 원칙을 경계가 있는 규칙으로, 속성을 이동할 방향이 있는 목표로 구분한다. 원칙 중심 접근에서는 코드가 규칙을 “지켰는가”가 중심이 되기 쉽지만, 속성 중심 접근에서는 현재 코드가 목표에 얼마나 가까운지와 다음에 어떤 작은 개선을 할지가 중심이 된다.2

CUPID 속성 자체도 다음 세 조건을 갖도록 설계되었다.

  • Practical: 설명하기 쉽고, 평가하기 쉽고, 작은 단위로 도입하기 쉬워야 한다.
  • Human: 코드 자체의 추상적 성질보다 코드를 읽고 사용하는 사람의 경험을 다뤄야 한다.
  • Layered: 초보자에게는 단순한 지침을, 숙련자에게는 더 깊은 설계 논의를 제공해야 한다.

따라서 CUPID에는 합격선이나 완성 상태가 없다. 한 속성을 개선하면 다른 속성도 함께 좋아지는 경우가 많지만, 모든 결정은 문맥과 트레이드오프를 고려해야 한다.12

다섯 가지 속성

C — Composable

Composable은 코드가 다른 코드와 쉽게 연결되고 재사용될 수 있는 성질이다. 단순히 함수를 잘게 나누는 것이 아니라, 사용자가 적은 지식으로도 구성 요소를 발견하고 선택하고 결합할 수 있어야 한다.5

원문은 세 가지 휴리스틱을 제시한다.

  1. 작은 공개 표면(small surface area): 배워야 할 API와 잘못 사용할 수 있는 경우의 수를 줄인다.
  2. 의도를 드러내는 이름과 목적(intention-revealing): 구성 요소를 쉽게 찾고 적합성을 빠르게 판단하게 한다.
  3. 최소한의 의존성(minimal dependencies): 버전 충돌, 환경 가정, 전이 의존성으로 인한 부담을 줄인다.

다만 “작을수록 항상 좋다”는 뜻은 아니다. API를 지나치게 파편화하면 올바른 조합 순서가 암묵지로 남아 오히려 사용하기 어려워진다. CUPID는 비대함과 파편화 사이에서 응집도 높은 적정 크기를 찾도록 요구한다.5

interface User {
  readonly status: "active" | "suspended";
  readonly roles: readonly Role[];
}

type Role = "reviewer" | "approver";
type Rule<T> = (value: T) => boolean;

const isActive: Rule<User> = (user) => user.status === "active";

const hasRole = (role: Role): Rule<User> =>
  (user) => user.roles.includes(role);

const all = <T>(...rules: readonly Rule<T>[]): Rule<T> =>
  (value) => rules.every((rule) => rule(value));

export const canApprove = all(isActive, hasRole("approver"));

이 예시는 모든 규칙이 동일한 입력·출력 계약을 사용하므로 쉽게 조합된다. 공개 API가 작고, 전역 상태나 외부 라이브러리에 의존하지 않으며, 이름이 의도를 드러낸다.

검토할 질문은 다음과 같다.

  • 이 모듈을 사용하려면 얼마나 많은 타입, 옵션, 초기화 순서를 알아야 하는가?
  • 입출력 계약이 다른 구성 요소와 자연스럽게 연결되는가?
  • 프레임워크 전체를 기동하지 않고 핵심 기능을 사용할 수 있는가?
  • 의존성 수가 아니라 **필요한 능력(capability)**만 요구하고 있는가?
  • 공개 API가 지나치게 넓거나 지나치게 잘게 쪼개져 있지 않은가?

U — Unix philosophy

CUPID의 Unix philosophy는 “한 가지를 잘 수행하는 구성 요소”와 “서로 연결할 수 있는 단순한 계약”을 지향한다. Unix 파이프처럼 한 구성 요소의 출력이 다음 구성 요소의 입력이 될 수 있어야 한다.6

이 생각은 Doug McIlroy가 정리한 Unix 설계 문화와 연결된다. 1978년 Bell System Technical Journal의 Unix 특집에는 각 프로그램이 한 가지를 잘 수행하고, 출력이 다른 프로그램의 입력이 될 수 있도록 설계하라는 취지의 격언이 기록되어 있다.7

CUPID의 “단일 목적”은 SOLID의 Single Responsibility Principle과 완전히 같은 개념이 아니다. Dan North의 구분에 따르면 다음과 같다.6

  • 단일 목적은 외부에서 보았을 때 구성 요소가 무엇을 제공하는지에 관한 outside-in 관점이다.
  • 단일 책임은 내부 코드를 어떤 변경 이유와 책임으로 나눌지에 관한 inside-out 관점이다.

따라서 한 모듈 안에 계산, 검증, 표현 같은 여러 구현 단계가 있어도 외부에 하나의 응집된 목적을 제공한다면 Unix 속성에 가까울 수 있다. 반대로 함수가 매우 짧아도 사용자가 여러 함수를 특정 순서로 조립해야만 의미 있는 일을 할 수 있다면 단일 목적이 불명확할 수 있다.

interface RawOrderLine {
  readonly productId: string;
  readonly unitPrice: number;
  readonly quantity: number;
}

interface RawOrder {
  readonly lines: readonly RawOrderLine[];
}

interface ValidatedOrder {
  readonly lines: readonly RawOrderLine[];
}

interface PricedOrder {
  readonly total: number;
}

interface OrderViewModel {
  readonly totalLabel: string;
}

const validateOrder = (raw: RawOrder): ValidatedOrder => {
  if (raw.lines.some((line) => line.unitPrice < 0 || line.quantity <= 0)) {
    throw new RangeError("주문 항목의 가격과 수량을 확인해야 합니다.");
  }

  return raw;
};

const priceOrder = (order: ValidatedOrder): PricedOrder => ({
  total: order.lines.reduce(
    (sum, line) => sum + line.unitPrice * line.quantity,
    0,
  ),
});

const presentOrder = (order: PricedOrder): OrderViewModel => ({
  totalLabel: `${order.total.toLocaleString("ko-KR")}원`,
});

export const buildOrderSummary = (raw: RawOrder): OrderViewModel =>
  presentOrder(priceOrder(validateOrder(raw)));

각 함수는 하나의 명확한 변환을 수행하고, 변환 결과가 다음 단계의 입력으로 이어진다. buildOrderSummary는 내부 단계를 감추면서 외부에는 “주문 요약 생성”이라는 하나의 완결된 목적을 제공한다.

검토할 질문은 다음과 같다.

  • 이 구성 요소의 목적을 한 문장으로 설명할 수 있는가?
  • 입력과 출력이 명확하며 다른 구성 요소가 소비하기 쉬운가?
  • 서로 독립적인 목적이 한 API에 누적되고 있지 않은가?
  • 반대로 관련 동작을 과도하게 분리해 사용 순서가 암묵지가 되지 않았는가?
  • 구현 조직보다 사용자의 관점에서 목적이 분명한가?

P — Predictable

Predictable은 코드가 이름과 구조가 암시하는 대로, 일관되고 신뢰성 있게 동작하며, 그 사실을 쉽게 확인할 수 있는 성질이다. 원문은 예측 가능성을 테스트 가능성보다 넓은 개념으로 설명한다.8

세 가지 핵심 차원이 있다.

  1. 기대한 대로 동작한다: 이름, 타입, 구조와 실제 결과 사이에 불쾌한 놀라움이 없다.
  2. 결정적이다(deterministic): 동일한 조건에서는 동일한 결과 또는 명확한 운영 범위를 보인다.
  3. 관찰 가능하다(observable): 출력과 텔레메트리를 통해 내부 상태와 실행 과정을 추론할 수 있다.

결정성은 무작위성이나 네트워크 사용을 금지한다는 뜻이 아니다. 시간, 난수, 환경 변수, 외부 서비스와 같은 변동 요인을 명시적 입력이나 경계로 드러내고, 실패·지연·자원 사용의 범위를 이해할 수 있게 해야 한다.

type ReservationStatus =
  | { readonly kind: "active"; readonly remainingMs: number }
  | { readonly kind: "expired" };

export function getReservationStatus(
  expiresAtMs: number,
  nowMs: number,
): ReservationStatus {
  const remainingMs = expiresAtMs - nowMs;

  return remainingMs > 0
    ? { kind: "active", remainingMs }
    : { kind: "expired" };
}

Date.now()를 함수 내부에서 직접 호출하지 않고 현재 시각을 입력으로 받으면 테스트와 재현이 쉬워진다. 반환값도 null, 예외, 특수 숫자 대신 구별 가능한 상태로 표현되어 호출자가 가능한 결과를 확인할 수 있다.

관찰 가능성은 로그를 많이 남기는 것과 동일하지 않다. 시스템이 실행 상태를 설명하는 적절한 신호를 내보내도록 설계하는 것이다. OpenTelemetry는 대표적인 소프트웨어 관찰 가능성 신호로 traces, metrics, logs 등을 다룬다.910

검토할 질문은 다음과 같다.

  • 이름, 타입, 문서와 실제 부작용이 일치하는가?
  • 시간, 난수, 네트워크, 저장소, 전역 상태가 숨겨져 있지 않은가?
  • 실패가 예외, 오류 값, 재시도 중 어떤 계약으로 전달되는지 명확한가?
  • 경계 조건, 자원 한계, 타임아웃과 재시도 정책을 설명할 수 있는가?
  • 운영 중 “무슨 일이 일어났는가”를 로그·메트릭·트레이스로 확인할 수 있는가?

기존 코드의 실제 동작이 불명확할 때는 Michael Feathers가 설명한 characterization test로 현재 동작을 기록한 뒤 개선하는 방법이 유용하다. 이 테스트의 목적은 바라는 동작이 아니라 시스템이 현재 보이는 동작을 문서화하는 데 있다.11

I — Idiomatic

Idiomatic은 해당 언어, 생태계, 도구 체인과 코드베이스에서 자연스럽게 읽히는 성질이다. 익숙한 관용구를 사용하면 독자가 스타일을 해독하는 데 쓰는 불필요한 인지 부하를 줄이고 문제 자체에 집중할 수 있다.12

두 층위가 있다.

  • 언어 관용구(language idioms): 언어의 표준 기능, 일반적인 타입 모델링, 라이브러리와 도구 사용 방식.
  • 지역 관용구(local idioms): 팀의 네이밍, 오류 처리, 디렉터리 구조, 테스트 스타일, 포맷터·린터와 도구 체인.

JavaScript와 TypeScript는 여러 패러다임을 허용하므로 같은 일을 다양한 방식으로 표현할 수 있다. 이 유연성은 장점이지만, 코드베이스 안에서 방식이 계속 바뀌면 인지 부하가 커진다. 팀은 공식 문서와 생태계 관례를 학습한 뒤, 합의가 없는 부분은 자동화 가능한 로컬 규칙으로 고정하는 편이 낫다.12

interface User {
  readonly name: string;
}

type LoadState<T> =
  | { readonly status: "idle" }
  | { readonly status: "loading" }
  | { readonly status: "success"; readonly data: T }
  | { readonly status: "failure"; readonly error: Error };

export function renderUserState(state: LoadState<User>): string {
  switch (state.status) {
    case "idle":
      return "대기 중";
    case "loading":
      return "불러오는 중";
    case "success":
      return state.data.name;
    case "failure":
      return state.error.message;
  }
}

TypeScript의 판별 유니온(discriminated union)은 네트워크 메시지나 상태 머신처럼 가능한 상태가 유한한 모델을 표현할 때 유용하다. 공식 핸드북도 판별 프로퍼티를 이용한 narrowing을 설명한다.13

Idiomatic은 “최신 문법을 많이 쓰는 코드”를 뜻하지 않는다. 독자가 이미 알고 있을 가능성이 높은 표현, 프로젝트 전반에서 일관된 표현, 자동 도구가 강제할 수 있는 표현을 우선하는 것이다.

검토할 질문은 다음과 같다.

  • 이 언어의 숙련자가 별도 해설 없이 코드를 자연스럽게 읽을 수 있는가?
  • 같은 문제를 코드베이스 안에서 여러 패러다임으로 제각각 풀고 있지 않은가?
  • 포매팅, import 순서, 네이밍 같은 기계적 논쟁을 도구로 제거했는가?
  • 오류, 비동기 처리, 상태 모델링에 일관된 관용구가 있는가?
  • 중요한 로컬 관례와 예외를 ADR 또는 개발 문서에 기록했는가?

D — Domain-based

Domain-based는 코드가 문제 도메인의 언어, 구조와 경계를 반영하는 성질이다. 목표는 요구 사항과 구현 사이의 인지적 거리를 줄이는 것이다.14

세 가지 차원이 있다.

  1. 도메인 언어: string, number, Map 같은 기술 타입만 노출하지 않고 업무 개념을 타입과 연산으로 표현한다.
  2. 도메인 구조: 최상위 디렉터리와 모듈 구성이 프레임워크의 기술 분류보다 제품 기능과 사용 사례를 먼저 보여 준다.
  3. 도메인 경계: 모듈, 소유권, 배포 단위가 의미 있는 업무 경계와 가능한 한 정렬된다.
type Currency = "KRW" | "USD";

interface Money {
  readonly amountMinor: bigint;
  readonly currency: Currency;
}

interface OrderLine {
  readonly productId: string;
  readonly unitPrice: Money;
  readonly quantity: number;
}

금액을 단순 number로 전달하는 대신 통화와 최소 화폐 단위를 묶은 Money를 사용하면 코드가 도메인 개념을 직접 말하게 된다. 유효성 검사와 연산 규칙도 이 개념 주변에 둘 수 있다. 이는 Domain-Driven Design의 Ubiquitous Language와도 맞닿아 있지만, CUPID의 Domain-based 속성이 DDD 전체 전술 패턴을 의무화하는 것은 아니다.1516

기술 유형을 최상위 구조로 삼은 예시는 다음과 같다.

src/
├─ components/
├─ controllers/
├─ hooks/
├─ models/
├─ repositories/
└─ services/

도메인을 최상위 구조로 드러낸 예시는 다음과 같다.

src/
├─ checkout/
│  ├─ domain/
│  ├─ application/
│  ├─ adapters/
│  └─ ui/
├─ catalog/
├─ customer-account/
└─ order-history/

두 번째 구조에서도 내부에 UI, 저장소, 어댑터가 필요할 수 있다. 차이는 제품의 주요 기능이 최상위 탐색 구조에 먼저 드러난다는 점이다. 도메인은 하위 도메인을 포함할 수 있으므로 모든 코드를 평평한 feature 폴더로 만들 필요는 없다.14

검토할 질문은 다음과 같다.

  • 타입과 함수 이름이 실제 업무 용어를 사용하는가?
  • 개발자와 도메인 전문가가 같은 단어를 같은 의미로 사용하는가?
  • 최상위 디렉터리만 보고 제품이 제공하는 기능을 짐작할 수 있는가?
  • 한 기능 변경에 필요한 파일이 기술 계층별 폴더에 과도하게 흩어져 있지 않은가?
  • 모듈·팀·배포 경계가 업무 경계와 충돌하고 있지 않은가?

속성 간 상호작용과 트레이드오프

CUPID의 다섯 속성은 서로 독립적인 체크박스가 아니다.

상호작용 효과
Composable + Unix 명확한 단일 목적과 안정적인 입출력 계약이 결합되어 더 큰 흐름을 쉽게 만든다.
Composable + Domain-based 도메인 타입이 모듈 사이의 의미 있는 연결 계약이 된다.
Predictable + Idiomatic 익숙한 언어 표현과 명시적 결과 모델이 놀라움을 줄인다.
Predictable + Composable 순수한 변환, 명시적 의존성, 작은 계약은 재현과 테스트를 쉽게 한다.
Idiomatic + Domain-based 언어의 자연스러운 표현으로 업무 개념을 모델링하면 읽기와 탐색이 쉬워진다.

동시에 다음 트레이드오프를 관리해야 한다.

  • API를 줄이다가 필수 설정과 호출 순서를 숨기면 Composable이 오히려 약해진다.
  • 의존성을 무조건 제거하면 검증된 라이브러리를 재구현하는 비용과 위험이 생긴다.
  • 결정성을 높이기 위한 의존성 주입이 지나치면 공개 표면과 설정 복잡도가 커질 수 있다.
  • 언어 관용구와 기존 프로젝트 관용구가 충돌할 수 있다. 대규모 일괄 변경보다 경계를 정해 점진적으로 통일해야 한다.
  • 도메인별 구조가 프레임워크 도구, 빌드 시스템 또는 조직 소유권과 충돌할 수 있다.
  • 관찰 가능성을 이유로 도메인 코드 전체에 특정 벤더 SDK를 퍼뜨리면 Composable과 Domain-based가 약해질 수 있다. 좁은 어댑터 경계가 필요하다.

CUPID와 SOLID

CUPID는 Dan North가 SOLID를 비판하고 현대적 대안을 고민하는 과정에서 시작되었다.3 그러나 둘은 같은 종류의 체계가 아니며, 각 글자를 일대일로 대응시키는 공식 매핑도 없다.

관점 CUPID SOLID
형식 코드가 가까워질 방향을 나타내는 속성 설계 판단을 안내하는 원칙
핵심 질문 “이 코드는 함께 일하기 좋은가?” “책임과 의존성을 어떻게 구조화할 것인가?”
적용 범위 API, 언어 관용구, 런타임 운영, 도메인 구조까지 폭넓음 주로 모듈·타입·객체 관계와 변경 용이성에 초점
평가 방식 더 가깝다/멀다, 문맥과 트레이드오프 중심 원칙을 적용하거나 위반했는지 논의하기 쉬움
대표 관점 사람의 사용 경험과 결과 속성 내부 설계 구조와 의존성 관리

실무 해석: CUPID를 도입하기 위해 SOLID를 폐기할 필요는 없다. SOLID가 유용한 구체적 설계 문맥에서는 사용하되, 그 결과가 Composable, Predictable, Domain-based한지 CUPID로 다시 평가할 수 있다. 반대로 SOLID를 형식적으로 지켰더라도 탐색이 어렵고 운영 중 설명할 수 없는 코드라면 CUPID 관점에서는 개선 여지가 크다.

Thoughtworks는 2022년 3월 Technology Radar에서 CUPID를 Assess 단계, 즉 조직에서 영향을 이해하기 위해 탐색할 가치가 있는 기법으로 분류했다. 이는 산업계의 독립적인 관심 신호이지만, 보편적 표준이나 실증적 검증을 의미하지는 않는다.17

TypeScript·프런트엔드 적용

프런트엔드 코드에서는 다음과 같이 번역할 수 있다.

속성 프런트엔드 적용 예시
Composable props와 반환 계약을 작게 유지하고, 프레임워크 전역 상태 없이 핵심 로직을 사용할 수 있게 한다.
Unix 컴포넌트와 훅의 목적을 사용자 행동 또는 기능 단위로 명확히 한다. 단순히 파일 크기로 분리하지 않는다.
Predictable 로딩·성공·실패 상태를 명시적으로 모델링하고, 브라우저 API·시간·스토리지를 경계 뒤에 둔다.
Idiomatic TypeScript narrowing, 표준 Promise 흐름, 팀의 React 상태 관리 관례와 도구 체인을 일관되게 쓴다.
Domain-based components/, hooks/보다 checkout/, account/, catalog/ 같은 기능을 최상위 탐색 구조로 둔다.

예를 들어 useCheckout()이 라우팅, 로컬 스토리지, 결제 SDK, 분석 이벤트, 서버 상태와 UI 상태를 모두 숨긴 채 거대한 객체를 반환한다면 사용하기는 편해 보여도 Composable과 Predictable이 약할 수 있다. 핵심 가격 계산은 순수한 도메인 함수로 분리하고, 브라우저와 외부 서비스는 작은 어댑터로 노출하며, 화면 상태는 판별 유니온으로 표현하는 방식이 CUPID에 더 가깝다.

종합 TypeScript 예시

다음 코드는 다섯 속성을 한 작은 기능에 적용한 편집 예시다. 공식 CUPID 예제 코드는 아니다.

export type Currency = "KRW" | "USD";

export interface Money {
  readonly amountMinor: bigint;
  readonly currency: Currency;
}

export interface CartLine {
  readonly productId: string;
  readonly unitPrice: Money;
  readonly quantity: number;
}

export type PricingError =
  | { readonly kind: "empty-cart" }
  | { readonly kind: "invalid-quantity"; readonly productId: string }
  | { readonly kind: "invalid-price"; readonly productId: string }
  | { readonly kind: "mixed-currency" };

export type Result<T, E> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };

export function calculateCartTotal(
  lines: readonly CartLine[],
): Result<Money, PricingError> {
  const first = lines[0];

  if (!first) {
    return { ok: false, error: { kind: "empty-cart" } };
  }

  const currency = first.unitPrice.currency;
  let amountMinor = 0n;

  for (const line of lines) {
    if (!Number.isSafeInteger(line.quantity) || line.quantity <= 0) {
      return {
        ok: false,
        error: { kind: "invalid-quantity", productId: line.productId },
      };
    }

    if (line.unitPrice.amountMinor < 0n) {
      return {
        ok: false,
        error: { kind: "invalid-price", productId: line.productId },
      };
    }

    if (line.unitPrice.currency !== currency) {
      return { ok: false, error: { kind: "mixed-currency" } };
    }

    amountMinor += line.unitPrice.amountMinor * BigInt(line.quantity);
  }

  return {
    ok: true,
    value: { amountMinor, currency },
  };
}

export interface Quote {
  readonly id: string;
  readonly total: Money;
  readonly createdAt: string;
}

export type QuoteEvent = {
  readonly kind: "quote-created";
  readonly quoteId: string;
  readonly amountMinor: bigint;
  readonly currency: Currency;
};

export interface CreateQuoteDependencies {
  readonly newQuoteId: () => string;
  readonly nowIso: () => string;
  /** 구현체는 텔레메트리 실패를 호출자에게 전파하지 않는 계약을 갖는다. */
  readonly record: (event: QuoteEvent) => void;
}

export function createQuote(
  lines: readonly CartLine[],
  dependencies: CreateQuoteDependencies,
): Result<Quote, PricingError> {
  const total = calculateCartTotal(lines);

  if (!total.ok) {
    return total;
  }

  const quote: Quote = {
    id: dependencies.newQuoteId(),
    total: total.value,
    createdAt: dependencies.nowIso(),
  };

  dependencies.record({
    kind: "quote-created",
    quoteId: quote.id,
    amountMinor: quote.total.amountMinor,
    currency: quote.total.currency,
  });

  return { ok: true, value: quote };
}

이 코드의 CUPID 해석은 다음과 같다.

속성 적용 지점
Composable 계산 함수와 견적 생성 함수의 입출력이 작고 명시적이며 프레임워크 의존성이 없다.
Unix calculateCartTotal은 총액 계산, createQuote는 견적 생성이라는 각각의 완결된 목적을 갖는다.
Predictable 시간과 ID 생성이 명시적 의존성이며, 오류가 판별 가능한 값으로 반환되고, 이벤트가 실행을 관찰하게 한다.
Idiomatic readonly, 판별 유니온, narrowing과 구조적 타입을 사용한다.
Domain-based Money, CartLine, Quote, PricingError가 기술 세부사항보다 업무 언어를 표현한다.

동일한 입력과 대체 의존성을 사용하면 결과를 재현할 수 있다.

const events: QuoteEvent[] = [];

const result = createQuote(
  [
    {
      productId: "product-1",
      unitPrice: { amountMinor: 10_000n, currency: "KRW" },
      quantity: 2,
    },
  ],
  {
    newQuoteId: () => "quote-1",
    nowIso: () => "2026-06-24T00:00:00.000Z",
    record: (event) => events.push(event),
  },
);

코드 리뷰용 질문표

CUPID를 점수표나 인증 기준으로 사용하기보다, 변경 대상의 마찰을 구체적인 증거와 연결하는 질문표로 사용하는 편이 적절하다.

속성 리뷰 질문 확인할 증거
C 소비자가 알아야 할 API와 설정이 최소한인가? 다른 코드와 자연스럽게 연결되는가? 공개 export 수, 초기화 단계, 전이 의존성, 테스트 설정량
U 목적을 한 문장으로 설명할 수 있는가? 입력과 출력이 다른 단계와 이어지는가? 모듈명, 함수명, 변경 이유, 호출 순서 문서화 필요성
P 결과와 실패가 예상 가능한가? 변동 요인과 운영 상태가 보이는가? 타입 계약, 테스트, 타임아웃, 재시도, 로그·메트릭·트레이스
I 언어 숙련자와 팀원이 자연스럽게 읽는가? 공식 문서와의 정렬, 린트·포맷 설정, 유사 코드 간 일관성
D 코드가 업무 용어와 경계를 드러내는가? 타입·함수 이름, 디렉터리 구조, 변경 시 수정 파일의 분포

리뷰 기록 템플릿:

## CUPID 리뷰: <대상>

### 변경 문맥
- 사용자가 하려는 일:
- 현재 가장 큰 마찰:

### 관찰
- C / Composable:
- U / Unix philosophy:
- P / Predictable:
- I / Idiomatic:
- D / Domain-based:

### 다음의 가장 작은 개선
- 변경:
- 기대 효과:
- 감수하는 트레이드오프:
- 검증 방법:

레거시 코드에 점진적으로 적용하는 방법

Dan North의 강연 자료는 CUPID를 기존 코드베이스 평가, 코드 비평, 레거시 코드의 시작점 선정에 사용할 수 있다고 제안한다.1819 다음 절차는 그 용도를 실무 흐름으로 확장한 편집 제안이다.

  1. 실제 변경 경로를 고른다. 전체 아키텍처를 추상적으로 평가하지 말고, 최근 장애나 다음 기능이 지나갈 코드 경로를 선택한다.
  2. 개발자의 마찰을 기록한다. 찾기 어려움, 설정 복잡성, 불명확한 상태, 숨은 부작용처럼 관찰 가능한 문제를 적는다.
  3. 가장 관련된 속성 하나를 고른다. 다섯 속성을 한 번에 개선하려 하지 않는다.
  4. 작고 되돌릴 수 있는 변경을 한다. 이름 개선, 입력 명시화, 도메인 타입 도입, feature 폴더로의 이동처럼 범위를 제한한다.
  5. 동작을 보호한다. 테스트가 부족하면 characterization test로 현재 동작을 고정하고, 운영 문제라면 계측을 먼저 추가한다.
  6. 효과를 확인한다. 다음 변경에서 탐색 시간, 설정량, 실패 재현성, 수정 파일 수가 실제로 줄었는지 본다.
  7. 반복되는 선택을 관용구로 만든다. 효과가 확인된 방식은 린터, 템플릿, ADR, 예제 코드로 팀의 로컬 관용구에 편입한다.

흔한 오해

“CUPID는 SOLID의 다섯 글자를 다른 다섯 글자로 치환한 것이다”

아니다. 탄생 배경은 SOLID 비판이지만, CUPID는 일대일 대체 규칙이 아니라 코드 결과를 관찰하는 속성 집합이다.

“Unix 속성을 지키려면 모든 함수를 아주 작게 만들어야 한다”

아니다. 핵심은 크기가 아니라 외부에서 인식되는 하나의 응집된 목적이다. 지나친 분해로 조합법이 암묵지가 되면 오히려 Composable이 약해진다.

“Predictable은 테스트 커버리지가 높다는 뜻이다”

아니다. 테스트는 증거 중 하나다. 명확한 이름과 타입, 결정적 동작, 오류 계약, 관찰 가능성도 포함한다.

“Idiomatic은 가장 최신 문법을 사용하는 것이다”

아니다. 해당 언어와 팀의 독자가 자연스럽게 읽고 도구가 일관성을 유지할 수 있는 표현을 사용하는 것이다.

“Domain-based는 반드시 DDD와 마이크로서비스를 도입하는 것이다”

아니다. 도메인 언어와 구조, 경계를 코드에 드러내는 것이 핵심이다. 모놀리스에서도 적용할 수 있으며 배포 단위를 무조건 잘게 나눌 필요가 없다.14

“Minimal dependencies는 외부 라이브러리를 사용하지 말라는 뜻이다”

아니다. 의존성의 수, 전이 비용, 충돌 가능성, 사용자가 떠안는 설정을 의식하라는 휴리스틱이다. 검증된 라이브러리를 제거하고 직접 재구현하는 것이 항상 더 낫지는 않다.

한계와 비판적으로 볼 점

  • 정성적이고 문맥 의존적이다. “얼마나 composable한가”에 객관적인 단일 척도가 없으므로 팀이 구체적인 증거와 트레이드오프를 기록해야 한다.
  • 기존 개념과 겹친다. Unix 철학, 최소 놀람, 관찰 가능성, 언어 관용구, Domain-Driven Design을 새로운 관계로 묶은 성격이 강하다. 이는 학습 장벽을 낮추는 장점이지만 개별 개념의 깊이를 대신하지는 않는다.
  • 자료가 창안자 중심이다. 본 조사에서 확인한 핵심 자료는 Dan North의 글·공식 사이트·강연과 산업계 해설이며, CUPID 자체의 효과를 검증한 동료평가 실증 연구는 확인하지 못했다.
  • 공식 실전 사례가 충분하지 않다. 조사 기준일인 2026-06-24 현재 공식 사이트의 Case Studies와 Resources 페이지에는 계획 항목이 TODO로 남아 있다.2021
  • 완전한 품질 모델이 아니다. 보안, 접근성, 성능, 데이터 일관성, 규제 준수, 비용, 복구 목표 같은 요구를 별도로 다뤄야 한다.
  • 속성끼리 충돌할 수 있다. 예측 가능성을 위한 추상화가 API를 넓힐 수 있고, 로컬 관용구가 언어 생태계 관용구와 다를 수 있다. CUPID 자체도 모든 것이 트레이드오프라고 전제한다.1

따라서 CUPID의 가장 강한 용도는 “좋은 코드의 보편 법칙”을 선언하는 것이 아니라, 코드 리뷰와 개선 대화에서 사람이 겪는 마찰을 다섯 방향으로 구체화하는 것이다.

자료 신뢰도와 조사 범위

등급 자료 사용 목적
1차 자료 Dan North 원문, 배경 글, 공식 CUPID 사이트, 발표 자료 정의, 의도, 세부 속성
독립 산업 자료 Thoughtworks Technology Radar 산업계의 평가와 도입 단계 확인
기반 자료 Bell System Technical Journal, Eric Evans의 DDD Reference Unix 철학과 도메인 언어의 역사적·개념적 배경
공식 기술 문서 TypeScript Handbook, OpenTelemetry 문서 TypeScript 및 관찰 가능성 예시의 근거
신뢰 가능한 실무 자료 Martin Fowler, Michael Feathers 단순 설계와 characterization testing 보충 설명

개인 블로그와 커뮤니티 글은 검색 과정에서 참고했지만, 핵심 정의와 결론의 근거로 사용하지 않았다. 코드 예시와 적용 절차 중 “편집 예시” 또는 “편집 제안”으로 표시한 부분은 공식 CUPID 규칙이 아니라 위 자료를 바탕으로 재구성한 실무 해석이다.

참고 자료

핵심 1차 자료

  1. Daniel Terhorst-North, CUPID: for joyful coding, 2022-02-10.
  2. Daniel Terhorst-North, CUPID: the back story, 2021-03-16.
  3. CUPID — for joyful code, 공식 사이트.
  4. CUPID Properties와 각 속성 페이지: Composable, Unix Philosophy, Predictable, Idiomatic, Domain-based.
  5. Daniel Terhorst-North, CUPID — for joyful coding, Speaker Deck, 2021-05-04.
  6. Daniel Terhorst-North, CUPID — For Joyful Coding in 7 Minutes, YOW! 2022.
  7. Daniel Terhorst-North, CUPID for joyful coding, Jfokus 발표 자료.

독립·보충 자료

  1. Thoughtworks, CUPID — Technology Radar, 2022-03-29.
  2. M. D. McIlroy 외, Unix Time-Sharing System 특집, Bell System Technical Journal, 1978.
  3. Eric Evans, Domain-Driven Design Reference, 2015.
  4. Martin Fowler, Ubiquitous Language, 2006.
  5. TypeScript, Narrowing — Discriminated unions, 공식 핸드북.
  6. OpenTelemetry, Observability primerSignals, 공식 문서.
  7. Michael Feathers, Characterization Testing, 2016.
  8. Martin Fowler, Beck Design Rules, 2015.

모든 웹 자료의 최종 확인일: 2026-06-24.

Footnotes

  1. Daniel Terhorst-North, “CUPID: for joyful coding”. 2 3 4 5 6

  2. CUPID Properties. 2 3

  3. Daniel Terhorst-North, “CUPID: the back story”. 2

  4. CUPID 공식 사이트.

  5. Composable. 2

  6. Unix Philosophy. 2

  7. Bell System Technical Journal, Unix 특집.

  8. Predictable.

  9. OpenTelemetry Observability primer.

  10. OpenTelemetry Signals.

  11. Michael Feathers, Characterization Testing.

  12. Idiomatic. 2

  13. TypeScript Handbook, Narrowing.

  14. Domain-based. 2 3

  15. Eric Evans, Domain-Driven Design Reference.

  16. Martin Fowler, Ubiquitous Language.

  17. Thoughtworks Technology Radar: CUPID.

  18. Daniel Terhorst-North, Speaker Deck.

  19. Jfokus 발표 자료.

  20. CUPID Case Studies.

  21. CUPID Resources.