개주 훈련일지/📚 코살대 교본 학습

디자인 패턴

lshfood2 2026. 2. 11. 11:13

[ 디자인 패턴 ]

디자인 패턴은 아키텍처 설계 수준보다

낮은 수준의 설계 문제에 대해,

재사용 가능한 솔루션을 제공한다.

 

중요한 포인트는 설계가

여러 수준에서 이루어진다는 점이다.

 

설계 문제와 목표는

추상화 수준마다 다르기 때문에,

한 수준에 적합한 패턴이 다른 레벨의

문제에는 맞지 않는 경우가 많다.


디자인 패턴이 적용되는 설계 수준

아키텍처 스타일, 디자인 패턴, 알고리즘/자료구조는

해결하려는 문제의 '수준'이 다르다.

아래 표처럼 구분해두면 지금 고민이

어떤 레벨의 설계 이슈인지 빠르게 정리된다.

설계 수준 주로 다루는 것 예시
최상위 설계
(아키텍처)
시스템 전체 구조,
컴포넌트 분리와
연결 규칙
MVC 같은
아키텍처 스타일
중위 설계 컴포넌트 내부의
주요 책임 분해,
서브시스템 협업 구조
모델-뷰-컨트롤러
역할 분해,
경계 정의
하위 설계
(디자인 패턴)
컴포넌트 내부
클래스들의 역할/협업,
반복되는 설계 이슈의 해법
옵서버, 어댑터,
데코레이터 등
알고리즘/자료구조 데이터 저장/탐색/처리의
구체적 방법
리스트/트리/해시,
탐색/정렬 등

 

간단 예로 보는 적용 레벨 구분

요구사항이 시스템은 재고 자료를

갱신하기 위한 컨트롤과,

재고 자료를 표시해야 한다라면,

사용자는 지시(입력)하고 결과를 본다.

 

즉 대화형 시스템 성격이 강하고,

이때 아키텍처 스타일로는 MVC가 잘 맞는다.

 

하지만 MVC를 구현하려고 들어가면

곧 하위 설계 문제가 나온다.

  • 뷰에 모델의 가장 최근
    변경 사항이 어떻게 반영될 것인가
  • 모델과 뷰를 느슨하게 연결하면서도
    변경을 전달하려면 어떻게 해야 하는가

이 지점이 디자인 패턴이 적용되는 수준이다.


MVC에서 모델-뷰 변경 반영의 대표적 2가지 설계 방향

요구를 만족시키는 설계는 크게

아래 2가지 방향으로 정리할 수 있다.

 

솔루션 선택지

선택지 1) 옵서버 패턴 기반
모델(Subject) 상태 변경
  -> notifyObservers()
     -> 뷰(Observer) update()
        -> 필요 시 모델 getState()로 최신 값 조회
        -> 화면 갱신

선택지 2) 변경 시작점이 컨트롤러인 경우
컨트롤러가 모델 변경 수행
  -> 모델 상태 변경
  -> 컨트롤러가 뷰에 갱신 요청(또는 모델 변경 이벤트를 전달)
  -> 뷰 화면 갱신

핵심은, 아키텍처(MVC)를 확정한 뒤

컴포넌트 내부에서 클래스 역할과 동작을

결정하는 과정에서 생기는 '설계 이슈'를

해결하는 해법이 디자인 패턴이라는 점이다.

 

디자인 패턴의 혜택

혜택 요지
쉽게 재사용 가능 검증된 설계 솔루션을
여러 응용 프로그램에서
재사용할 수 있게 한다
설계 작업이 쉬워짐 이미 알려진 고품질 해법이 있어
설계 이슈 해결이 빨라진다
(상황에 맞춘 커스텀은 필요)
설계 관련 지식이 정리됨 분석/설계 경험을 체계화해
숙련된 설계 판단을 돕는다
의사소통이 쉬워짐 추상화 레벨의 공통 어휘를 제공해
설계 논의를 정확하게 만든다
객체지향 설계 원리를
잘 따르게 됨
캡슐화, 응집, 추상화와 SOLID 원칙을
잘 지킨 모범 사례로 작동한다

 

디자인 패턴을 설명하는 기본 형식

