자바 디자인 패턴 개념 정리
GoF 23개 패턴 + 동시성 패턴 + 안티패턴 + SOLID 원칙을, 의도·구조·Java 표준 라이브러리 적용 사례·모던 Java(21+) 변형 중심으로 정리. 실무에서 자주 마주치고 서로 헷갈리기 쉬운 항목을 우선했다.
읽는 법 — 각 카테고리는 ① 요약 표(한눈에) → ② 패턴별 핵심 노트(의도·JDK·주의) 순서. 헷갈리는 패턴 비교는 §7에 모았다.
📌 0. 기초 용어 사전
0.1 패턴이란
- 디자인 패턴(Design Pattern) — 자주 반복되는 설계 문제에 대한 검증된 해법 템플릿. 코드가 아니라 설계 아이디어다.
- GoF (Gang of Four) — Design Patterns (1994) 저자 4인(Gamma·Helm·Johnson·Vlissides). 23개 패턴을 생성·구조·행위 3분류로 정리.
- 패턴의 4요소 — 이름 · 문제(언제) · 해법(구조) · 결과(trade-off).
추상화 수준 구분 (작은 것 → 큰 것)
| 용어 | 범위 | 예 |
|---|---|---|
| 이디엄(idiom) | 한 언어 한정 관용구 | Java try-with-resources, equals/hashCode 쌍 |
| 디자인 패턴 | 언어 무관 객체 설계 | Strategy, Observer |
| 아키텍처 패턴 | 시스템 구조 | MVC, 레이어드, 헥사고날 |
0.2 OOP·결합 핵심 용어
- 결합도(Coupling) — 모듈 간 의존 정도. 낮을수록 좋다(loose coupling).
- 응집도(Cohesion) — 한 모듈이 한 가지 책임에 집중하는 정도. 높을수록 좋다.
- 캡슐화·추상화·상속·다형성 — OOP 4대 특성. 패턴 대부분은 다형성 + 합성으로 변화를 흡수한다.
- 위임(Delegation) — 일을 다른 객체에게 넘겨 처리. 상속의 대안으로 패턴의 핵심 메커니즘.
0.3 GoF의 2대 설계 원칙 ⭐
- 구현이 아니라 인터페이스에 프로그래밍하라 (Program to an interface, not an implementation)
- 구체 클래스가 아닌 추상 타입(인터페이스) 에 의존 → 구현 교체가 자유로움.
- 상속보다 객체 합성을 선호하라 (Favor object composition over class inheritance)
- 상속은 컴파일 타임 고정·캡슐화 약화. 합성은 런타임 교체 가능 → 유연.
이 두 원칙이 거의 모든 패턴의 뿌리다. Strategy·Decorator·Bridge·State가 대표적.
0.4 UML 관계 표기 (다이어그램 읽기)
| 관계 | 화살표 | 의미 |
|---|---|---|
| 일반화(상속) | ─▷ (빈 삼각형) | is-a, extends |
| 실체화(구현) | ┈▷ (점선+빈삼각형) | implements |
| 연관(association) | → | 참조 보유 |
| 집약(aggregation) | ◇→ (빈 마름모) | has-a, 약한 소유(부품 독립 생존) |
| 합성(composition) | ◆→ (찬 마름모) | has-a, 강한 소유(전체 사라지면 부품도 소멸) |
| 의존(dependency) | ┈> | 파라미터·지역변수로 잠깐 사용 |
📌 1. SOLID 원칙 — 패턴의 토대
암기: SOLID = S·O·L·I·D
| 원칙 | 정의 | 한 줄 | 위반 신호 | 관련 패턴 |
|---|---|---|---|---|
| SRP 단일 책임 | 클래스는 하나의 변경 이유만 | "왜 바뀌나?"가 둘이면 분리 | 한 클래스가 UI+DB+검증 | Facade, Mediator |
| OCP 개방-폐쇄 | 확장엔 열고, 변경엔 닫고 | 새 기능 = 코드 추가, 기존 수정 X | 새 타입마다 if/switch 증식 | Strategy, Template Method, Decorator |
| LSP 리스코프 치환 | 자식은 부모를 완전히 대체 가능 | 자식이 부모 계약을 깨면 위반 | Square extends Rectangle, 예외 던지는 오버라이드 | (상속 설계 전반) |
| ISP 인터페이스 분리 | 안 쓰는 메서드에 의존 강요 X | 큰 인터페이스 → 작게 쪼개기 | 빈 구현(throw new UnsupportedOperation) | Adapter |
| DIP 의존 역전 | 추상에 의존, 구체에 의존 X | 고수준이 저수준 구체를 직접 new | 상위 모듈이 new MySqlRepo() | Factory, DI(IoC) |
- OCP의 실현 도구가 곧 다형성 — 분기문 대신 인터페이스 + 구현 교체.
- DIP ≈ DI(의존성 주입)·IoC(제어의 역전) — Spring의 핵심.
📌 2. 생성 패턴 (Creational · 5개)
객체 생성을 캡슐화 — 무엇을·어떻게 만들지를 사용처에서 분리. 암기: 싱·팩·추·빌·프
| 패턴 | 의도 | 대표 JDK 예 |
|---|---|---|
| Singleton | 인스턴스 딱 하나 + 전역 접근 | Runtime.getRuntime(), Desktop.getDesktop() |
| Factory Method | 어떤 객체를 만들지 하위 클래스가 결정 | Calendar.getInstance(), NumberFormat.getInstance() |
| Abstract Factory | 관련 객체 군(family) 을 일관되게 생성 | DocumentBuilderFactory.newInstance(), TransformerFactory |
| Builder | 복잡한 객체를 단계별로 조립 | StringBuilder, Stream.Builder, Locale.Builder |
| Prototype | 기존 객체를 복제해 생성 | Object.clone() + Cloneable |
Singleton — 단 하나의 인스턴스
- 핵심: private 생성자 + 정적 접근자. 설정·로거·커넥션풀 등 공유 자원.
- 모던 Java(권장): enum 싱글톤 — 직렬화·리플렉션 공격에 안전 (Effective Java Item 3).
public enum Config { INSTANCE; public String url() { return url; } } - 지연 초기화 + 스레드 안전:
volatile+ DCL(이중 검사 잠금), 또는 holder idiom(정적 중첩 클래스). - 주의: 전역 상태 → 테스트 곤란·결합도 ↑. 안티패턴화되기 쉬움 (§6 참고).
Factory Method — 생성 책임을 하위 클래스로
- 핵심: 상위에
createXxx()추상 메서드, 하위가 어떤 구체 클래스를 반환할지 결정. - 정적 팩토리 메서드와 구별 —
of()·valueOf()·getInstance()는 GoF Factory Method가 아니라 Effective Java Item 1의 관용구지만 실무에서 같이 묶어 부른다. - JDK:
Calendar.getInstance(),Iterator iterator(),java.sql.Connection.createStatement().
Abstract Factory — 제품 군을 통째로
- 핵심: 서로 어울리는 객체 세트를 만드는 팩토리 인터페이스. (예: GTK 버튼+GTK 체크박스 vs Win 버튼+Win 체크박스)
- Factory Method와 차이: Factory Method = 한 제품, Abstract Factory = 제품 군 여러 개 (§7 비교).
- JDK:
DocumentBuilderFactory,TransformerFactory,XPathFactory— 모두newInstance().
Builder — 복잡한 생성자 길들이기
- 핵심: 선택 인자가 많을 때 점층적 생성자(telescoping) 대신 메서드 체이닝으로 조립.
- Effective Java Item 2 권장. 가독성·불변성 동시 확보.
var http = HttpRequest.newBuilder().uri(uri).GET().timeout(D).build(); - 모던:
record+ 정적 팩토리로 단순 케이스 대체 가능, Lombok@Builder. - JDK:
StringBuilder/StringBuffer,Stream.Builder,Locale.Builder,HttpRequest.Builder.
Prototype — 복제로 생성
- 핵심:
new비용이 크거나 런타임에 타입이 정해질 때 원본을 복사. - 모던(권장):
Cloneable/clone()은 결함 많음 → 복사 생성자·복사 정적 팩토리 선호 (Effective Java Item 13). - JDK:
Object.clone(),ArrayList·HashMap의clone().
📌 3. 구조 패턴 (Structural · 7개)
객체를 조합해 더 큰 구조를 만들거나 인터페이스를 맞춘다. 암기: 어·브·컴·데·퍼·플·프
| 패턴 | 한 줄 의도 | 대표 JDK 예 |
|---|---|---|
| Adapter | 호환 안 되는 인터페이스를 맞춰줌 (변환) | Arrays.asList(), InputStreamReader |
| Bridge | 추상과 구현을 분리해 독립 확장 | JDBC Driver, AWT 컴포넌트↔Peer |
| Composite | 개별·복합 객체를 동일하게 다룸 (트리) | java.awt.Container, Swing JComponent |
| Decorator | 객체를 감싸 기능을 동적 추가 | java.io 스트림, Collections.unmodifiableXxx |
| Facade | 복잡한 서브시스템에 단순 창구 제공 | javax.faces, Spring JdbcTemplate(외부) |
| Flyweight | 공유로 메모리 절약(불변 내부 상태) | Integer.valueOf()(캐시), 문자열 풀 |
| Proxy | 대상 객체 접근을 대리·제어 | java.lang.reflect.Proxy, RMI, Spring AOP |
Adapter — 인터페이스 변환기
- 핵심: 기존 클래스를 클라이언트가 기대하는 인터페이스로 감싸 변환. "콘센트 어댑터".
- JDK:
Arrays.asList(arr)(배열→List),InputStreamReader(바이트→문자),Collections.list(enumeration).
Bridge — 추상과 구현을 분리
- 핵심: 두 축이 각자 변하면(예: 도형 × 렌더링 방식) 합성으로 연결해 곱집합 폭발을 막음.
- Adapter와 차이: Adapter는 사후에 안 맞는 걸 맞춤, Bridge는 설계 단계부터 분리 (§7).
- JDK: JDBC API(추상) ↔ 드라이버 구현, AWT
Component↔ComponentPeer.
Composite — 부분-전체 트리
- 핵심: 잎(Leaf)과 가지(Composite)가 같은 인터페이스 → 재귀적으로 동일 처리.
- JDK:
java.awt.Container(Component를 담는 Component), Swing 컴포넌트 트리,org.w3c.dom.Node.
Decorator — 래핑으로 기능 추가 ⭐
- 핵심: 대상과 같은 인터페이스를 구현하면서 감싸서 책임을 덧붙임. 상속 대신 런타임 조합.
- 상속과 차이: 상속은 컴파일 타임 고정, Decorator는 여러 겹 동적 조합 (
new BufferedInputStream(new FileInputStream(f))). - JDK:
java.io(Buffered·Data·GZIP 스트림),Collections.synchronizedList/unmodifiableList/checkedList.
Facade — 단순화된 창구
- 핵심: 복잡한 내부 호출들을 하나의 고수준 인터페이스로 감싼다. 서브시스템을 숨기지 않고 단순화.
- Decorator와 차이: Facade는 인터페이스를 단순화, Decorator는 같은 인터페이스 유지 + 기능 추가.
- 예: Spring
JdbcTemplate, SLF4J 로깅 파사드(외부 라이브러리에서 흔함).
Flyweight — 공유로 메모리 절약
- 핵심: 변하지 않는 내부 상태(intrinsic) 를 공유 객체로, 변하는 외부 상태(extrinsic) 는 인자로 전달.
- JDK:
Integer.valueOf()/Long.valueOf()(−128~127 캐시),Boolean.valueOf(),Character.valueOf(), 문자열 상수 풀(String.intern()).Integer.valueOf(100) == Integer.valueOf(100) // true (캐시 공유) Integer.valueOf(200) == Integer.valueOf(200) // false (범위 밖)
Proxy — 대리·접근 제어
- 핵심: 대상과 같은 인터페이스를 가진 대리인이 접근을 통제. 종류: 가상(지연 로딩)·보호(권한)·원격(RMI)·로깅/캐싱.
- Decorator와 차이: Decorator는 기능 추가, Proxy는 접근 제어 (둘 다 같은 인터페이스로 감쌈).
- JDK:
java.lang.reflect.Proxy(동적 프록시), RMI 스텁, Spring AOP(트랜잭션·@Transactional).
📌 4. 행위 패턴 (Behavioral · 11개)
객체 간 책임 분배와 통신. 11개로 가장 많다. 암기: 책·커·인·이·중·메·옵·상·전·템·비
| 패턴 | 한 줄 의도 | 대표 JDK 예 |
|---|---|---|
| Chain of Responsibility | 처리자 사슬로 요청 위임 | Servlet FilterChain, Logger 부모 위임 |
| Command | 요청을 객체로 캡슐화 (실행 분리·취소) | Runnable, Callable, Swing Action |
| Interpreter | 문법 규칙을 클래스로 표현·해석 | java.util.regex.Pattern, java.text.Format |
| Iterator | 컬렉션 내부 노출 없이 순회 | Iterator, Enumeration, for-each |
| Mediator | 객체 간 통신을 중재자로 집중 | ExecutorService(작업↔스레드, 개념적) |
| Memento | 캡슐화 깨지 않고 상태 저장·복원 | 직렬화(Serializable) 기반 스냅샷 |
| Observer | 상태 변화를 구독자에 통지 (1:N) | PropertyChangeListener, Flow.Subscriber |
| State | 상태에 따라 행동을 바꿈 | (개념 — enum+추상 메서드로 구현) |
| Strategy | 알고리즘을 교체 가능하게 캡슐화 | Comparator, RejectedExecutionHandler |
| Template Method | 알고리즘 골격은 고정, 일부만 하위 구현 | AbstractList, InputStream.read() |
| Visitor | 구조는 두고 연산을 외부 추가 | FileVisitor(Files.walkFileTree) |
Chain of Responsibility — 처리 사슬
- 핵심: 요청을 처리할 수 있을 때까지 다음 핸들러로 넘김. 송신자-수신자 결합 해제.
- JDK:
javax.servlet.Filter/FilterChain,java.util.logging.Logger(부모 로거로 위임), 예외 전파.
Command — 요청의 객체화
- 핵심: 동작 + 인자를 객체로 포장 → 큐잉·로깅·undo/redo 가능.
- 모던: 인터페이스가 단일 메서드면 람다로 대체 (
Runnable r = () -> ...). - JDK:
Runnable,Callable,java.util.concurrent작업 제출.
Interpreter — 문법 해석
- 핵심: 간단한 언어/문법을 클래스 트리로 표현하고 해석. (DSL·수식·정규식)
- JDK:
java.util.regex.Pattern,java.text.MessageFormat/DecimalFormat.
Iterator — 일관된 순회
- 핵심: 컬렉션의 내부 표현을 숨긴 채 원소를 차례로 접근.
- 모던:
Iterable구현 → for-each,Stream으로 선언형 순회. - JDK:
java.util.Iterator,Enumeration(레거시).
Mediator — 중재자
- 핵심: 다대다로 얽힌 객체들을 중재자 하나에 연결 → 결합도 ↓. (관제탑이 비행기끼리 직접 통신 막음)
- Observer와 차이: Mediator = 양방향 조정, Observer = 단방향 통지 (§7).
Memento — 스냅샷·복원
- 핵심: 객체 내부 상태를 캡슐화 유지한 채 저장했다가 되돌림. (Ctrl+Z)
- 구성: Originator(원본)·Memento(스냅샷)·Caretaker(보관자).
Observer — 발행/구독 ⭐
- 핵심: Subject 상태가 바뀌면 등록된 Observer들에 자동 통지 (1:N). 느슨한 결합.
- 모던:
java.util.Observer/Observable은 Java 9부터 deprecated →PropertyChangeListener, Reactive Streams(Flow.Publisher/Subscriber), 외부 라이브러리(RxJava) 사용. - JDK: Swing 이벤트 리스너,
java.beans.PropertyChangeListener.
State — 상태 기계
- 핵심: 객체의 상태를 클래스로 만들어, 상태마다 다른 행동 + 전이를 캡슐화.
if(state==…)분기 제거. - Strategy와 차이: 구조는 같지만 State는 스스로 다음 상태로 전이, Strategy는 외부가 알고리즘 선택 (§7).
- 모던:
enum+ 추상 메서드로 간결 구현.
Strategy — 알고리즘 교체 ⭐
- 핵심: 알고리즘 군을 인터페이스로 캡슐화해 런타임 교체. OCP의 교과서적 실현.
- 모던: 함수형 인터페이스 → 람다·메서드 참조로 전략 주입.
list.sort(Comparator.comparing(User::age)); // 전략 = 람다 - JDK:
Comparator,ThreadPoolExecutor의RejectedExecutionHandler,Collections.sort(list, cmp).
Template Method — 골격 고정 ⭐
- 핵심: 상위가 알고리즘 뼈대를 정의하고, 변하는 단계만 하위가 오버라이드. 상속 기반.
- Strategy와 차이: Template Method = 상속(컴파일 타임), Strategy = 합성(런타임) (§7).
- JDK:
AbstractList/AbstractMap(get,size만 구현),InputStream.read(),HttpServlet.service()→doGet/doPost.
Visitor — 연산의 외부화
- 핵심: 객체 구조는 그대로 두고, 새 연산을 Visitor로 추가 (double dispatch). 구조 안정 + 연산 자주 추가일 때.
- 단점: 새 요소 타입 추가는 모든 Visitor 수정 필요.
- 모던(대안): sealed 인터페이스 + switch 패턴 매칭(Java 21)이 닫힌 타입 계층에서 Visitor를 대체.
sealed interface Shape permits Circle, Rect {} double area = switch (shape) { case Circle c -> …; case Rect r -> …; }; - JDK:
java.nio.file.FileVisitor(Files.walkFileTree),javax.lang.model.element.ElementVisitor(애너테이션 처리).
📌 5. 동시성 패턴 (Concurrency)
멀티스레드 환경의 자원·실행 제어. Java Concurrency in Practice 기반.
| 패턴 | 의도 | Java 도구 |
|---|---|---|
| Thread Pool | 스레드 재사용으로 생성비용·과생성 방지 | ExecutorService, ThreadPoolExecutor |
| Producer–Consumer | 생산·소비 속도 분리(버퍼) | BlockingQueue |
| Future / Promise | 비동기 결과를 나중에 받기 | Future, CompletableFuture |
| Read–Write Lock | 읽기 동시 허용, 쓰기 배타 | ReentrantReadWriteLock, StampedLock |
| Double-Checked Locking | 지연 초기화 + 락 최소화 | volatile 필드 + 이중 검사 |
| Thread-Local Storage | 스레드별 독립 상태 | ThreadLocal |
| Immutable Object | 공유해도 안전(상태 불변) | record, final, List.of() |
| Guarded Suspension / Balking | 조건 충족까지 대기 / 즉시 포기 | wait/notify, Condition |
모던 Java(21+) 동시성
- 가상 스레드(Virtual Threads, JEP 444) — 경량 스레드. thread-per-request를 값싸게.
Executors.newVirtualThreadPerTaskExecutor(). - 구조적 동시성(Structured Concurrency) — 연관 작업을 한 단위로 묶어 취소·예외 전파.
StructuredTaskScope. - 불변성 우선 — 가장 단순한 동시성 전략. 공유 + 불변 = 락 불필요.
📌 6. 안티패턴 & 리팩토링
6.1 대표 안티패턴
| 안티패턴 | 증상 | 처방 |
|---|---|---|
| God Object / God Class | 한 클래스가 모든 책임 | SRP로 분리(Facade·Mediator) |
| Singleton 남용 | 전역 상태·숨은 의존·테스트 곤란 | DI로 주입, 범위 제한 |
| Anemic Domain Model | 데이터만 있고 행위는 서비스에 | 도메인에 행위 부여 |
| Spaghetti Code | 제어 흐름 뒤엉킴 | 함수 추출·계층화 |
| Golden Hammer | 한 패턴/도구를 모든 곳에 | 문제에 맞는 도구 선택 |
| Magic Number/String | 의미 불명 리터럴 | 상수·enum화 |
| Premature Optimization | 측정 없이 성급한 최적화 | 먼저 측정(프로파일링) |
| Over-engineering | 불필요한 패턴 떡칠 | YAGNI — 필요할 때 도입 |
6.2 패턴으로 가는 리팩토링
- 조건 분기 → 다형성 (
Replace Conditional with Polymorphism) ⇒ Strategy / State - 거대 생성자 → Builder
- 중복 알고리즘 골격 → Template Method
- 인자 뭉치 → Introduce Parameter Object
- 전역 변경 통지 → Observer
핵심 태도: 패턴은 목적이 아니라 수단. 문제(중복·경직·산탄총 수정)가 보일 때 처방으로 도입한다. — Refactoring (Fowler)
📌 7. 자주 헷갈리는 패턴 비교 ⭐ (실무에서 자주 혼동)
Strategy vs State
- 구조 동일(상태/전략을 객체로 위임), 의도 다름.
- Strategy — 외부 클라이언트가 알고리즘을 선택. 전략끼리 서로 모름.
- State — 객체가 스스로 다음 상태로 전이. 상태끼리 전이를 앎.
Factory Method vs Abstract Factory
- Factory Method — 상속 기반, 한 제품 생성을 하위 클래스가 결정.
- Abstract Factory — 합성 기반, 제품 군(여러 종류) 을 일관되게 생성.
Adapter vs Bridge vs Decorator vs Proxy vs Facade
| 패턴 | 핵심 의도 | 인터페이스 |
|---|---|---|
| Adapter | 안 맞는 걸 변환 (사후) | 다른 → 기대하는 |
| Bridge | 추상·구현 분리 (사전 설계) | 두 축 독립 |
| Decorator | 기능 추가 | 같음 + 추가 |
| Proxy | 접근 제어 | 같음 |
| Facade | 단순화 창구 | 새 단순 인터페이스 |
Decorator·Proxy는 둘 다 같은 인터페이스로 감싸지만 — Decorator=기능, Proxy=통제.
Template Method vs Strategy
- Template Method — 상속, 컴파일 타임 고정, 골격 일부만 교체.
- Strategy — 합성, 런타임 통째 교체. (상속보다 합성 → 더 유연)
Observer vs Mediator vs Pub-Sub
- Observer — Subject→Observer 단방향 통지 (직접 등록).
- Mediator — 여러 객체의 양방향 통신을 중재자가 조정.
- Pub-Sub — Observer 변형, 중간 브로커로 발행자-구독자 완전 분리.
Composite vs Decorator
- Composite — 부분-전체 트리 (여러 자식).
- Decorator — 한 겹씩 래핑 (자식 하나).
📌 8. Java 표준 라이브러리 패턴 빠른 표
"이 JDK 클래스 = 무슨 패턴?" 식별 문제 대비.
| 패턴 | JDK 적용 사례 |
|---|---|
| Singleton | Runtime.getRuntime(), Desktop.getDesktop() |
| Factory Method | Calendar.getInstance(), NumberFormat.getInstance(), valueOf() |
| Abstract Factory | DocumentBuilderFactory, TransformerFactory, XPathFactory |
| Builder | StringBuilder, Stream.Builder, HttpRequest.Builder, Locale.Builder |
| Prototype | Object.clone(), Cloneable |
| Adapter | Arrays.asList(), InputStreamReader, Collections.list() |
| Bridge | JDBC Driver, AWT Component↔Peer |
| Composite | java.awt.Container, Swing JComponent, DOM Node |
| Decorator | java.io(Buffered·Data·GZIP), Collections.unmodifiable/synchronized/checkedXxx |
| Flyweight | Integer.valueOf()(캐시), Boolean.valueOf(), 문자열 풀 |
| Proxy | java.lang.reflect.Proxy, RMI, Spring AOP |
| Chain of Responsibility | Servlet FilterChain, java.util.logging.Logger |
| Command | Runnable, Callable, Swing Action |
| Interpreter | java.util.regex.Pattern, java.text.Format |
| Iterator | java.util.Iterator, Enumeration |
| Observer | PropertyChangeListener, Flow.Subscriber, Swing 리스너 |
| Strategy | Comparator, RejectedExecutionHandler |
| Template Method | AbstractList/AbstractMap, InputStream.read(), HttpServlet.service() |
| Visitor | FileVisitor(Files.walkFileTree), ElementVisitor |
📌 9. 실무·프레임워크 적용 예시 ⭐
JDK API 너머, 실제 백엔드·프레임워크·도메인에서 패턴이 어떻게 쓰이는지.
생성 패턴
| 패턴 | 실무 적용 사례 |
|---|---|
| Singleton | 스프링 빈 기본 스코프(singleton), 앱 설정 레지스트리, DB 커넥션 풀, 캐시 매니저 |
| Factory Method | LoggerFactory.getLogger()(SLF4J), JDBC DriverManager.getConnection(), 스프링 BeanFactory |
| Abstract Factory | 클라우드 SDK 추상화(AWS↔GCP 클라이언트 군), UI 테마(다크/라이트 위젯 세트), DB 방언별 객체 군 |
| Builder | OkHttp Request.Builder, HttpRequest.newBuilder(), Lombok @Builder, 테스트 데이터 빌더, k8s 매니페스트 빌더 |
| Prototype | 게임 엔티티 복제, 무거운 설정·문서 템플릿 복제, 표 셀 스타일 복사 |
구조 패턴
| 패턴 | 실무 적용 사례 |
|---|---|
| Adapter | 레거시 결제·인증 API를 신규 인터페이스로 통합, SLF4J↔Log4j/Logback 연결, 서드파티 SDK 래핑 |
| Bridge | 결제 게이트웨이 추상화(PG사 교체 자유), 알림 발송(채널 × 메시지 종류), 크로스플랫폼 렌더링 |
| Composite | React/Vue 컴포넌트 트리, 파일시스템(폴더/파일), 조직도·메뉴·권한 트리 |
| Decorator | HTTP 미들웨어/인터셉터 체인(인증·gzip·로깅), 스프링 HttpServletRequestWrapper, 커피 주문 토핑 |
| Facade | 스프링 JdbcTemplate·RestTemplate, 결제·주문 모듈 단일 진입점, 복잡한 레거시 래핑 |
| Flyweight | 게임 타일·파티클·스프라이트, 폰트 글리프 캐시, 에디터 문자 객체 공유 |
| Proxy | 스프링 AOP @Transactional·@Cacheable·@Async, JPA/Hibernate 지연 로딩 프록시, API 레이트리밋·인증 프록시 |
행위 패턴
| 패턴 | 실무 적용 사례 |
|---|---|
| Chain of Responsibility | 스프링 시큐리티 필터 체인, 서블릿 필터, 미들웨어 파이프라인, 전자결재 승인 라인 |
| Command | 메시지 큐 작업(Job), 에디터 undo/redo, 스케줄러 태스크, 이벤트 소싱 커맨드 |
| Interpreter | SQL·정규식 엔진, 규칙 엔진(Drools), 스프링 SpEL, 검색 쿼리 파서 |
| Iterator | 컬렉션 순회, DB 커서·페이지네이션, Kafka consumer, 스트림 파이프라인 |
| Mediator | 채팅 서버(참가자 중재), UI 폼 컴포넌트 조정, 메시지 브로커(개념), 항공 관제 |
| Memento | IDE/에디터 undo, 게임 세이브포인트, 폼 임시저장(draft), DB 스냅샷·롤백 |
| Observer | 스프링 ApplicationEvent, 리액티브(Reactor·RxJava), 도메인 이벤트, 알림 구독, 실시간 대시보드 |
| State | 주문 상태머신(결제→배송→완료), TCP 연결 상태, 워크플로/BPM 엔진, 게임 캐릭터 FSM |
| Strategy | 결제 수단(카드/간편결제/계좌), 할인·과금 정책, 압축·직렬화 선택, 로드밸런싱·라우팅 전략 |
| Template Method | 스프링 JdbcTemplate(콜백 훅), 테스트 @BeforeEach/@AfterEach, 빌드·배포 파이프라인 단계 |
| Visitor | 컴파일러·린터 AST 순회, 파일 트리 처리, 리포트·집계 생성기, 애너테이션 프로세서 |
동시성 패턴
| 패턴 | 실무 적용 사례 |
|---|---|
| Thread Pool | 웹서버 요청 처리, 스프링 @Async 실행기, 배치 워커 풀 |
| Producer–Consumer | 로그 수집 버퍼, 이벤트 처리 큐, 이미지 처리 파이프라인 |
| Future/Promise | 외부 API 병렬 호출, 비동기 집계(CompletableFuture.allOf) |
| Immutable | DTO·값 객체(record), 설정 스냅샷, 동시성 안전 공유 데이터 |
시나리오 → 패턴 빠른 선택
- "기능을 런타임에 갈아끼우고 싶다" → Strategy
- "상태에 따라 행동·전이가 다르다" → State
- "요청 전후로 공통 처리를 끼우고 싶다" → Decorator / Chain of Responsibility
- "복잡한 내부를 간단한 창구 하나로" → Facade
- "변경을 여러 곳에 알리고 싶다" → Observer
- "취소·재실행·큐잉이 필요하다" → Command
📌 10. 실무 노하우 — 현장에서 실제로 겪는 일
운영에서 디자인 패턴 때문에 실제로 터지거나 패턴으로 해결한 사례. 패턴 이름보다 왜 그렇게 되는지가 핵심이다.
1. SimpleDateFormat을 static으로 공유했다가 멀티스레드에서 깨진다 — 싱글톤/공유의 함정
SimpleDateFormat은 내부에 가변 상태가 있어 스레드 안전하지 않다. 전역 하나로 공유하면 동시 호출 시 파싱 결과가 뒤섞이거나 예외가 난다.- 노하우: 불변인
DateTimeFormatter(java.time)를 쓰거나, 불가피하면ThreadLocal로 분리. 공유 싱글톤은 불변일 때만 안전하다.
2. @Transactional이 같은 클래스 내부 호출에서 안 먹힌다 — 프록시의 한계(self-invocation)
- 스프링은 빈을 프록시로 감싸 트랜잭션을 적용하는데, 같은 객체의
this.method()호출은 프록시를 거치지 않아@Transactional·@Cacheable·@Async가 무시된다. - 노하우: 대상 메서드를 다른 빈으로 분리하거나 self-주입한 프록시로 호출. "애너테이션 붙였는데 왜 안 걸리지?"의 대부분이 이것.
3. ThreadLocal을 스레드 풀에서 안 비워 메모리 누수 — 스레드 재사용 함정
- 톰캣 워커 스레드는 재사용된다.
ThreadLocal에 담고remove하지 않으면 값이 다음 요청으로 새거나, 재배포 시 클래스로더가 GC되지 못해 메모리가 계속 찬다(톰캣 경고 로그의 그 현상). - 노하우:
try { set(...); ... } finally { threadLocal.remove(); }를 습관화한다.
4. Integer 캐시 때문에 ==가 127까지만 true — 플라이웨이트 캐시
Integer.valueOf(127) == Integer.valueOf(127)은 true,128부터 false. 작은 값에선 멀쩡하다 큰 값에서만 틀어지는 미궁 버그.- 노하우: 래퍼 값 비교는 항상
equals. 캐시는 성능용이지 동일성 보장이 아니다.
5. HashMap을 동시성 환경에서 공유해 CPU 100% 무한 루프 — 유명한 운영 장애(Java 7)
- 여러 스레드가 동시에 resize하면 내부 링크가 꼬여 조회가 무한 루프에 빠졌다. Java 8에서 무한 루프는 완화됐지만
HashMap은 여전히 스레드 안전하지 않다. - 노하우: 공유 맵은
ConcurrentHashMap. "운영에서 가끔 한 코어만 100%"면 이걸 의심.
6. 단순 로직에 패턴 떡칠 → 유지보수 악몽 — 오버엔지니어링
FizzBuzzEnterpriseEdition(깃허브 풍자 프로젝트)처럼 20줄이면 될 코드를 팩토리·전략·비지터로 수십 클래스로 부풀린 현장이 흔하다.- 노하우: 패턴은 중복·변경 압력이 실제로 보일 때 도입(YAGNI). "나중에 바뀔 수도 있으니 미리"가 가장 비싼 핑계.
📌 11. 실무 데이터 — 외워두면 좋은 구체 사실
패턴이 실제 JDK·프레임워크에서 어떤 숫자·기본값·동작으로 나타나는지. (버전에 따라 달라질 수 있는 값은 표기)
| 주제 | 구체 데이터 |
|---|---|
| Flyweight — Integer 캐시 | 기본 −128 ~ 127 공유. 상한은 -XX:AutoBoxCacheMax=N 또는 -Djava.lang.Integer.IntegerCache.high로 조정 |
| Flyweight — String 풀 | 문자열 리터럴은 String Pool에 자동 intern. 런타임 문자열은 String.intern()으로 명시 등록 |
| Singleton — Spring 빈 | 기본 스코프 singleton(컨테이너당 1개). prototype은 주입·조회마다 새 인스턴스 |
| Proxy — Spring AOP | java.lang.reflect.Proxy는 인터페이스만 프록시 가능 → 클래스 프록시는 CGLIB/ByteBuddy. 스프링 AOP가 둘 다 사용 |
| Observer — Observable | java.util.Observable/Observer는 Java 9(2017)부터 @Deprecated → Flow·PropertyChangeListener로 대체 |
| 공유 가변 — 날짜 포맷 | SimpleDateFormat은 스레드 안전 X(javadoc 명시) / DateTimeFormatter는 불변·스레드 안전 |
| Iterator — HashMap 내부 | 기본 부하율 0.75, 한 버킷 8개 초과 + 용량 64 이상이면 트리(red-black)화, 6 이하로 줄면 리스트 복귀 |
| Decorator — java.io 단축 | Files.newBufferedReader(path)가 BufferedReader+InputStreamReader+FileInputStream 3중첩을 대체 |
| Strategy — Comparator | comparing · thenComparing · reversed · nullsFirst/Last · naturalOrder로 합성 |
| Builder — JDK 표준 | StringBuilder, Stream.builder(), HttpRequest.newBuilder()·HttpClient.newBuilder()(java.net.http, Java 11+), Locale.Builder |
| Abstract Factory — JAXP | DocumentBuilderFactory·TransformerFactory·XPathFactory의 newInstance()가 클래스패스 구현을 선택 |
| Command — 동시성 API | Runnable(반환 없음)·Callable<V>(값·예외 반환)을 ExecutorService.submit이 Future로 |
모던 Java 변형 데이터
- Visitor →
sealed+switch패턴 매칭(JEP 441, Java 21)으로 망라성 컴파일 검사- Strategy/Command → 단일 메서드 함수형 인터페이스는 람다·메서드 참조로 대체
- Prototype →
Cloneable/clone()대신 복사 생성자(Effective Java Item 13)- Builder →
record(Java 16+)가 단순 값 객체의 보일러플레이트를 제거
📌 12. 암기 팁
23패턴 3분류
- 생성(5) — 객체를 어떻게 만드나: 싱글톤·팩토리메서드·추상팩토리·빌더·프로토타입
- 구조(7) — 객체를 어떻게 엮나: 어댑터·브리지·컴포지트·데코레이터·퍼사드·플라이웨이트·프록시
- 행위(11) — 객체가 어떻게 소통·행동하나: 책임연쇄·커맨드·인터프리터·이터레이터·미디에이터·메멘토·옵저버·상태·전략·템플릿메서드·비지터
원리 한 줄
- 변하는 것 ↔ 변하지 않는 것을 분리하라.
- 상속보다 합성, 구현보다 인터페이스.
- 패턴은 처방이지 목표가 아니다 — 과용은 안티패턴.
1차 자료: GoF Design Patterns (1994) · Head First Design Patterns (Java) · Effective Java 3e (Bloch) · Refactoring 2e (Fowler) · Refactoring.Guru · Java SE 21 API Docs.