[ 디자인 패턴 ]
디자인 패턴은 아키텍처 설계 수준보다
낮은 수준의 설계 문제에 대해,
재사용 가능한 솔루션을 제공한다.
중요한 포인트는 설계가
여러 수준에서 이루어진다는 점이다.
설계 문제와 목표는
추상화 수준마다 다르기 때문에,
한 수준에 적합한 패턴이 다른 레벨의
문제에는 맞지 않는 경우가 많다.
디자인 패턴이 적용되는 설계 수준
아키텍처 스타일, 디자인 패턴, 알고리즘/자료구조는
해결하려는 문제의 '수준'이 다르다.
아래 표처럼 구분해두면 지금 고민이
어떤 레벨의 설계 이슈인지 빠르게 정리된다.
| 설계 수준 | 주로 다루는 것 | 예시 |
| 최상위 설계 (아키텍처) |
시스템 전체 구조, 컴포넌트 분리와 연결 규칙 |
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 |