항목 의미
패턴 이름 짧은 이름과 설명으로
의사소통을 촉진
소개 배경과 학습 동기 제공
해결하는 문제 어떤 설계 이슈를 다루는지
솔루션 실행 코드보다 추상적인
설계 해법(클래스/상호작용 구조 포함)
예제 적용 방법 이해를 돕는 사례
관련 패턴 유사/포함/비교 대상 패턴과의 관계

아래부터는 제공된 범위의 패턴들을

같은 형식으로 정리한다.


1. 싱글톤 패턴

목적: 객체를 강제적으로 하나만 생성하고,

모두가 같은 인스턴스를 공유하게 만든다.

 

해결하는 문제

  • 어떤 클래스 타입에 대해 생성되는
    객체 수를 제한해야 한다.
  • 단일 액세스 지점이 필요하다.

솔루션

  • 클래스 내부에 자신을
    정적(static) 속성으로 보관한다.
  • 유일 객체를 반환하는
    정적 메서드를 제공한다.
  • 생성자를 private으로 두어
    외부 생성을 차단한다.

구성 요소 표

구성 요소 역할
private static instance 유일 인스턴스 저장
private constructor 외부 new 차단
public static instance() 유일 인스턴스 제공
(필요 시 최초 생성)

 

흐름도

클라이언트 -> instance() 호출
  instance가 null이면 생성
  생성된 instance 반환
다음 호출부터는 기존 instance 반환

 

관련 패턴

  • 추상 팩토리와 함께 사용되어 '팩토리 자체'가
    최대 하나만 생성되도록 보장할 수 있다.
  • 상태 패턴에서도 상태 객체가 전환될 때
    재생성을 막기 위해 사용할 수 있다.

2. 반복자 패턴

- 목적
집합(컨테이너)의 내부 자료구조와 무관하게

요소에 순차 접근하는 방법을 제공한다.

 

해결하려는 문제

  • 벡터/트리/리스트 등 자료구조가 달라도,
    클라이언트는 같은 방식으로 순회하고 싶다.
  • 반복/집계를 구현하는 방법이 바뀌어도
    클라이언트 코드는 영향이 없어야 한다.

솔루션(구현 절차 요약)

  • Iterator 인터페이스를 정의한다.
    (예: getFirst, getNext, hasNext)
  • Aggregate 인터페이스를 정의하고,
    반복자를 반환하는 팩토리 메서드를 둔다.
    (예: createIterator)
  • ConcreteAggregate/ConcreteIterator가
    실제 순회 로직을 구현한다.
  • 클라이언트는 Iterator/Aggregate만 사용한다.

텍스트 UML

Iterator(인터페이스) <--- ConcreteIterator
Aggregate(인터페이스, createIterator) <--- ConcreteAggregate
Client -> Aggregate.createIterator() -> Iterator로 순회

 

핵심 효과 표

효과 설명
접근 방식 통일 자료구조별
순회 알고리즘을 숨긴다
결합도 감소 클라이언트는
집합 내부 구현을 몰라도 된다
변경 용이 순회 방식 변경이
클라이언트에 영향 주지 않는다

 

사례(개념 연결)

  • Java 컬렉션에서 Collection과
    iterator() 개념이 대표적 예로 연결된다.

3. 어댑터 패턴

- 목적

인터페이스가 맞지 않는 클라이언트와 서비스를

함께 작동시키기 위해, 인터페이스를 변환한다.

 

해결하는 문제

  • 클라이언트가 기대하는 인터페이스와
    서비스가 제공하는 인터페이스가 다르다.
  • 기존 컴포넌트를 수정하지 않고
    호환되게 만들고 싶다.

솔루션

  • 클라이언트가 기대하는
    인터페이스(Target)를 구현하는
    Adapter를 만든다.
  • Adapter 내부에
    실제 서비스(Adaptee)를 보관하고,
    호출을 위임해 변환한다.

텍스트 UML

Client -> Target(클라이언트 기대 인터페이스)
Adapter implements Target
Adapter has Adaptee(서비스)
Target.method() -> Adapter.method() -> Adaptee.serviceMethod()

 

