title: "클래스 계층 전체를 대체하는 타입 객체 패턴" title_original: "This Design Pattern Replaces an Entire Class Hierarchy" channel: "ArjanCodes" video_id: "IdwdqdywNOM" published: "2026-09-18" url: "https://www.youtube.com/watch?v=IdwdqdywNOM" duration: "15:23" repository: "https://github.com/ArjanCodes/examples/tree/main/2026/type"
📌 핵심 질문 / 설계 논점
==변하는 것이 단순한 값인지, 도메인 데이터와 규칙인지, 아니면 근본적으로 다른 동작인지 구분한 뒤 그 성격에 맞는 모델링 방식을 선택해야 한다.==
- 단순한 범주나 상태는 enum 또는 다른 단순 값으로 충분하다.
- 가격·기능·용량처럼 범주에 의미 있는 데이터와 규칙이 붙으면 Type Object(타입 객체)가 적합하다.
- 고정 가격과 사용량 기반 가격처럼 구현 알고리즘 자체가 다르면 클래스나 프로토콜을 통한 다형성이 적합하다.
- 하나의 설계가 모든 상황에서 우월한 것이 아니며, 서로 다른 변형을 각자 알맞은 위치에 배치하는 조합이 가장 강력할 수 있다.
SaaS 구독 시스템의 플랜 변형을 사례로 삼으면, FreeSubscription, ProSubscription, BusinessSubscription 같은 상속 계층, PlanType과 외부 매핑 테이블, SubscriptionPlan 객체를 참조하는 Type Object 패턴을 차례로 비교할 수 있다. 설정만 달라지는 변형을 계속 서브클래스로 추가하면 비즈니스 설정이 클래스 계층에 흩어지지만, 타입 객체로 묶으면 플랜이라는 도메인 개념과 그 설정을 하나의 객체로 다룰 수 있다. 반대로 동작 알고리즘이 실제로 다르면 다형성을 유지해야 한다.
1. SaaS 구독 플랜에서 발생하는 변형
구독 객체는 고객과 구독 기간을 보유하면서 가격, 프로젝트 수, 저장 공간, 기능 목록을 통해 플랜별 권한을 표현한다.
1.1. 구독 도메인 모델의 공통 정보
-
구독의 공통 식별·시점 정보
- customer_id는 구독 고객을 식별한다.
- started_at은 구독이 시작된 시점을 나타낸다.
-
플랜별 설정 정보
- monthly_price는 월 요금이며 Decimal로 금액을 표현한다.
- max_projects는 고객이 생성할 수 있는 프로젝트 수의 한도다.
- storage_gb는 사용할 수 있는 저장 공간을 기가바이트 단위로 나타낸다.
- features는 플랜이 제공하는 기능의 불변 집합이다.
-
공통 질의와 규칙
- supports(feature)는 특정 기능이 플랜에 포함되는지 확인한다.
- can_create_project(current_projects)는 현재 프로젝트 수가 플랜 한도 안에 있는지 판단한다.
- 기능 열거형에는 BASIC_ANALYTICS, EXPORT_DATA, CUSTOM_REPORTS, SSO, AUDIT_LOG가 있다.
1.2. 플랜 수가 늘어날 때의 문제
-
서브클래스의 누적
- 초기에는 Free, Pro, Business 정도로 시작할 수 있다.
- Startup, Education, Business Plus, Legacy, Enterprise, Ultimate 같은 플랜이 추가되면 플랜 하나마다 Python 클래스가 늘어난다.
- 기존 플랜을 제거하더라도 과거 고객을 처리하려면 Legacy 같은 클래스가 남을 수 있다.
-
설정과 상속의 불일치
- 플랜 서브클래스가 서로 다른 알고리즘을 구현하는 것이 아니라 가격과 한도, 기능 목록만 지정한다면 상속은 설정 저장소 역할을 하게 된다.
- “데이터만 다른데 왜 상속이 필요한가?”라는 질문이 상속 계층을 재검토하게 만든다.
- 중요한 설계 선택은 플랜의 변형을 언어의 타입 시스템으로 표현할지, 데이터나 객체로 표현할지 결정하는 일이다.
2. 접근법 1 — 변형을 서브클래스로 모델링하기
서브클래스는 동일한 구독 인터페이스 뒤에 플랜별 값을 숨기므로, 플랜이 적고 안정적일 때 간단하고 읽기 쉽다.
2.1. 서브클래스 구현의 구조
-
공통 Subscription 데이터 클래스
- 기본 클래스는 고객 ID, 시작 시점, 월 요금, 프로젝트 한도, 저장 공간, 기능 집합을 가진다.
- 기능 지원 여부와 프로젝트 생성 가능 여부 같은 공통 메서드는 기본 클래스에 둔다.
-
플랜별 초기화
- FreeSubscription은 월 $0.00, 프로젝트 3개, 저장 공간 1GB, 기본 분석 기능만 제공한다.
- ProSubscription은 월 $20.00, 프로젝트 50개, 저장 공간 100GB를 제공하며 기본 분석·데이터 내보내기·사용자 정의 보고서를 포함한다.
- BusinessSubscription은 월 $75.00, 프로젝트 250개, 저장 공간 1,000GB를 제공하며 Pro 기능에 SSO와 감사 로그를 추가한다.
- 각 생성자는 고객 ID와 시작 시점을 받아 공통 생성자에 플랜별 설정을 전달한다.
-
다형적 사용
- 호출자는 값이 Free, Pro, Business 중 어느 구독인지 몰라도 supports()와 can_create_project()를 동일하게 호출할 수 있다.
- 호출자는 구체적인 클래스보다 Subscription이라는 공통 추상화에 의존한다.
- Pro 구독 예제는 월 $20.00, 데이터 내보내기 허용, SSO 불허, 프로젝트 50개 생성 가능이라는 결과를 낸다.
2.2. 언제 합리적인가
-
안정적인 소수의 플랜
- 플랜이 세 개처럼 적고 앞으로 크게 변하지 않는다면 이 구조는 충분히 합리적이다.
- 각 플랜을 명시적인 타입으로 읽을 수 있다는 점은 도메인 가독성에 도움이 된다.
-
설정만 담는 클래스의 비용
- 플랜 클래스에 본질적으로 다른 구독 알고리즘이 없고 초기화 코드만 있다면 새 제품 설정마다 새 Python 클래스가 필요하다.
- 플랜이 늘어날수록 클래스 계층이 비즈니스 설정 목록으로 변한다.
- 배포 없이 플랜을 추가하거나 과거 플랜을 유지해야 하는 요구에는 코드 변경이 필요하다는 점이 큰 제약이다.
3. 접근법 2 — 타입 값과 외부 매핑 테이블 사용하기
서브클래스를 제거하고 PlanType 열거형과 플랜별 매핑 딕셔너리로 차이를 표현하면 단순한 설정 모델을 빠르게 만들 수 있다.
3.1. 데이터 중심 구조
-
플랜 타입 값
- PlanType은 FREE, PRO, BUSINESS 같은 범주를 나타낸다.
- Subscription은 구체적 서브클래스 대신 customer_id, plan_type, started_at을 가진다.
-
분산된 매핑
- FEATURES_BY_PLAN은 플랜별 기능 집합을 저장한다.
- MONTHLY_PRICE_BY_PLAN은 플랜별 월 요금을 저장한다.
- MAX_PROJECTS_BY_PLAN은 플랜별 프로젝트 한도를 저장한다.
- STORAGE_GB_BY_PLAN은 플랜별 저장 공간을 저장한다.
- 새 플랜을 추가하려면 열거형과 각각의 매핑에 해당 데이터를 넣어야 한다.
-
실행 방식
- supports()는 구독의 plan_type으로 기능 매핑을 조회한다.
- can_create_project()는 동일한 plan_type으로 프로젝트 한도를 조회한다.
- Pro 구독 예제는 월 $20.00, 저장 공간 100GB, 데이터 내보내기 허용, SSO 불허, 프로젝트 50개 생성 불가라는 결과를 낸다.
3.2. 장점과 구조적 위험
-
단순함이 이기는 조건
- 설정 항목이 적고 플랜 종류도 제한적이면 매핑 테이블은 클래스보다 간단하다.
- 플랜이 단순한 범주에 가깝고 관련 규칙이 많지 않다면 객체를 추가하지 않아도 된다.
-
변형 증가에 따른 확장
- 기능, 가격, 프로젝트 한도, 저장 공간 외의 설정이 추가될 때마다 별도 자료구조가 늘어난다.
- 플랜 종류가 늘어나면 각 매핑에 같은 키를 반복해서 추가해야 한다.
-
불완전한 데이터의 위험
- 특정 플랜의 월 요금 매핑을 빠뜨리면 일부 실행 경로에서 조회 실패나 애플리케이션 오류가 발생할 수 있다.
- 한 플랜에 관한 지식이 여러 자료구조로 흩어져 있어 한 플랜의 전체 정의를 한눈에 확인하기 어렵다.
- 모든 설정을 하나의 거대한 plan_config 딕셔너리로 합치면 플랜의 데이터와 규칙을 한 단위로 묶게 되고, Type Object 패턴에 가까워진다.
4. 접근법 3 — Type Object로 변형을 객체화하기
Type Object 패턴은 언어의 상속 타입으로 변형을 표현하는 대신, 변형 자체를 도메인 객체로 표현한다.
4.1. SubscriptionPlan 도메인 객체
-
플랜을 독립적인 개념으로 모델링
- SaaS 플랫폼에서 “구독 플랜”은 고객 구독과 구별되는 실제 도메인 개념이다.
- SubscriptionPlan은 이름, 월 요금, 프로젝트 한도, 저장 공간, 기능 집합을 하나의 불변 데이터 클래스로 묶는다.
- supports()와 can_create_project() 같은 플랜 관련 규칙도 플랜 객체에 둔다.
-
구독과 플랜의 관계
- Subscription은 plan_type 열거형 대신 실제 SubscriptionPlan 객체를 참조한다.
- 고객은 “특별한 ProSubscription 객체”를 갖는 것이 아니라 “Pro 플랜에 대한 구독”을 가진다.
- FREE, PRO, BUSINESS 객체가 각 플랜의 전체 정의를 보유한다.
-
구현 예시
- FREE는 $0.00, 3개 프로젝트, 1GB, 기본 분석 기능을 가진다.
- PRO는 $20.00, 50개 프로젝트, 100GB, 기본 분석·데이터 내보내기·사용자 정의 보고서를 가진다.
- BUSINESS는 $75.00, 250개 프로젝트, 1,000GB, 위 기능과 SSO·감사 로그를 가진다.
- Pro 객체를 참조하는 구독의 출력은 월 $20.00, 100GB, 데이터 내보내기 허용, SSO 불허, 프로젝트 50개 생성 불가다.
4.2. Type Object와 단순 열거형의 경계
-
열거형이 충분한 변형
- active, past_due, canceled 같은 구독 상태는 대개 범주 또는 레이블이다.
- 상태 자체에 의미 있는 데이터와 규칙이 붙지 않는다면 별도의 객체로 만들 필요가 없다.
-
객체가 필요한 변형
- Pro 플랜의 의미는 “pro라는 이름”에 그치지 않고 가격, 프로젝트 한도, 저장 공간, 기능 권한을 포함한다.
- 이런 정보를 하나의 객체로 묶으면 플랜의 정체성·데이터·규칙이 함께 이동한다.
- Type Object는 데이터 중심 설계이면서도 분산 매핑보다 도메인 모델을 선명하게 만든다.
5. 플랜 정의를 어디에 둘 것인가
Type Object의 형태를 정한 뒤에는 플랜 객체가 코드에 고정될지, 외부 데이터로 관리될지 결정해야 한다.
5.1. 배포 시점에만 바뀌는 고정 플랜
-
plans.py에 불변 객체 배치
- 플랜이 고정되어 있고 애플리케이션을 배포할 때만 변경된다면 plans.py 같은 별도 파일에 불변 객체를 정의한다.
- 필요한 곳에서 객체를 import하면 되므로 설정의 단일 출처를 유지할 수 있다.
- 데이터 중심 접근법을 선택해도 별도 파일에 플랜 정의를 둘 수 있다.
-
적합한 상황
- 플랜이 제품 릴리스와 함께만 바뀐다.
- 코드 리뷰와 자동 테스트를 통해 변경을 검증하고 싶다.
- 데이터베이스 조회 비용과 운영 데이터 불일치 위험을 피하고 싶다.
5.2. 배포 없이 바뀌는 동적 플랜
-
데이터베이스 모델
- 더 큰 SaaS 시스템에서는 새 플랜이나 가격 정책을 코드 배포 없이 추가해야 할 수 있다.
- 구독 레코드는 고객 ID와 플랜 ID를 저장한다.
- 구독 플랜 테이블은 플랜 정의를 저장하고, 관련 테이블은 기능 목록을 저장할 수 있다.
- 기존 코드는 새 레코드를 읽어 동작하므로 새 플랜 추가가 새 클래스 추가보다 유연하다.
-
데이터베이스 방식의 비용
- 고객별로 기능 목록을 따로 저장하면 제품 변경 뒤 일부 고객의 문서를 갱신하지 못해 권한 불일치가 생길 수 있다.
- 과거·현재 플랜과 고객별 기능 조합이 늘어나므로 테스트해야 하는 경우의 수가 커진다.
- 기능과 설정을 매번 데이터베이스에서 읽으면 애플리케이션이 느려질 수 있다.
- 데이터베이스는 높은 유연성을 주지만 일관성·테스트·성능을 관리하는 비용을 요구한다.
6. 행동이 다르면 다형성을 유지하기
설정 값만 다른 플랜과 알고리즘 자체가 다른 플랜을 같은 방식으로 모델링하면 안 된다.
6.1. 고정 가격과 사용량 기반 가격
-
본질적으로 다른 청구 동작
- 고정 가격 구독은 매월 동일한 금액을 청구한다.
- 사용량 기반 가격은 사용량을 모아 최종 금액을 계산한다.
- 두 방식은 단순히 다른 설정값이 아니라 서로 다른 가격 계산 알고리즘이다.
-
가격 모델 프로토콜
- PricingModel 프로토콜은 monthly_price(usage)라는 동작 계약을 정의한다.
- FixedPricing은 고정 금액을 반환한다.
- UsageBasedPricing은 기본 요금, 포함 좌석, 추가 좌석 단가, 포함 API 요청 수, 추가 요청 블록 단가를 사용한다.
- 사용량 기반 계산은 포함 좌석을 초과한 좌석 수와 1,000건 단위의 추가 API 요청 블록을 계산해 기본 요금에 더한다.
6.2. Type Object와 행동 다형성의 조합
-
플랜 객체 안에 가격 모델 합성하기
- SubscriptionPlan은 이름, 프로젝트 한도, 기능 집합과 함께 pricing이라는 가격 모델 참조를 가진다.
- Pro 플랜은 FixedPricing($20.00)을 사용한다.
- Enterprise 플랜은 최대 10,000개 프로젝트와 전체 기능을 제공하며 사용량 기반 가격 모델을 사용한다.
-
사용량 기반 예시
- Enterprise의 기본 요금은 $500.00이다.
- 기본 포함 좌석은 25명이고 초과 좌석은 한 명당 $12.00이다.
- 기본 포함 API 요청은 100,000건이고 초과 요청 1,000건마다 $0.50을 더한다.
- 활성 좌석 40명과 API 요청 125,000건이면 초과 좌석 15명과 초과 요청 25블록이 발생해 월 $692.50이 된다.
-
합성의 의미
- Type Object는 플랜이라는 데이터 변형을 표현한다.
- 가격 모델 프로토콜은 가격 계산이라는 행동 변형을 표현한다.
- 다형성을 없애는 것이 아니라 각각의 변형을 실제로 속한 위치에 배치한다.
- 이 조합은 상속 계층 하나에 설정과 알고리즘을 모두 넣는 대신, 객체 합성으로 확장 지점을 분리한다.
7. 변형의 성격에 따른 선택 기준
설계 선택은 “상속인가 Type Object인가”라는 이분법이 아니라 무엇이 실제로 변하는지에 따라 내려야 한다.
7.1. 세 가지 변형 분류
-
값 또는 범주가 변하는 경우
- 변형이 레이블이나 카테고리뿐이면 enum 또는 단순 값이 적합하다.
- 상태·종류를 식별하는 것 외에 데이터와 규칙을 요구하지 않는다면 객체를 만들지 않는다.
-
도메인 데이터와 규칙이 변하는 경우
- 변형마다 가격, 권한, 한도, 저장 공간처럼 의미 있는 정보가 있으면 Type Object를 고려한다.
- 변형 정의를 하나의 객체로 묶으면 관련 데이터와 플랜 규칙을 함께 변경·검증할 수 있다.
-
행동 구현이 근본적으로 변하는 경우
- 가격 계산처럼 같은 인터페이스를 따르지만 구현 알고리즘이 본질적으로 다르면 클래스 또는 Python 프로토콜을 사용한다.
- 이런 경우 상속이나 프로토콜 기반 다형성이 각 구현의 차이를 보존한다.
7.2. 실무 점검 질문
-
변하는 대상을 먼저 명명하기
- 지금 변하는 것은 단순한 값인가?
- 도메인 데이터와 규칙인가?
- 아니면 정말로 서로 다른 행동 구현인가?
-
변경의 출처와 빈도 확인하기
- 배포 때만 바뀌는 고정 플랜이면 코드의 불변 객체가 단순하고 안전할 수 있다.
- 운영 중 자주 추가·수정되는 플랜이면 데이터베이스 기반 모델이 필요할 수 있다.
- 데이터베이스 기반 모델은 유연성의 대가로 일관성, 테스트 범위, 성능을 관리해야 한다.
-
혼합 설계 허용하기
- 플랜 데이터는 Type Object에 두고 가격 알고리즘은 프로토콜 구현으로 분리할 수 있다.
- 상속을 무조건 제거하거나 모든 변형을 객체화하는 대신, 각 변형의 성격에 맞게 조합한다.
주요 발언 모음
“변하는 것이 무엇인지 생각하라. 값인가, 도메인 데이터와 규칙인가, 정말로 다른 행동인가?”
“Type Object는 변형을 프로그래밍 언어의 타입 시스템이 아니라 객체로 표현한다.”
“다형성을 없애는 것이 아니라, 각 변형을 실제로 속한 곳에 배치한다.”
“상속이나 Type Object 패턴을 곧바로 선택하지 말고, 변형의 성격부터 확인해야 한다.”
핵심 데이터 & 수치
- Free 플랜: 월 $0.00, 최대 프로젝트 3개, 저장 공간 1GB, 기본 분석 기능.
- Pro 플랜: 월 $20.00, 최대 프로젝트 50개, 저장 공간 100GB, 기본 분석·데이터 내보내기·사용자 정의 보고서.
- Business 플랜: 월 $75.00, 최대 프로젝트 250개, 저장 공간 1,000GB, Pro 기능·SSO·감사 로그.
- Enterprise 예시: 최대 프로젝트 10,000개, 기본 요금 $500.00, 포함 좌석 25명, 초과 좌석당 $12.00, 포함 API 요청 100,000건, 추가 1,000건당 $0.50.
- 사용량 기반 결과: 활성 좌석 40명과 API 요청 125,000건이면 Enterprise 월 요금은 $692.50이다.
- 영상 길이와 챕터: 총 15분 23초이며, 0:00 문제 제기, 2:22 서브클래스, 3:47 타입 값, 5:47 타입 객체, 8:51 열거형과 비교, 9:45 타입 객체의 저장 위치, 14:09 세 접근법 선택, 15:05 마무리로 구성된다.
결론 및 실무 시사점
- 플랜 수가 적고 안정적이며 각 타입이 실제로 다른 행동을 갖는다면 서브클래스와 다형성이 자연스럽다.
- 단순한 범주를 표현하는 데 상속을 사용하면 설정 변경마다 클래스를 추가해야 하므로 enum 또는 단순 값이 더 적합하다.
- 범주에 가격·권한·한도·규칙이 붙으면 SubscriptionPlan 같은 Type Object가 도메인 의미를 보존하면서 설정을 한곳에 모은다.
- 배포 없는 변경이 필요하면 플랜 ID와 데이터베이스 정의로 확장할 수 있지만, 데이터 일관성과 테스트 범위를 함께 관리해야 한다.
- 고정 가격과 사용량 기반 가격처럼 알고리즘이 다르면 PricingModel 프로토콜과 구현 객체로 행동을 분리한다.
- Type Object와 프로토콜을 합성하면 플랜 데이터의 변형과 가격 알고리즘의 변형을 서로 독립적으로 확장할 수 있다.
- 좋은 설계는 패턴 이름을 먼저 고르는 것이 아니라 변화의 종류, 변경 빈도, 데이터 저장 위치, 행동 차이를 먼저 파악하는 데서 시작한다.
부가 정보
- Software Design Mastery 프로그램 대기자 명단: https://arjan.codes/mastery
- 예제 저장소: https://github.com/ArjanCodes/examples/tree/main/2026/type
- 예제 파일: 01_subclasses.py, 02_plan_type.py, 03_type_object.py, 04_pricing_composition.py
- Python 예제는 dataclass, Decimal, StrEnum, Protocol, frozenset을 사용해 불변 설정과 타입별 행동을 표현한다.
- 마무리에서는 시청자에게 자신의 코드에서 같은 설계 문제를 겪었는지, 구독 외 어떤 도메인에서 어떤 해법을 선택했는지 댓글로 공유해 달라고 요청하고 다음 추천 콘텐츠와 구독·좋아요를 안내한다.
핵심 요약 (20줄)
SaaS 구독 시스템은 플랜마다 가격·기능·프로젝트 한도·저장 공간이 달라지는 변형 문제를 가진다. 플랜 수가 적고 안정적이면 FreeSubscription·ProSubscription·BusinessSubscription 같은 서브클래스가 읽기 쉽다. 서브클래스 방식은 공통 메서드 뒤에 플랜별 설정을 숨겨 호출자가 구체 타입을 몰라도 되게 한다. 설정만 다른 플랜을 계속 클래스로 추가하면 클래스 계층이 비즈니스 구성 목록으로 변한다. PlanType 열거형과 외부 매핑 테이블은 작은 구성 문제를 단순하게 해결한다. 기능·가격·프로젝트 한도·저장 공간을 별도 매핑에 나누면 한 플랜의 정보가 여러 곳으로 흩어진다. 매핑 항목을 빠뜨리면 특정 플랜에서 조회 실패나 애플리케이션 오류가 발생할 수 있다. SubscriptionPlan은 플랜의 이름·요금·한도·기능·규칙을 하나의 도메인 객체로 묶는다. Type Object 패턴은 언어의 상속 타입 대신 객체로 변형을 표현한다. 고객은 특별한 ProSubscription 타입이 아니라 Pro 플랜에 대한 Subscription을 가진다고 모델링할 수 있다. 단순한 active·past_due·canceled 같은 상태는 보통 enum만으로 충분하다. 가격과 권한처럼 범주에 의미 있는 데이터와 규칙이 붙으면 그 범주를 객체로 만드는 편이 자연스럽다. 배포 때만 플랜이 바뀌면 plans.py에 불변 플랜 객체를 두는 방법이 간단하다. 배포 없이 플랜을 추가해야 하면 플랜 ID와 정의를 데이터베이스에서 읽는 구조를 고려할 수 있다. 데이터베이스 기반 플랜은 유연하지만 고객별 권한 불일치와 더 넓은 테스트 조합을 낳을 수 있다. 고정 가격과 사용량 기반 가격은 설정값이 아니라 서로 다른 청구 알고리즘이다. PricingModel 프로토콜은 서로 다른 가격 계산 구현을 같은 인터페이스로 다루게 한다. Type Object와 가격 모델 프로토콜을 합성하면 데이터 변형과 행동 변형을 독립적으로 확장할 수 있다. 변하는 것이 값인지 데이터와 규칙인지 행동인지 구분해야 패턴을 올바르게 선택할 수 있다. 상속·enum·Type Object·프로토콜은 경쟁 관계가 아니라 변화의 성격에 따라 조합할 수 있는 도구다.
