OOD, Object Oriented Design 모델링 디자인 [3]
Creational Design Pattern and OOD interview approaching guide again.
객체지향 설계(Object Oriented Design) 질문에서 면접관은 복잡한 문제의 미묘한 부분(뉘앙스)을 이해하는 능력과, 요구사항을 이해하기 쉬운 클래스 구조로 변환하는 능력을 보고자 합니다.
사실 OOD 질문은 전반적으로 거의 비슷한 패턴을 따릅니다. 면접에서는 다소 모호한 문제와 시스템을 설계할 때 고려해야 할 제약 조건 몇 가지만 주어지고, 그 외에는 거의 제공되지 않습니다. 이후에는 지원자인 당신이 면접관이 원하는 해답의 “수준(level)”이 어느 정도인지, 어떤 기능이 필요할지 파악한 뒤, 실제로 동작 가능한 해결책을 만들어내야 합니다.
면접관이 보는 핵심은 한 가지입니다.
지금 당장 바로 동작하는 해결책과 미래의 변화에도 유연하게 적응할 수 있는 해결책 사이에서 올바른 균형을 찾는 것입니다.
이를 단순화하기 위해, 어떤 OOD 문제를 만나더라도 다음과 같은 접근법을 사용할 수 있습니다
따라서, 복잡하고 모호한 문제를, 이해 가능한 클래스로 변환하는 능력.
문제 설명은 대충
제약 조건은 몇 개만
구체적 기능은 거의 없음
그리고 그다음은 전부 지원자 몫이다.
어떤 수준의 설계를 기대하는지 파악하고,
어떤 기능이 필요한지 후보를 만들고,
확장 가능한 구조를 제시해야 한다.
1) 요구사항을 명확히 하기 (Clarify the requirements)
면접관이 기대하는 바를 확실히 이해해야 합니다. 꼭 필요하다면 질문을 통해 요구사항을 명확히 하세요.면접관은 이를 싫어하지 않으며 오히려 좋은 인상을 받을 가능성이 큽니다. 예를 들어, “해결책의 구조를 보여주는 수준을 원하시나요, 아니면 완전한 구현까지 원하시나요?” 같은 질문을 할 수 있습니다.
이 단계는 5~10초 정도면 충분하지만, 이후 시간을 크게 절약해줍니다.
2) 주요 사용 사례(use case)를 정리하기 (Hash out the primary use cases)
시스템이 가져야 하는 다양한 기능과 사용 사례를 먼저 생각하고, 말로 정리해보세요.
이렇게 하면 시스템이 어떤 기능을 제공해야 하는지 확실히 이해할 수 있습니다. 또한 말로 설명하는 과정에서, 바로 구현을 시작했다면 놓쳤을 요구사항이나 아이디어를 새롭게 발견할 수도 있습니다.
여기서 중요한 포인트는 기능을 떠올리고 끝내는 게 아니라,
면접관에게 “요구사항을 구조로 바꾸는 사고”를 보여주는 것이다.
예를 들어, Parking Lot 문제라면
차량이 들어온다
주차 공간을 배정한다
차량이 나간다
결제한다
주차 공간 상태가 업데이트된다
이걸 “사용자 관점”으로 정리해주는 게 핵심이다.
이 단계에서 말을 꺼내면서 정리하면, 두 가지 이점이 생긴다.
내 설계가 무엇을 커버해야 하는지 명확해짐
면접관이 “이 지원자, 요구사항을 제대로 잡고 가는구나”라고 느낀다
OOD 문제에서 가장 흔한 실수는 Use case가 확정되지 않았는데
클래스를 먼저 만드는 것입니다.
3) 핵심 객체(Object)를 식별하기 (Identify key Objects)
이제 해결책에서 역할을 수행할 객체들을 모두 찾아냅니다.
예를 들어 주차장(parking lot)을 설계한다면, 차량(vehicle), 주차 공간(parking spot), 주차장(parking garage), 입구(entrance), 출구(exit), 운영자(garage operator) 등이 여기에 포함될 수 있습니다.
예를 들어 Parking Lot이면 객체 후보는 이런 것들이다.
Vehicle(차량)
ParkingSpot(주차 공간)
ParkingGarage(주차장)
Entrance / Exit
Ticket
PaymentProcessor
Operator(관리자)
여기서 중요한 건 “모든 걸 객체로 만들지 않는 것”이다.
OOD는 객체를 많이 만드는 게 아니라,
책임이 자연스럽게 나뉘는 경계를 찾는 게 핵심이다.
면접에서.
변할 가능성이 높은 것 → 객체화
정책/규칙이 붙는 것 → 객체화
시스템 흐름의 중심이 되는 것 → 객체화
4) 객체가 지원해야 하는 동작(Operations)을 정의하기 (Identify Operations supported by Objects)
앞 단계에서 식별한 각 객체가 어떤 행동(기능)을 수행해야 하는지 정리합니다.
예를 들어 자동차는 이동할 수 있어야 하고, 특정 공간에 주차할 수 있어야 하며, 번호판 정보를 가져야 합니다.
주차 공간은 이륜 차량 또는 사륜 차량을 수용할 수 있어야 하는 등, 각 객체에 기대되는 행동을 구체화합니다.
이제 각 객체의 “역할과 행동”을 만듭니다. 여기서 해야 하는 질문은 단순합니다.
“이 객체는 무엇을 알고, 무엇을 할 수 있어야 하는가?”
예를 들어 Vehicle이라면,
licensePlate(번호판) 정보를 가진다
특정 Spot에 주차할 수 있다(park)
나갈 수 있다(exit)
ParkingSpot이라면,
현재 점유 여부를 가진다(isOccupied)
어떤 타입의 차량을 수용 가능한지 알고 있다(canFit)
차량이 들어오면 점유로 바뀐다(assignVehicle)
차량이 나가면 비워진다(removeVehicle)
이 단계를 잘하면 자연스럽게 “클래스 책임 분리”가 선명해진다.
그리고 면접에서 특히 점수가 오르는 포인트는
Operations를 말할 때 이런 식으로 표현하는 것이다.
“이 객체는 상태(State)를 갖고”
“상태를 변경하는 메서드를 통해”
“시스템의 일관성을 유지합니다”
OOD는 단순히 메서드를 나열하는 게 아니라
상태와 행동을 함께 설계하는 문제.
5) 객체 간 상호작용(Interactions)을 정의하기 (Identify Interactions between Objects)
서로 다른 객체들이 어떤 관계를 맺고 어떻게 상호작용해야 하는지 관계를 맵핑합니다. 여기서 전체 설계가 하나로 연결됩니다.
예를 들어 자동차는 주차 공간에 주차할 수 있어야 하고, 주차장은 여러 개의 주차 공간을 담을 수 있어야 합니다.
요약하면, 마지막 단계는 “관계”를 설계하는 것.
이 단계에서 설계가 한 번에 정리된다.
어떤 객체가 어떤 객체를 소유(Composition)하는지
어떤 객체가 어떤 객체에 의존(Dependency)하는지
누가 누구를 호출하고 흐름이 어떻게 진행되는지
예를 들어 Parking Lot이면 이런 관계가 나온다.
ParkingGarage는 ParkingSpot 여러 개를 포함한다
Entrance는 Ticket을 발급한다
Ticket은 Vehicle과 Spot 정보를 참조한다
Exit은 Ticket을 받아 PaymentProcessor와 상호작용한다
결제가 완료되면 Spot을 비운다
이 과정에서 면접관이 원하는 것은
“if-else로 풀 수 있는 문제를 객체 협력으로 풀어내는 과정”
즉, 시스템이 커질수록 복잡해지는 로직을
객체 간 관계와 책임으로 정리할 수 있는지 보는 것이다.
그래서 면접관이 보는 건 구조 그 자체보다 아래의 질문.
지금 요구사항에는 이 정도 구조면 충분한가?
앞으로 변경이 들어오면 어디가 바뀔 것 같은가?
그 변경을 최소한의 수정으로 흡수할 수 있는가?
그래서 좋은 OOD 답변은 항상 다음을 포함한다.
“지금은 이렇게 설계하고”
“추가 요구가 나오면 이 부분을 확장하면 된다”
“미래 변화”까지 고려했다는 시그널이 됩니다.
OOD를 위한 Design Patterns 정리 (Creational / Structural / Behavioral)
OOD 면접에서 디자인 패턴을 “외우는 것”만으로는 부족하다. 면접관이 보고 싶은 건 보통 아래와 같다.
상황을 정확히 분해하고
확장 가능하고 유지보수 가능한 구조로 재조립하며
변경에 강한 경계(추상화)를 어디에 둘지 결정하는 능력
디자인 패턴은 그 결정을 빠르게 도와주는 “검증된 설계 레시피”다.
특히 실무/면접에서 패턴은 아래처럼 활용된다
요구사항이 바뀌어도 덜 흔들리는 구조 만들기
객체 생성과 사용의 결합도를 낮추기
복잡한 조건문(if-else 폭발)을 다형성으로 정리하기
기능 추가가 반복될 때 기존 코드 수정 없이 확장하기(Open/Closed)
패턴은 크게 3가지 분류로 나뉜다.
Creational(생성) 패턴
“객체를 어떻게 만들 것인가?”
Structural(구조) 패턴
“객체를 어떻게 조립할 것인가?”
Behavioral(행동) 패턴
“객체가 어떻게 협력하고 변화할 것인가?”
아래는 대표적인 OOD 연습 문제 리스트입니다. 디자인 패턴 컨텐츠가 완료되면,
연습문제와 실제 인터뷰 문제들로 다룰 예정입니다.
Parking Lot
Elevator
Vending Machine
ATM
Movie Ticket Booking
Ride Sharing (Uber)
Restaurant Reservation
Library Management
Chess / Card Game
File System
디자인 패턴을 공부할 때 연습해야 하는 사고법 - 어떤 문제에서라도 해결안을 도출하기 위함.
OOD의 본질은 기능 구현이 아니라 변경 비용 최소화입니다.
어떤 부분이 자주 바뀔까?
어떤 부분이 계속 추가될까?
어떤 부분이 외부 의존성으로 흔들릴까?
위 3가지 요소를 먼저 생각해서 시작해야 합니다.
“객체 생성이 복잡해지면” 생성 패턴은 선택이 아니라 필수다
파라미터가 많다 → Builder
타입이 늘어난다 → Factory Method
세트로 바뀐다 → Abstract Factory
복제가 효율적이다 → Prototype
물론 장점과 단점을 모두 알고 있어야 합니다.
면접관이 원하는 건 왜 그 책임을 그 객체에 줬는가 이기 때문에 아래와 같이 제시해야 합니다.
“이 로직은 변동성이 높아서 여기로 빼고,
호출부는 고정된 인터페이스만 보게 했습니다.”
“경계(boundary)”를 먼저 친다 (외부/내부 분리)
여기서 설계 수준이 갈린다.
내부
도메인 모델(주문, 상품, 사용자)
외부
결제 게이트웨이, 배송사 API, 이메일/SMS
외부가 있는 순간
인터페이스 표준화 → Adapter
복잡도 숨기기 → Facade
캐싱/권한/레이지 → Proxy
객체 책임을 “단일 책임 + 확장 가능”으로
패턴은 결국 SRP(단일 책임)를 위해 존재한다.
좋은 분리 기준
“한 클래스가 바뀌는 이유가 하나인가?”
“정책이 늘어나도 기존 코드를 수정하지 않는가?”
정책이 늘어나는 로직이 한 클래스에 몰려 있으면 → 패턴 투입 타이밍.
패턴 적용 후 검증 질문 3개로 체크해봅니다.
패턴을 적용한 뒤, 이 3개 질문에 “예”면 성공이다.
새 정책/타입 추가할 때 기존 코드를 거의 수정 안 하는가?
외부 시스템 변경이 내부 도메인에 새지 않는가?
테스트가 쉬운 구조인가? (의존성을 주입할 수 있는가?)
면접에서.
“이 부분이 변화 축이라서 인터페이스로 분리하겠습니다.”
“여기는 정책 계층이고, 여기서 다형성으로 확장하겠습니다.”
“외부 의존성은 Adapter로 감싸서 도메인 모델을 보호하겠습니다.”
“요청 흐름은 Facade/Service가 오케스트레이션하고, 세부 정책은 Strategy로 분리하겠습니다.”
즉, 패턴 이름이 아니라 설계 이유를 먼저 말합니다.
2. Creational Patterns (생성 패턴)
생성 패턴은 “객체 생성 로직이 복잡해질 때” 빛난다.
특히 면접에서 자주 나오는 포인트는 new를 남발하지 않고,
객체 생성의 책임을 분리해서 변경에 강한 구조를 만드는 것이다.
1) Builder Pattern
핵심: “복잡한 객체를 단계적으로 안전하게 생성한다.”
언제 쓰나?
생성자 파라미터가 너무 많아져서
new User(a,b,c,d,e...)같은 코드가 나오기 시작할 때옵션 조합이 많아 필수/선택 값이 섞이고
객체 생성 과정이 “순서”나 “중간 상태”를 갖는 경우
해결하는 문제
Telescoping Constructor 문제 (생성자 오버로드 폭발)
필드가 많아서 가독성 + 실수가 늘어남
장점
가독성 최고
builder.setA().setB().build()
immutable 객체 만들기 쉬움
validation을 build 시점에 모아서 처리 가능
단점
클래스/코드가 늘어남
간단한 객체에 쓰면 오히려 과함
면접에서.
“필수 파라미터는 생성자에서 받고, 옵션은 Builder에서 처리하면 안전합니다. 객체 생성이 많아지고 옵션이 늘어나도 코드가 깨끗하게 유지됩니다.”
인터뷰 문제 예시
“Order(주문) 생성 시 옵션이 너무 많고 조합이 많다. 안정적으로 만들 방법은?”
“CheckoutRequest가 파라미터가 15개인데 실수 없이 만드는 구조는?”
Amazon에서 어디에 쓰나?
Checkout / Order 생성
배송지, 결제수단, 쿠폰, 기프트, 배송속도, 세금, 통화, 구독할인 등 옵션 폭발
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
user_id: str
items: list
shipping_address: str
payment_method: str
coupon_code: str | None
gift_wrap: bool
delivery_speed: str
class OrderBuilder:
def __init__(self, user_id: str):
self._user_id = user_id
self._items = []
self._shipping_address = ""
self._payment_method = ""
self._coupon_code = None
self._gift_wrap = False
self._delivery_speed = "STANDARD"
def add_item(self, sku: str, qty: int):
self._items.append({"sku": sku, "qty": qty})
return self
def shipping_to(self, address: str):
self._shipping_address = address
return self
def pay_with(self, method: str):
self._payment_method = method
return self
def apply_coupon(self, code: str):
self._coupon_code = code
return self
def gift_wrap(self, enabled: bool = True):
self._gift_wrap = enabled
return self
def delivery_speed(self, speed: str):
self._delivery_speed = speed
return self
def build(self) -> Order:
if not self._items:
raise ValueError("Order must have at least 1 item.")
if not self._shipping_address or not self._payment_method:
raise ValueError("Shipping + Payment are required.")
return Order(
user_id=self._user_id,
items=self._items,
shipping_address=self._shipping_address,
payment_method=self._payment_method,
coupon_code=self._coupon_code,
gift_wrap=self._gift_wrap,
delivery_speed=self._delivery_speed,
)
2) Singleton Pattern
핵심: “프로세스 내에서 인스턴스를 딱 1개만 유지한다.”
언제 쓰나?
전역적으로 공유해야 하는 단 하나의 리소스
예: Config, Logger, Metrics Registry, Connection Pool Manager
장점
접근이 쉬움 (어디서든 같은 인스턴스)
상태 공유/캐싱 등에 사용 가능
단점 (면접에서 중요)
테스트가 어려워짐
전역 상태로 인해 의존성이 숨겨짐
멀티스레드에서 초기화 경쟁, 동기화 문제가 생김
면접에서.
“Singleton은 전역 상태를 만들기 쉬워서 남발하면 위험합니다. 필요하다면 DI(Dependency Injection) 컨테이너로 대체하거나, Singleton을 인터페이스 뒤에 숨겨 테스트 가능성을 확보하는 게 좋습니다.”
인터뷰 문제 예시
“서비스 전역에서 하나만 있어야 하는 컴포넌트는 뭐가 있을까?”
“Rate limiter / Config cache를 앱 내에서 한 번만 초기화하려면?”
Amazon에서 어디에 쓰나?
Config Registry, Feature Flag Client, Metrics Collector
“주문 서비스가 어떤 리전인지 / 실험군인지” 같은 설정은 전역 공유가 일반적
class ConfigRegistry:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._data = {}
return cls._instance
def set(self, k, v): self._data[k] = v
def get(self, k, default=None): return self._data.get(k, default)3) Prototype Pattern
핵심: “새 객체를 생성할 때 기존 객체를 복제(clone)한다.”
언제 쓰나?
객체 생성이 무겁거나(구성 요소가 많음)
유사한 객체를 반복적으로 생성해야 할 때
초기 상태(템플릿)가 중요한 경우
장점
복잡한 초기화 로직을 복제 기반으로 단순화
런타임에서 새로운 타입을 쉽게 추가 가능
단점
deep copy vs shallow copy 문제
복제 대상이 mutable이면 버그 가능성 증가
면접에서.
“게임에서 몬스터 스폰 시 기본 스탯 템플릿을 Prototype으로 들고 있다가 복제해서 일부만 변경하는 구조가 깔끔합니다.”
인터뷰 문제 예시
“유사한 장바구니/주문을 반복 생성해야 한다. 빠르게 찍어내려면?”
“템플릿 기반으로 동일한 구조의 ProductConfig를 복제해야 한다.”
Amazon에서 어디에 쓰나?
Cart 복제: “Save for later”, “Buy again”, “Reorder”
프로모션/정책 적용 전 “임시 주문”을 복제해서 시뮬레이션
import copy
class Cart:
def __init__(self, user_id: str, items: list):
self.user_id = user_id
self.items = items # list of dicts
def clone(self) -> "Cart":
return copy.deepcopy(self)
cart = Cart("u1", [{"sku": "A", "qty": 1}])
reorder_cart = cart.clone()
reorder_cart.items.append({"sku": "B", "qty": 2})4) Factory Method Pattern
핵심: “객체 생성 책임을 서브클래스(또는 구현체)에게 위임한다.”
언제 쓰나?
if(type==A) new A(); else if(type==B) new B();
이런 조건문이 계속 늘어날 때어떤 구현체를 만들지는 런타임에 결정될 때
장점
생성 로직을 한 곳에 모음
OCP(Open/Closed): 새로운 타입 추가 시 기존 코드 수정 최소화
단점
클래스가 늘어날 수 있음
면접에서.
“Factory Method는 객체 생성의 결합도를 줄이고, 조건문 폭발을 다형성으로 흡수합니다.”
인터뷰 문제 예시
“결제 수단이 늘어난다(카드/포인트/BNPL). 객체 생성 분기를 어떻게 정리할까?”
“배송 정책(Prime/Standard/Same-day) 생성 로직을 깔끔하게 만들자.”
Amazon에서 어디에 쓰나?
Payment Method 생성
Shipment Planner 생성
Tax Calculator 생성(국가별/주별 정책)
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount: int) -> str: ...
class CardPayment(PaymentProcessor):
def pay(self, amount: int) -> str:
return f"paid {amount} by CARD"
class PointsPayment(PaymentProcessor):
def pay(self, amount: int) -> str:
return f"paid {amount} by POINTS"
class PaymentFactory:
def create(self, payment_type: str) -> PaymentProcessor:
if payment_type == "CARD":
return CardPayment()
if payment_type == "POINTS":
return PointsPayment()
raise ValueError("unsupported payment type")
processor = PaymentFactory().create("CARD")
print(processor.pay(100))
5) Abstract Factory Pattern
핵심: “관련된 객체 군(family)을 통째로 생성한다.”
언제 쓰나?
UI 컴포넌트처럼 “한 세트”로 교체되어야 하는 경우
예: Windows UI vs Mac UI, Dark Theme vs Light ThemeDB Driver 세트, Cloud Provider 세트(AWS/GCP/Azure)처럼
연관된 객체들이 일관되게 바뀌어야 할 때
Factory Method와 차이
Factory Method: “한 종류 객체 생성”
Abstract Factory: “연관된 여러 객체를 한 번에 생성”
면접에서.
“Abstract Factory는 서로 호환되는 객체 세트를 강제해서, 런타임 환경 전환(플랫폼/테마/벤더)을 안정적으로 만듭니다.”
인터뷰 문제 예시
“나라/리전에 따라 세금/통화/결제/배송 세트가 통째로 바뀐다.”
“마켓플레이스가 US/JP/KR로 확장될 때 설계를 어떻게 할까?”
Amazon에서 어디에 쓰나?
Region Pack
결제 게이트웨이 + 세금 계산기 + 통화 포맷터 + 배송 정책이 한 세트로 묶임
from abc import ABC, abstractmethod
class TaxCalculator(ABC):
@abstractmethod
def calc(self, subtotal: int) -> int: ...
class USTax(TaxCalculator):
def calc(self, subtotal: int) -> int: return int(subtotal * 0.08)
class JPTax(TaxCalculator):
def calc(self, subtotal: int) -> int: return int(subtotal * 0.10)
class RegionFactory(ABC):
@abstractmethod
def tax(self) -> TaxCalculator: ...
@abstractmethod
def currency_symbol(self) -> str: ...
class USFactory(RegionFactory):
def tax(self) -> TaxCalculator: return USTax()
def currency_symbol(self) -> str: return "$"
class JPFactory(RegionFactory):
def tax(self) -> TaxCalculator: return JPTax()
def currency_symbol(self) -> str: return "¥"
