2025-10-23
오늘 배운 것
디자인 패턴
프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 ‘규약’으로 정의한 것
싱글톤(Singleton) 패턴
하나의 클래스에 대해 오직 하나의 인스턴스만 생성하고, 전역적으로 공유하는 패턴
대표적으로 DB Connection Pool, 로그 관리 객체, 설정 정보 객체 등에 사용된다
✅ 장점
- 인스턴스 생성 비용 절감
- 객체를 매번 생성하지 않아도 되므로 메모리/시간 비용 감소
- 전역 상태 공유 가능
- 공통 리소스를 일관되게 유지하기 쉬움
⚠️ 단점
- 의존성 증가 (Strong Coupling)
- 싱글톤 객체에 직접 의존하면 테스트·교체·확장이 어려움
- TDD/Testability 저하
- 단위 테스트는 독립성과 순서 무관 실행이 중요한데, 전역 공유 객체는 상태가 남거나 격리가 어렵다
⇒ 이 문제를 해결하기 위해 등장하는 개념이 바로 DI(Dependency Injection)
의존성 주입(Dependency Injection)과의 관계
싱글톤 패턴 자체는 “전역 인스턴스 공유”를 해결하지만
그 과정에서 특정 객체에 대한 직접 의존성이 강해지는 문제를 만든다.
Test / 교체 / 확장 가능한 구조를 만들기 위해
하위 객체를 직접 생성하지 않고 외부에서 “주입”받는 방식으로 해결한다.
DI 적용 효과
- 실제 서비스 객체 ↔ 가짜(Mock) 객체 교체 쉬움 ⇒ 단위테스트 가능
- 특정 구현체를 갈아끼우더라도 상위 코드 수정 ↓ ⇒ 유연한 설계
- 의존성 구조가 명시적으로 드러나 구조 이해 쉬움
부작용
- 클래스 수 증가 → 구조 복잡도 증가
- 런타임 바인딩으로 인한 약간의 오버헤드
의존 역전 원칙(DIP)과의 연결
DI는 단순 기법이 아니라 DIP(Dependency Inversion Principle) 의 구현이다.
상위 모듈은 하위 모듈의 구현에 의존하지 않는다
둘 다 추상화(Interface)에 의존해야 한다
추상화는 구체 구현에 의존하지 않는다
즉,
사용 측에서는 “어떤 객체인지” 모르고
“그 객체가 보장하는 인터페이스”만 알고 다루는 구조로 설계하라는 원칙이다.
정리
| 항목 | 핵심 요약 |
|---|---|
| Singleton | 인스턴스 1개 공유로 비용 절감 & 리소스 일관성 유지 |
| 문제 | 강한 결합·테스트 어려움·유연성 부족 |
| 해결 | DI 사용으로 의존성 역전 + 테스트 가능 구조 |
| 철학 | 구현이 아니라 추상화에 의존하라 (DIP) |
결론적으로,
싱글톤은 흔하지만 그대로 쓰면 테스트 불가 & 유지보수 지옥으로 이어질 수 있다.
DI+DIP를 통해 “관리 가능한 싱글톤 활용”을 만들어내는 게 현대 프레임워크(Spring 등)의 핵심 설계 철학이다.
팩토리(Factory) 패턴
객체 생성 로직을 사용하는 코드로부터 분리하여 생성 책임을 별도 영역으로 위임하는 패턴
상위(추상) 클래스가 전체 구조/규약을 정의하고, 하위(구체) 클래스가 어떤 객체를 생성할지 결정한다.
왜 쓰는가
- 객체 생성
new자체가 코드 내에 박혀 있으면,- 교체가 어렵고
- 테스트하기 어렵고
- 확장할 때 기존 코드 수정이 발생한다
→ Factory가 생성 책임을 갖고, 클라이언트는 ‘무엇을 만들지’ 모른 채 ‘만들어진 것만’ 사용한다.
장점
- 느슨한 결합 (Low Coupling)
- 사용자는 어떤 구현체가 생성되는지 모름
- 상위(인터페이스/추상 클래스)와 하위(구현 클래스)가 분리
- 유연성 증가
- 새 클래스 추가 시 factory만 수정하면 클라이언트는 영향 없음
- 유지보수성 향상
- 객체 생성 로직이 한 곳에 모여 있어 리팩터링·정책 변경 시 한 지점만 수정
DI와의 관계
팩토리 패턴은 본질적으로
“외부에서 객체(구현체)를 생성하여 주입한다”는 점에서 DI와 철학적으로 연결된다.
- 클라이언트는 구현체 정보를 모르고 주입된 추상화에만 의존
- DIP(의존 역전 원칙) 구현을 위한 전형적 전단계
- 스프링 컨테이너의 Bean 생성/주입 과정 자체가 확장된 Factory 역할
쉽게 말하면,
DI가 “누가 넣어줄지”를 해결한다면,
Factory는 “무엇을 어떻게 만들지”를 정의한다.
핵심 요약
| 키워드 | 핵심 내용 |
|---|---|
| 목적 | 객체 생성 책임 분리 (사용 ↔ 생성 분리) |
| 효과 | 결합도↓ 유연성↑ 유지보수성↑ |
| DI 관계 | 생성 주체를 분리/위임한다는 면에서 DI 철학과 이어짐 |
| 확장 | Spring Bean Factory, Abstract Factory, Builder로 확장 가능 |
전략(Strategy) 패턴
객체의 행위(알고리즘)를 캡슐화하여 외부에서 갈아끼울 수 있게 하는 패턴
코드를 직접 수정하지 않고 전략만 교체함으로써 행위 변경을 가능하게 한다.
왜 쓰는가
if/else,switch등 분기 로직이 서비스 코드에 박혀 있으면- 코드 복잡도 상승
- 변경 시 기존 코드 수정 필요 → OCP 위배
- 전략 패턴은 “행동을 클래스로 분리”하여
- 전략 교체 = 행위 변경
- 컨텍스트는 전략이 무엇인지 모른 채 위임
구성 요소
| 구성 | 설명 |
|---|---|
| Strategy (인터페이스) | 공통 알고리즘 규약 선언 |
| ConcreteStrategy | 각각의 알고리즘 구현체 |
| Context | 전략을 사용하고 호출만 하는 쪽 (전략 내부 구현 모름) |
팩토리 패턴과의 차이 정리
| 항목 | 전략 패턴 | 팩토리 패턴 |
|---|---|---|
| 목적 | 행위(알고리즘) 변경/교체 | 객체 생성 책임 분리 |
| 적용 시점 | 실행 중 전략을 갈아끼움 (Runtime 교체) | 생성 시점에서 어떤 객체 만들지 결정 |
| 바꾸는 대상 | 알고리즘(행위) | 인스턴스(구현체) |
| 관련 원칙 | OCP(행위 변경 시 코드 수정 X) | DIP(구현에 의존하지 않음) |
| 예시 | 결제 수단 전략 / 정렬 방식 교체 | DB 커넥터 선택 / 엔진별 생성 |
쉽게 말해서
팩토리는 “무엇을 만들지”를 바꾸게 하는 패턴
전략은 “어떻게 동작할지”를 바꾸게 하는 패턴
둘은 동시에 조합해서도 많이 사용된다.
(예: 전략 객체를 Factory로 생성 → Context에 주입)
핵심 요약
- 전략 패턴은 “행위 교체”를 위한 OCP 친화적 설계
- 팩토리는 “생성 책임 분리”를 위한 DIP 기반 설계
- 둘 다 확장에는 열려 있고 변경에는 닫혀 있게 만드는 패턴이다.
옵저버(Observer) 패턴
특정 객체(Subject)의 상태 변화를 구독(Subscribe) 해두었다가
변화가 발생하면 등록된 옵저버들에게 자동으로 알림을 보내는 패턴
특징
- 발행-구독(Pub-Sub) 구조
- Subject는 상태 변경 사실만 알리고, Observer는 그 알림을 받아 자체 로직 수행
- 양방향 의존도를 낮춘 비동기 이벤트 기반 설계 가능
예시
- 트위터 / 인스타 — 팔로우한 사람의 새 글 업로드 알림
- 주식 앱 — 특정 종목 가격 변동 이벤트 감지
- 이벤트 리스너 — 클릭 시 콜백 실행
왜 쓰는가
- 상태 변경에 민감한 여러 모듈에게 동기화 필요
- 상태 변경 감지를 “폴링(polling)” 하지 않고 이벤트 기반 처리
- 폴링 : 변화 여부를 확인하기 위해 일정 간격으로 반복적으로 상태를 조회하는 방식
- 확장에 유리 — 옵저버 추가/제거가 독립적
프록시(Proxy) 패턴
실제 객체(Real Subject)에 접근하기 전, 중간 계층이 요청을 가로채어 필터링·검증·변환·캐싱 등의 추가 작업을 수행하는 패턴
특징
- 원본 객체에 직접 접근하지 않고 대리 객체를 통해 간접 접근
- 접근 제어, 성능 최적화, 로깅, 보안 등 부가 기능을 삽입할 수 있음
- 원본 객체 변경 없이 기능 확장 가능 (OCP 친화적)
활용 사례
- 보안/권한 체크 — 접근 허용/거부 판단
- 캐싱 — 반복 요청에 빠른 응답
- 로깅 — 요청 흐름 기록
- 지연 로딩(Lazy Loading) — 실제 객체 생성 지연
실전 예시: 프록시 서버
- Nginx
- Reverse Proxy 역할
- 부하 분산, 보안 강화, Node.js 과부하 방지
- Cloudflare
- CDN + Reverse Proxy
- DDoS 방어, HTTPS 인증, 속도 향상
- 프론트 ↔ 백 간 CORS 해결용 Proxy
- 브라우저 제한 우회
이터레이터(Iterator) 패턴
컬렉션 내부 구조를 몰라도, 동일한 인터페이스(Iterator)를 통해 일관된 방식으로 요소를 순회할 수 있게 하는 패턴
특징
- 자료구조(Stack, List, Set, Tree 등)가 달라도 순회 방법을 통일
- 내부 구조(인덱스/포인터 등)를 외부로 노출하지 않음 (캡슐화 유지)
- 순회 로직을 별도 객체로 분리 — 컬렉션과 책임 분리
예시 (Java)
for (Element e : list) { ... }— 내부적으로 Iterator 사용list.iterator().hasNext()/next()
노출 모듈(Revealing Module) 패턴
즉시 실행 함수(IIFE)를 사용해 내부 변수/함수를 private로 감추고, 외부에 노출하고 싶은 API만 public으로 반환하는 방식으로 접근 제어를 흉내내는 패턴
왜 필요한가
- JavaScript에는 원래 private / public 키워드가 없었음
- 외부에서 직접 접근하면 안 되는 내부 상태를 보호할 방법 필요
- 클로저 + IIFE 조합을 통해 “은닉 + 선택적 노출” 구현
예시
const counterModule = (function () {
let count = 0; // private
function increase() {
count++;
}
function get() {
return count;
}
return {
increase, // public
get // public
};
})();
counterModule.increase();
console.log(counterModule.get()); // 1
MVC / MVP / MVVM 패턴 비교
MVC (Model–View–Controller)
애플리케이션을 Model / View / Controller 로 분리하여 책임을 나누는 대표적인 아키텍처 패턴
✅ 장점
- 관심사 분리로 개발 단계별 집중 가능 (역할 명확)
- 재사용성·확장성 용이
⚠️ 단점
- 복잡해질수록 Model ↔ View 간 의존/관계가 얽히기 쉬움
구성
| 역할 | 설명 |
|---|---|
| Model | 데이터(DB, 상태 값 등), 비즈니스 로직 담당 |
| View | 화면(UI), 사용자에게 상태 표현, 변경 시 Controller에 통보 |
| Controller | 이벤트 처리·흐름 제어, Model↔View 연결자 역할 |
MVP (Model–View–Presenter)
MVC의 Controller 역할이 Presenter로 대체된 패턴
MVC와의 차이
| 항목 | MVC의 Controller | MVP의 Presenter |
|---|---|---|
| View 접근 | View를 직접 조작하기도 함 | View를 인터페이스로만 참조 |
| 결합도 | View 의존도 상대적으로 높음 | 뷰를 추상화하여 결합도 낮춤 |
MVVM (Model–View–ViewModel)
Controller가 ViewModel 로 대체된 패턴
ViewModel은 View를 더 높은 수준으로 추상화한 계층
핵심 특징
- Command
- UI 동작들을 하나의 액션으로 캡슐화하여 View가 직접 로직을 가지지 않도록 함
- 예: 버튼 클릭 이벤트를
saveCommand로 바인딩하여 ViewModel이 처리
- Data Binding
- ViewModel 상태 ↔ View 가 자동 동기화
- ViewModel 값을 바꾸면 View가 자동 업데이트
장점
- UI를 코드 수정 없이 재사용 가능
- 테스트 용이 (View와 로직 분리)
요약 비교
| 패턴 | 핵심 포인트 | View 의존성 | 테스트 용이성 |
|---|---|---|---|
| MVC | 역할 분리의 기초형 | 비교적 높음 | 중간 |
| MVP | Presenter로 결합도 감소 | 낮음 | 높음 |
| MVVM | Data Binding/Command 기반 | 매우 낮음 | 매우 높음 |