핵심 비교 표

항목 내용
초점 호환성(인터페이스 변환)
효과 클라이언트-서비스를
느슨하게 연결
구현 Adapter가 Target 구현
+ Adaptee 위임

관련 패턴

퍼사드와 비슷해 보일 수 있으나,
퍼사드는 새 인터페이스 층을 제공해
'더 단순한 사용'을 만들고,

 

어댑터는 불일치 인터페이스를
'변환'하는 데 초점이 있다.


4. 데코레이터 패턴

- 목적

기존 클래스의 동작을 '가볍고 유연하게' 확장한다.

상속 대신 구성(composition)과 위임을 사용한다.

 

왜 필요한가(대안들의 한계 정리)

  • 수정으로 기능 추가
    기존 클래스를 바꿔야 하므로 OCP를 위배한다.
    (확장에는 열려 있고, 수정에는 닫혀야 함)
  • 상속으로 기능 추가
    기본 클래스의 보호된 인터페이스가
    노출되어 캡슐화가 약해질 수 있다.
    또한 기능 조합 수만큼 서브클래스가 폭증하고,
    런타임에 기능을 추가/제거하기 어렵다.

문제 상황을 표로 정리

방법 장점 핵심 단점
기존 클래스 수정 단순함 OCP 위배, 변경 영향 큼
상속 기존 코드 수정 없이
확장 가능
캡슐화 약화, 조합 폭증,
런타임 유연성 부족
구성 + 데코레이터 런타임에 기능
추가/제거 가능
구조가 약간
복잡해질 수 있음

해결하는 문제

  • 기본 클래스 동작을 동적으로 추가하고 싶다.
  • 상속처럼 컴파일 타임에 확정되는 확장이 아니라,
    실행 중에 기능을 조합하고 싶다.

솔루션

  • Component 인터페이스(또는 추상 클래스)를
    정의한다(operation 같은 공용 동작).
  • ConcreteComponent가 기본 기능을 제공한다.
  • Decorator는 Component를 구성 관계로 보관하고,
    동일한 인터페이스를 구현한다.
  • ConcreteDecorator들이
    추가 기능(addBehavior)을 끼워 넣는다.
  • Decorator 체인으로 여러 기능을
    얼마든지 래핑할 수 있다.

텍스트 UML

Component(operation)
  ^ 
  |-- ConcreteComponent
  |
  |-- Decorator(Component를 보관, operation 위임)
        ^
        |-- ConcreteDecorator1(addBehavior)
        |-- ConcreteDecorator2(addBehavior)

Client는 Component 타입으로만 사용

 

동작 흐름도(체인 호출)

Client -> Decorator2.operation()
  -> (추가 동작) addBehavior2
  -> Decorator1.operation()
       -> (추가 동작) addBehavior1
       -> ConcreteComponent.operation()

 

사례(개념 연결)

  • Java의 java.io 입력 스트림에서,
    기본 스트림을 여러 데코레이터로 감싸
    기능을 조합하는 방식이 대표적 예로 연결된다.

관련 패턴

  • 어댑터는 '다른 인터페이스'를
    구현해 호환성을 만든다.
  • 데코레이터는 '같은 인터페이스'를
    유지하면서 동작을 확장한다.
  • 정리하면 데코레이터의 초점은 확장,
    어댑터의 초점은 호환성이다.

5. 팩토리 메서드 패턴

- 목적

객체 생성 책임을 분리해,

어떤 구체 클래스가 생성될지 변화해도

클라이언트가 흔들리지 않게 한다.

 

해결하려는 문제

  • 필요한 클래스의 유형은 알지만,
    구체 클래스까지는 모르거나
    신경 쓰고 싶지 않다(특히 프레임워크 맥락).
  • 객체 생성 변화에 대비하고 싶다.

솔루션

  • 생성될 product의 추상 인터페이스를 정의한다.
  • createProduct 같은 팩토리 메서드를 가진
    추상 클래스(AbstractCreator)를 둔다.
  • 구체 생성(ConcreteCreator)이 팩토리 메서드를
    구현해 구체 객체를 만든다.
  • 클라이언트는 Creator/Product의
    추상에 기대어 사용한다.

