[CS] 디자인 패턴
본 게시글은 [면접을 위한 CS 전공지식 노트]를 기반으로 개인 학습 내용을 정리한 글입니다.
디자인 패턴
프로그램을 설계할때 반복적으로 발생했던 문제점들을 효율/효과적으로 해결하기 위한 하나의 “규약” 형태로 만들어 놓은 것
= 재사용 가능한 설계 지침/방법
<주요 특징>
- 문제 해결의 모범 사례 : 다양한 개발 현장에서 검증된 문제 해결 전략 (동일 문제가 반복될 때 일관적으로 사용 가능)
- 코드 재사용성 : 같은 설계 패턴을 여러 프로젝트에서 반복 사용가능
- 코드 가독성/확장성/유지보수성 향상 : 공통된 구조/규약으로 코드의 이해와 확장 쉬워짐
- 버그 감소
싱글톤 패턴
하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
프로그램 전체에서 이 인스턴스에 전역적으로 접근 가능 보장
ex) 보통 DB 연결 모듈에 많이 사용
<장점>
- 어디서든 동일한 객체 참조 가능
- 메모리 절약 : 여러 객체 생성이 필요없음
- 데이터 일관성
- 전역 접근성
<단점>
- 의존성 ⬆️ : 모듈 간의 결합을 강하게 만듦
- 의존성 주입(DI, Dependency Injection)으로 결합 느슨하게 만들기
(단, 상위모듈은 하위 모듈에서 어떠한 것도 가져오지 않기, 둘다 추상화에 의존) - 메인 모듈이 직접 다른 하위 모듈에 의존성을 주지않고, 의존성 주입자가 간접적으로 의존성 주입
- 장점 : 모듈 쉽게 교체 가능, 테스팅 하기 쉬움, 마이그레이션하기 쉬움
- 단점 : 클래스 수가 늘어나 복잡성 증가, 약간의 런타임 페널티
- 자세한 설명
- 의존성 주입(DI, Dependency Injection)으로 결합 느슨하게 만들기
- TDD(Test Driven Development) 할 때 걸림돌
- TDD : 단위 테스트는 테스트끼리 독립적, 어떤 순서로도 실행 가능
- 자세한 설명
싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 하기 때문에,
각 테스트마다 ‘독립적인’ 인스턴스를 만들기 어려움
싱글톤 아닐 때
1
2
3
4
5
6
class DB {
constructor() {
console.log("DB 연결 중...");
this.conn = connectToDatabase(); // 매우 비싼 작업
}
}
싱글톤일 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DB {
constructor() {
if (!DB.instance) {
console.log("📡 DB 연결");
DB.instance = this; // 연결은 한 번만
}
return DB.instance;
}
}
const a = new DB(); // 📡 DB 연결
const b = new DB(); // (연결 안 함)
console.log(a === b); // true
=> 한 번만 생성하고, 그 하나만 계속 써서 자원을 아낀다!!
팩토리 패턴
객체 생성 로직을 별도의 “팩토리” 클래스로 분리해 클라이언트 코드가 구체적인 클래스(객체 생성 방식)에 직접 의존하지 않도록 만드는 디자인 패턴
상위 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정
하위클래스에서 객체 생성에 관한 구체적인 내용 결정 패턴
목적 : 클라이언트는 어떤 구체 클래스가 생성되는지 몰라도 팩토리에 요청만 하면됨, 코드 변경시 팩토리만 수정/확장하면 됨
[구조]
1
2
Client → IProduct ← Factory → ConcreteProductA
→ ConcreteProductB
- Product 인터페이스: 생성될 객체들이 구현해야 할 공통 인터페이스 또는 추상 클래스
- ConcreteProduct: Product 인터페이스를 구현한 구체 클래스들
- Factory 인터페이스/추상 클래스: Product 객체를 생성하는 메서드(팩토리 메서드)를 선언
- ConcreteFactory: Factory를 구현/상속하여 실제 ConcreteProduct 인스턴스를 반환
<장점>
- 모듈 간의 느슨한 결합, 더 많은 유연성, 의존성 감소
- 객체 생성 로직이 따로 있기 때문에 코드 리팩터링 시 한 곳만 고쳐도 됨, 유지 보수성 ⬆️
<단점>
- 설계가 복잡해질 수 있음
- 팩토리 클래스/메서드 추가 필요로 개발 비용 즈가
예시)
- CoffeFactory 상위 클래스가 중요 뼈대를 결정하고, 하위 클래스인 LatteFactory/EspressoFactory가 구체적인 내용을 결정하는 경우
- 팩토리들의 팩토리 방식
- 각 커피마다 전용 팩토리 클래스가 있고, 중앙 팩토리는 레지스트리(map)로 어떤 팩토리를 쓸지 결정
- 새 커피 추가 시 새 팩토리 클래스를 추가하고 레지스트리에 둥록 -> 중앙 로직 수정 최소화
- 클라이언트는 CoffeFactory.createCoffe(type)만 알고, 실제 생성은 다형성으로 위임
- 런타임에 타입을 추가/제거하기 좋음
- CoffeFactory밑에 Coffe클래스를 놓고, 해당 클래스를 상속하는 LatteFactory/EspressoFactory 클래스로 구현하는 경우
- Simple Factory(정적 팩토리)
- 하나의 팩토리 메서드가 switch/enum으로 바로 제품 생성
- 새 커피 추가시 switch/팩토리 메서드 수정 필요(중앙 로직 변경)
- 클라이언트는 CoffeFactory.createCoffe(type)만 알고, 내부는 조건문 의존
- 타입 수 적고 생성 로직 단순할 때 가장 간단, enum으로 타입 안전성 확보
전략 패턴
strategy pattern = 정책 패턴(policy pattern)
객체의 행위를 바꾸고 싶을때, 행위(알고리즘)=전략이라는 객체로 분리(캡슐화)해 동적으로 교체 가능한 패턴
목적 : 동일 계열의 알고리즘을 인터페이스로 추상화하고, 이를 구현한 여러 개별 전략 클래스를 만들어 런타임에 객체 행위를 바꿀 수 있음
[구조]
1
2
3
Client → Context ─ uses → Strategy(인터페이스)
├─ ConcreteStrategyA
└─ ConcreteStrategyB
- Strategy: 공통 메서드 시그니처
- ConcreteStrategy: 실제 알고리즘들
- Context: 전략을 보유하고 위임. 필요하면 중간 데이터 관리
<장점>
- 코드 유연성/확장성 향상 : 새로운 알고리즘(전략, 행위) 추가가 쉬움
- 클라이언트 코드와 행위 구현을 분리해 결합도 감소
- 런타임에 동적으로 행위 변경 가능
<단점>
- 전략(알고리즘) 클래스가 많아질 수 있음
- 컨텍스트(개발자가 어떤 작업을 완료하는데 필요한 모든 관련 정보)가 전략 객체 관리를 별도로 해야함
옵저버 패턴
주체가 한 객체의 상태 변화가 있을때, 그 객체에 등록된 여러 옵저버(객체)들에게 자동으로 전파하는 패턴
ex) 주로 이벤트 기반 시스템에 사용, MVC패턴에도 사용됨
[구조]
- Subject: 관찰자 목록을 관리하고, 상태가 바뀌면 전부에 notify한다.
- Observer: update(…) 같은 메서드를 구현해 알림을 받는다.
-> 결합도↓: Subject는 “누가 듣는지”를 몰라도 되고, Observer는 “누가 발화하는지”를 몰라도 됨.
[동작 흐름]
- Observer가 Subject에 구독(register/subscribe)
- Subject 상태 변경
- Subject가 notify 호출 → 모든 Observer의 update(…) 실행
- Observer는 알림(payload)이나 Subject에서 상태를 pull해서 처리
예시) 트위터
- 내(팔로워)가 어떤 사람인 주체를 팔로우
- 주체가 포스팅 올림
- 팔로워에게 알림이 감
<장점>
- 주체와 옵저버 간의 결합도 낮음, 확장성 유지보수성 좋음
- 새로운 옵저버 추가에 용이
<단점>
- 옵저버가 많아질수록 알림 처리 시 성능 저하 가능성
- 복잡한 의존 관계가 늘어날 경우 관리 어려움
- 멀티스레드 환경에선 notify 시 동기화 필요
상속 vs 구현
상속 : 자식 클래스가 부모 클래스의 메서드 등을 상속 받아 사용, 자식 클래스에서 추가 및 확장구현 : 부모 인터페이스를 자식클래스에서 재정의해 구현
프록시 패턴과 프록시 서버
프록시 패턴
객체 지향 프로그래밍에서 대상 객체에 대한 접근을 제어하거나 기능을 추가하기 위해, 똑같은 인터페이스를 가진 ‘대리 객체(proxy)’를 중간에 두는 디자인 패턴
프록시 객체 : 어떤 대상의 기본적인 동작의 작업을 가로챌 수 있는 객체
-> 클라이언트는 진짜 객체와 같은 인터페이스만 보고 사용, 프록시가 접근 제어/지연 로딩/캐싱/로깅 같은 부가 기능 끼워 넣음
목적 : 실제 객체에 대한 접근 통제, 객체 생성 지연(무거운 리소스를 필요할 때만 생성), 추가 기능 주입
[구조]
1
2
3
Client → Subject(인터페이스)
├─ RealSubject (실제 기능)
└─ Proxy (대리인; RealSubject에 위임 + 부가 기능)
프록시 서버에서의 캐싱
캐시 안에 있는 정보를 요구하는 요청에 대해 원격 서버에 요청하지 않고, 캐시 안에 있는 데이터를 활용하는 것 (불필요한 외부와의 연결 없음, 트래픽 감소)프록시 서버
클라이언트와 서버 사이에 위치해 트래픽을 중개하는 서버
클라이언트는 네트워크 서비스에 간접적으로 접속
보안/캐싱/로드밸런싱/익명화/정책 적용 등을 담당
[구조]
- 사용자가 웹사이트에 접속하면, 요청이 프록시 서버로 먼저 전달됨
- 프록시 서버가 사용자 대신 목적지 서버에 요청
- 목적지 서버가 프록시 서버에 응답 → 프록시 서버가 사용자에게 전달
[종류]
- 포워드 프록시(Forward Proxy): 내부 사용자가 인터넷 자원 접근 시 프록시를 경유
- 리버스 프록시(Reverse Proxy): 외부 사용자가 내부 서버에 접근 시 요청을 받아 대신 전달(보안·부하분산 목적)
대표 기능)
- 중계 및 우회: 네트워크 우회, 차단 사이트 접근
- 캐싱: 자주 요청되는 데이터 임시 저장, 응답 속도 향상, 트래픽 절감
- 보안: 내부 네트워크 IP 은닉, 접근 제어 및 로깅, 악성 사이트 차단, DDoS 완화
- 익명성 제공: 내부 서버 IP/토폴로지 노출 방지
| 항목 | 프록시 패턴 | 프록시 서버 |
|---|---|---|
| 영역 | 애플리케이션 코드 레벨 | 네트워크/인프라 레벨 |
| 목적 | 객체 접근을 대리하며 부가 로직 삽입 | 트래픽을 중개하며 보안·성능·확장성 제공 |
| 형태 | 클래스/객체(인터페이스 동일 유지) | 서버 프로세스/장비(Nginx, Envoy, Squid 등) |
| 예 | 지연 로딩, 접근 제어, 캐싱, 로깅 | 로드밸런싱, SSL 종료, CDN 캐시, WAF |
이터레이터 패턴
컬렉션의 내부 구조를 몰라도, 이터레이터(인터페이스)를 사용해 컬렉션의 요소를 접근(순회)하는 디자인 패턴
탐색 로직(다음 원소가 뭐냐, 끝났냐 등)을 컬렉션 바깥의 이터레이터 객체로 분리
[구조]
1
2
3
4
5
6
7
Client → Iterator (hasNext, next[, remove])
↑
ConcreteIterator
↑
Aggregate (createIterator)
↑
ConcreteAggregate (컬렉션)
- Aggregate: 이터레이터를 만들어 주는 공장 역할
- Iterator: hasNext(), next() 같은 공통 인터페이스
- ConcreteIterator: 실제 순회 상태(현재 인덱스 등)를 가짐
- Client: 이터레이터만 보고 순회
이터레이터 프로토콜 : 이터러블한 객체들을 순회할 때 쓰이는 규칙
이터러블한 객체 : 반복 가능한 객체로 배열을 일반화한 객체
<장점>
- 컬렉션 내부 구조 은닉: 클라이언트는 동일한 인터페이스로 순회
- 여러 순회 전략을 독립적으로 추가 가능 (역순, 필터링, BFS/DFS 등)
- 순회 상태가 이터레이터에 캡슐화
<단점>
- 이터레이터/클래스 수 증가
- 동시 수정 시 주의 필요(예: Java의
fail-fastConcurrentModificationException) - 외부 반복(external iteration)은 비동기·병렬 처리가 다소 번거로울 수 있음
외부 반복 vs 내부 반복
- 외부 반복: 클라이언트가 while (it.hasNext())로 제어 유연하지만 코드가 장황해질 수 있음
- 내부 반복: 컬렉션/프레임워크가 콜백으로 돌려줌(map/filter/forEach, Python 제너레이터), 병렬화·최적화에 유리
노출 모듈 패턴
Revealing Module Pattern
즉시 실행 함수(IIFE)를 통해 모듈 내부의 변수와 함수를 클로저로 감싸서, public과 private 같은 접근 제어자를 만드는 패턴
내부 구현은 감추고, 외부에는 깔끔한 public API만 보여줌
-> 간단한 퍼블릭 API와 감춰야할 내부상태가 문명할때,
전역 오염을 피하면서 특정 기능을 하나의 네임스페이스로 묶고 싶을때,
공개 API를 한곳에서 관리하고 리팩터링 가독성을 높이고 싶을떄 사용
<장점>
- 코드 응집성 높임, 정보은닉으로 유지보수성/안전성 향상
- 즉시 실행 함수를 사용으로 전역 네임스페이스 오염 방지, 모듈간의 변수 충돌 문제 줄여줌
<단점>
- IIFE로 만들면 기본적으로 싱글톤임. 인스턴스 여러개가 필요하면 팩토리 함수로 바꿔야함
- 내부 상태가 클로저에 잡혀 메모리 누수가 날 수 있음, 이벤트 리스너 등은 해제 관리 필요
- 내부 구현을 바깥에서 주입/핫패치하기 어려움
- 공개 객체에 내부의 변경 가능한 참조를 직접 노출 시 캡슐화 깨질 수 있음
protected : 클래스에 정의된 함수에서 접근 가능, 자식 클래스에서 접근 가능하지만 외부 클래스에서 접근 불가능
즉시 실행 함수 : 함수를 정의하자마자 바로 호출하는 함수
클로저 : 함수가 선언될 당시의 환경(스코프)을 기억하고, 그 환경 안의 변수에 접근할 수 있는 함수
핫패치 : 프로그램을 종료하거나 재시작하지 않고, 실행 중인 상태에서 코드나 동작을 수정하는 것
1
2
3
4
5
6
7
8
9
10
function makeCounter() {
let count = 0; // 외부 함수 변수(은닉)
return function () { // 내부 함수(클로저)
count++;
return count;
};
}
const counter = makeCounter(); // counter = 클로저
console.log(counter()); // 1
MVC 패턴
모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴
->화면/인페이스 바뀌어도 도메인 로직은 그대로일 때,
팀의 역할을 나눠 병렬 개발 해야할 때, 사용
| 구성 요소 | 역할 |
|---|---|
| Model | 데이터 관리, 비즈니스 로직 처리(뷰/컨트롤러 정보 없음) |
| View | 사용자 인터페이스 표시, 사용자 입력 수집(사용자 입력을 컨트롤러에 전달) |
| Controller | 사용자 요청 처리, Model과 View 연결 및 중재 역할 |
[동작 흐름]
- 사용자가 View를 통해 입력(요청)을 하면, 이 요청이 Controller에 전달됩니다.
- Controller는 요청을 처리하기 위해 Model을 호출하거나 데이터를 조작합니다.
- Model이 데이터를 처리하거나 업데이트하고, 그 결과를 Controller에 반환합니다.
- Controller는 Model의 결과를 View에 전달해 화면을 업데이트합니다.
<장점>
- 역할이 분리되어 테스트와 교체 쉬움, 팀 협업에 유리, 규모가 커져도 무너지지 않음
- 재사용성, 확장성 용이
<단점>
- 애플리케이션이 복잡해질수록 모델과 뷰 관계가 복잡해짐
- 작은 프로젝트에 과할 수도
- 컨트롤러에 비즈니스 로직이 섞이기 쉬워 설계원칙을 꾸준히 지켜야함
MVP 패턴
모델(Model), 뷰(View), 프레젠터(Presenter)로 이루어진 디자인 패턴
뷰와 프레젠터는 일대일 관계이기 때문에, MVC 패턴보다 더 강한 결합
| 구성 요소 | 핵심 역할 | 특징 |
|---|---|---|
| Model | 데이터·비즈니스 로직 처리 | View와 분리 |
| View | UI 표시, 사용자 입력 전달 | 비즈니스 로직 없음 |
| Presenter | 입력 해석, Model·View 중재 | 주로 로직 담당, 1:1 |
[동작 흐름]
- 사용자가 UI(View)에서 입력(이벤트)을 발생
- View가 이 입력을 Presenter에 전달
- Presenter가 비즈니스 로직에 따라 Model에 작업 요청
- Model은 데이터 처리를 수행
- Presenter가 Model의 결과를 받아 View에 화면 업데이트 요청
- View가 화면에 결과를 표시
<장점>
- View와 Model이 Presenter를 통해서만 연결되어 서로 직접 참조하지 않기 때문에 결합도가 낮아짐
- 유지보수성·테스트 용이성이 높음(각 구성요소를 독립적으로 테스트 가능)
- UI 로직과 비즈니스 로직이 명확히 분리됨
<단점>
- View와 Presenter가 대부분 1:1로 동작해 코드량이 MVC에 비해 많아질 수 있음.
- 앱 규모가 크면 Presenter가 비대해질 위험이 있음.
MVVM 패턴
모델(Model), 뷰(View), 뷰모델(View Model)로 이루어진 디자인 패턴
뷰를 더 추상화한 계층, 커맨드와 데이터 바인딩을 가짐
뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원, UI를 별도의 코드 수정 없이 재사용 가능
[동작 흐름]
- 사용자가 View에서 액션을 취함
- View는 데이터를 ViewModel에 전달(바인딩)
- ViewModel은 데이터를 Model에 요청하거나 업데이트
- Model에서 변경된 데이터를 ViewModel이 받아서 가공
- ViewModel의 데이터가 바뀌면 데이터 바인딩을 통해 View가 자동으로 갱신됨
<장점>
- UI와 로직 분리로 유지보수 용이
- View는 단순, ViewModel은 테스트 쉬움, 코드 재사용성과 테스트성 우수
- 대규모 앱 구조화에 적합
- 중복 로직 제거
<단점>
- 데이터 바인딩, Rx 등 추가 학습 필요
- 바인딩 많아지면 추적/디버깅 어려움
- 설계 구조가 복잡해질 수 있음
- ViewModel이 비대해지거나 메모리 사용 증가 소지
MVC vs MVP vs MVVM
| 구분 | MVC (Model-View-Controller) | MVP (Model-View-Presenter) | MVVM (Model-View-ViewModel) |
|---|---|---|---|
| 주요 구성요소 | Model, View, Controller | Model, View, Presenter | Model, View, ViewModel |
| 역할 분리 | Controller가 View와 Model 중재 (View와 Controller 간 상호참조 가능) | Presenter가 View와 Model 완전 중재 (View는 인터페이스로 Presenter만 참조) | ViewModel이 View와 Model 중재. View와 ViewModel은 데이터 바인딩으로 연결됨. ViewModel은 View를 알지 못함. |
| 데이터/이벤트 흐름 | View → Controller → Model, Model 변경 시 View 갱신 | View → Presenter → Model, Presenter가 결과를 View에 전달 | View ↔ ViewModel(데이터 바인딩), ViewModel이 Model과 직접 통신 |
| 결합도 | View와 Controller 간 결합 발생 가능 | View와 Presenter 간 인터페이스로 약간의 결합 | View와 ViewModel은 데이터 바인딩으로 결합 감소, 구조적 분리 향상 |
| 테스트 용이성 | 보통, Controller/Model 테스트 | 우수, Presenter/Model 단위테스트 쉬움 | 우수, ViewModel/Model 단위테스트 용이 |
| UI 업데이트 | Controller(또는 Observer)에서 View 갱신 | Presenter가 View에 명시적으로 알림 | 데이터 바인딩으로 View 자동 갱신, 코드량 감소 |
| 대표 적용 사례 | Spring MVC, ASP.NET MVC 등 | Android, WinForms 등 | WPF, Xamarin, Angular, React(+MobX), Vue.js 등 |