텍스트 UML

Product <--- ConcreteProduct
AbstractCreator(createProduct)
  ^
  |-- ConcreteCreator(createProduct가 ConcreteProduct 생성)
Client -> AbstractCreator의 createProduct 사용

 

사례(개념 연결)

  • 데스크탑 애플리케이션 프레임워크에서
    Application/Document 같은 추상 클래스가 있고,
    실제 응용 프로그램이 하위 클래스로
    구체 Document를 결정하는 구조에 연결된다.

6. 추상 팩토리 패턴

- 목적

관련 객체 '패밀리'를 생성하는 책임을

클라이언트 밖으로 옮겨 일관된 제품군을

유연하게 만들게 한다.

 

팩토리 메서드와 차이

  • 팩토리 메서드
    한 종류 product를 생성하는 방식 중심
  • 추상 팩토리
    서로 관련된 여러 product를
    한 세트(패밀리)로 생성하는 방식 중심

해결하려는 문제

  • 클라이언트가 구체 객체 생성을 지정하지 않으면서도,
    관련 객체 패밀리를 일관되게 만들고 싶다.

솔루션

  • Product1, Product2 등 각 Product에 대한
    추상 인터페이스를 정의한다.
  • AbstractFactory에 createProduct1,
    createProduct2 같은 생성 메서드를 둔다.
  • ConcreteFactoryFamilyA/B가
    각 패밀리에 맞는 ConcreteProduct들을 만든다.
  • 클라이언트는 추상 인터페이스들에 밀접하고,
    구체 제품/구체 팩토리에는 느슨하게 연결된다.

텍스트 UML

AbstractProduct1 <--- ConcreteProduct1_FamilyA / FamilyB
AbstractProduct2 <--- ConcreteProduct2_FamilyA / FamilyB

AbstractFactory(createProduct1, createProduct2)
  ^ 
  |-- ConcreteFactoryFamilyA
  |-- ConcreteFactoryFamilyB

Client -> AbstractFactory만 사용

 

사례(개념 연결)

  • 음악 재생에서 Player, Media 같은 구성 요소가
    함께 맞물려야 할 때 패밀리(디지털/아날로그)에
    맞는 부품을 세트로 생성하는 구조로 연결된다.

7. 상태 패턴

- 목적

객체의 내부 상태에 따라 동작이 바뀌는 경우,

조건문(if-else, switch) 폭발을 피하고

상태별 동작을 클래스로 분리한다.

 

해결하는 문제

  • 상태 변수 + 조건문으로 상태별 동작을 처리하면,
    상태/전환이 늘수록 코드가 장황해지고 유지가 어렵다.
  • 상태 전환 로직 변경도 조건문 수정으로
    이어져 영향이 크다.
  • 가능한 모든 상태와 전환을
    초기 설계에서 완벽히 예측하기도 어렵다.

솔루션

  • 가능한 모든 상태에 대해 상태 클래스를 만들고,
    상태별 동작을 그 클래스 안으로 몰아둔다.
  • Context는 현재 상태(State 객체) 참조만 보관한다.
  • Context는 상태 관련 작업을 State에게 위임한다.
  • 상태 전환은 활성 상태 객체를
    다른 상태 객체로 바꾸는 것으로 수행한다.
  • 모든 상태 클래스는 동일한 인터페이스(State)를 따른다.

텍스트 UML

Context has State
State(인터페이스: doThis, doThat 등)
  ^
  |-- ConcreteStateA
  |-- ConcreteStateB

Client -> Context.doThis()
Context -> state.doThis()로 위임
필요 시 Context.changeState(newState)

 

사례(문서 출판 흐름을 상태로 분리)

상태 publish 동작
Draft(초안) Moderation(검토)로 전환
Moderation(검토) 관리자인 경우 Publish로 전환
Publish(게시) 아무 것도 하지 않음

 

관련 패턴

  • 구조가 전략 패턴과 비슷해 보일 수 있지만,
    상태 패턴은 상태들이 서로를 인식하고
    상태 전환을 구동할 수 있다는 점이 중요한 차이다.
    전략 패턴은 서로에 대해 거의 알지 못한다.

8. 옵서버 패턴

- 목적

데이터(Subject)와 이를 사용하는 뷰/구독자(Observer)를
느슨하게 결합하면서 변경 사항을 효과적으로 전달한다.

 

문제 상황

  • 데이터가 바뀌면, 그 데이터에 관심 있는
    모든 뷰(원형 차트, 막대 차트 등)도 갱신되어야 한다.
  • Subject는 '구체적으로 어떤 뷰인지'까지는 알 필요가 없고,
    상태 변경 시 알림을 보내야 할 옵서버 목록만 알면 된다.
  • 이 느슨한 결합을 제공하는 방법이 필요하다.

해결하는 문제

  • Subject가 옵서버들과 효과적으로 통신하면서도
    느슨하게 결합하려면 어떻게 해야 하는가
  • 옵서버가 매번 변경을 체크(polling)하지 않고,
    Subject가 변경 시 통지(push)해
    결합을 느슨하게 만들고 싶다
  • 옵서버는 Subject나 다른 옵서버에
    영향 없이 추가/변경 가능해야 한다

솔루션

  • Subject는 옵서버 목록을 유지한다.
  • 상태 변경 시 notifyObservers()로 알린다.
  • Observer는 콜백 메서드 update()를 구현한다.
  • 옵서버가 통지 후 변경된 상태를 가져오기 위한
    getState() 같은 메서드를 Subject가 제공할 수 있다.

텍스트 UML

Subject
  - addObserver(o)
  - deleteObserver(o)
  - notifyObservers()

Observer(인터페이스)
  - update()

ConcreteSubject(getState, modifyState 등)
ConcreteObserver(update 구현)

 

동적 흐름도(구독과 갱신)

1) 구독
Observer -> Subject.addObserver(this)

2) 데이터 변경
어떤 동작으로든 Subject 상태 변경(modifyState)

3) 통지
Subject -> notifyObservers()

4) 갱신
Observer.update()
  -> 필요 시 Subject.getState()로 최신 값 조회

 

사례(뉴스 대행사)

  • NewsPublisher가 구독자 목록을 유지하고,
    새 뉴스 발생 시 구독자에게 통지한다.
  • 구독자는 EmailSubscriber, SMSSubscriber 등
    다양한 형태로 확장 가능하다.
  • 새 통신 기술이 나오면 새로운 Subscriber를
    추가할 수 있어야 하고 날씨/스포츠 같은
    다른 Publisher 유형도 확장 가능해야 한다.

패턴 빠른 비교 표

패턴 핵심 목표 핵심 구조 키워드
싱글톤 인스턴스 1개 보장 private 생성자,
static instance,
단일 접근점
반복자 자료구조와 무관한 순회 Iterator/Aggregate 분리,
순회 로직 캡슐화
어댑터 인터페이스 호환 Target 구현 + Adaptee 위임
데코레이터 동적 기능 확장 동일 인터페이스 유지,
구성/위임, 체인 래핑
팩토리 메서드 생성 책임 분리 Creator의 createProduct,
추상에 의존
추상 팩토리 제품군(패밀리) 생성 Factory가 여러 Product를
세트로 생성
상태 상태별 동작 분리 Context가 State 위임,
상태 객체 교체로 전환
옵서버 변경 통지, 느슨한 결합 Subject 목록/notify,
Observer update 콜백

[ 마무리 정리 ]

디자인 패턴은 요구사항을 바탕으로 아키텍처를 확정한 뒤,

그 아키텍처 컴포넌트 내부를 구현하는 단계에서 등장하는

설계 이슈에 대한 해법이다.

 

즉 컴포넌트 안의 클래스 역할과 동작이

구체화될 때 생기는 반복 문제를,

재사용 가능한 구조로 해결해준다.

'개주 훈련일지 > 📚 코살대 교본 학습' 카테고리의 다른 글

UI 설계와 기본 개념  (0) 2026.02.16
아키텍처 평가  (0) 2026.02.13
아키텍처 스타일  (0) 2026.02.10
SQL) DCL (Data Control Language)  (0) 2026.02.09
아키텍처 기초  (0) 2026.02.09