Ch 17.1 디자인 패턴 개요
디자인 패턴(Design Pattern) 은 소프트웨어 개발에서 반복적으로 등장하는 문제들에 대한 검증된 해결책 입니다. 마치 건축의 설계 도면처럼, 비슷한 상황에서 재사용 가능한 코드 구조와 설계 아이디어를 제공합니다.
GoF(Gang of Four)가 1994년 저서 "Design Patterns: Elements of Reusable Object-Oriented Software"에서 정리한 23개의 패턴 이 표준으로 통용되며, 크게 3가지로 분류됩니다.
GoF란?
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 4인이 공동 집필한 디자인 패턴 바이블. 30년이 지난 지금도 소프트웨어 설계의 표준 언어로 사용됩니다.
1. 왜 디자인 패턴을 배워야 하나?
1.1 공통 언어 제공
// 패턴 없이 설명:
// "여기서 객체를 한 번만 만들고, 어디서든 같은 인스턴스를 사용하게 해주세요."
// 패턴으로 설명:
// "여기에 싱글톤 패턴 적용합시다."
// → 팀원 모두가 즉시 설계 의도를 이해
1.2 검증된 해결책
수십 년간 수백만 명의 개발자들이 검증한 솔루션을 처음부터 고민하지 않아도 됩니다.
1.3 유지보수 용이
패턴을 따른 코드는 변경과 확장에 강합니다 (OPEN-CLOSED 원칙).
1.4 취업 면접
백엔드 개발자 면접에서 디자인 패턴은 단골 질문입니다.
2. 패턴의 3가지 분류
| 분류 | 목적 | 주요 패턴 |
|---|---|---|
| 생성 (Creational) | 객체 생성 방법 캡슐화 | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| 구조 (Structural) | 클래스/객체를 더 큰 구조로 조합 | Adapter, Decorator, Proxy, Facade, Composite, Bridge, Flyweight |
| 행동 (Behavioral) | 객체 간 책임 분배와 협력 | Strategy, Observer, Template Method, Command, Iterator, State, Chain of Responsibility |
3. SOLID 원칙 복습
디자인 패턴은 대부분 SOLID 원칙을 실현하는 수단입니다.
| 원칙 | 약어 | 핵심 한 줄 |
|---|---|---|
| Single Responsibility | SRP | 클래스는 하나의 책임만 가져야 한다 |
| Open-Closed | OCP | 확장에는 열려있고, 수정에는 닫혀있어야 한다 |
| Liskov Substitution | LSP | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다 |
| Interface Segregation | ISP | 사용하지 않는 인터페이스에 의존하지 않아야 한다 |
| Dependency Inversion | DIP | 구체 클래스가 아닌 추상에 의존해야 한다 |
// OCP 위반 예시: 새 결제 수단 추가 시 기존 코드 수정 필요
class PaymentService {
void pay(String type, int amount) {
if ("card".equals(type)) {
// 카드 결제
} else if ("cash".equals(type)) {
// 현금 결제
}
// 새 수단 추가 시 여기를 수정해야 함 → OCP 위반
}
}
// OCP 준수 예시: Strategy 패턴 적용
interface PaymentStrategy {
void pay(int amount);
}
class CardPayment implements PaymentStrategy {
public void pay(int amount) { System.out.println("카드 결제: " + amount); }
}
class CashPayment implements PaymentStrategy {
public void pay(int amount) { System.out.println("현금 결제: " + amount); }
}
// 새 결제 수단 추가 시 기존 코드 수정 없이 클래스만 추가
class KakaoPay implements PaymentStrategy {
public void pay(int amount) { System.out.println("카카오페이: " + amount); }
}
4. 자주 쓰이는 패턴 TOP 5 미리보기
4.1 Singleton (싱글톤) — 생성 패턴
애플리케이션 전체에서 단 하나의 인스턴스만 존재해야 할 때.
public class AppConfig {
// volatile: 멀티스레드 환경에서 가시성 보장
private static volatile AppConfig instance;
private String dbUrl;
private AppConfig() {
this.dbUrl = "jdbc:mysql://localhost:3306/mydb";
}
// Double-Checked Locking (DCL) 방식
public static AppConfig getInstance() {
if (instance == null) { // 1차 체크 (성능)
synchronized (AppConfig.class) {
if (instance == null) { // 2차 체크 (동시성)
instance = new AppConfig();
}
}
}
return instance;
}
public String getDbUrl() { return dbUrl; }
}
// 사용
AppConfig config1 = AppConfig.getInstance();
AppConfig config2 = AppConfig.getInstance();
System.out.println(config1 == config2); // true - 동일 인스턴스
4.2 Factory Method (팩토리 메서드) — 생성 패턴
어떤 클래스의 인스턴스를 생성할지를 서브클래스에서 결정하게 할 때.
// 추상 팩토리
abstract class NotificationFactory {
abstract Notification createNotification(String message);
// 템플릿 메서드: 생성은 서브클래스, 전송 로직은 여기서
public void sendNotification(String message) {
Notification notification = createNotification(message);
notification.send();
}
}
interface Notification {
void send();
}
class EmailNotification implements Notification {
private String message;
EmailNotification(String msg) { this.message = msg; }
public void send() { System.out.println("[EMAIL] " + message); }
}
class SmsNotification implements Notification {
private String message;
SmsNotification(String msg) { this.message = msg; }
public void send() { System.out.println("[SMS] " + message); }
}
// 구체 팩토리
class EmailFactory extends NotificationFactory {
public Notification createNotification(String message) {
return new EmailNotification(message);
}
}
class SmsFactory extends NotificationFactory {
public Notification createNotification(String message) {
return new SmsNotification(message);
}
}
// 사용
NotificationFactory factory = new EmailFactory();
factory.sendNotification("주문이 완료되었습니다."); // [EMAIL] 주문이 완료되었습니다.
4.3 Builder (빌더) — 생성 패턴
생성자 인자가 많고 선택적 매개변수가 있을 때.
public class HttpRequest {
private final String url;
private final String method;
private final String body;
private final int timeout;
private final String authToken;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.body = builder.body;
this.timeout = builder.timeout;
this.authToken = builder.authToken;
}
@Override
public String toString() {
return method + " " + url + " (timeout=" + timeout + "ms)";
}
public static class Builder {
private final String url;
private String method = "GET";
private String body = "";
private int timeout = 5000;
private String authToken = "";
public Builder(String url) { this.url = url; }
public Builder method(String m) { this.method = m; return this; }
public Builder body(String b) { this.body = b; return this; }
public Builder timeout(int t) { this.timeout = t; return this; }
public Builder auth(String token) { this.authToken = token; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
// 사용 - 가독성 높은 유창한 API
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.body("{\"name\": \"Alice\"}")
.timeout(10000)
.auth("Bearer token123")
.build();
System.out.println(request); // POST https://api.example.com/users (timeout=10000ms)
4.4 Observer (옵저버) — 행동 패턴
이벤트 발생 시 관련 객체들에게 자동으로 알림을 보낼 때.
import java.util.*;
// 이벤트 발행자 (Subject)
interface Observable {
void addObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers(String event);
}
// 이벤트 구독자 (Observer)
interface Observer {
void update(String event);
}
class OrderService implements Observable {
private final List<Observer> observers = new ArrayList<>();
public void addObserver(Observer o) { observers.add(o); }
public void removeObserver(Observer o) { observers.remove(o); }
public void notifyObservers(String event) {
observers.forEach(o -> o.update(event));
}
public void placeOrder(String orderId) {
System.out.println("주문 생성: " + orderId);
notifyObservers("ORDER_CREATED:" + orderId); // 모든 구독자에게 알림
}
}
// 구독자들
class EmailNotifier implements Observer {
public void update(String event) {
if (event.startsWith("ORDER_CREATED")) {
System.out.println("[이메일] 주문 확인 이메일 발송: " + event);
}
}
}
class InventoryManager implements Observer {
public void update(String event) {
if (event.startsWith("ORDER_CREATED")) {
System.out.println("[재고] 재고 감소 처리: " + event);
}
}
}
// 사용
OrderService orderService = new OrderService();
orderService.addObserver(new EmailNotifier());
orderService.addObserver(new InventoryManager());
orderService.placeOrder("ORD-001");
// 주문 생성: ORD-001
// [이메일] 주문 확인 이메일 발송: ORDER_CREATED:ORD-001
// [재고] 재고 감소 처리: ORDER_CREATED:ORD-001
4.5 Strategy (전략) — 행동 패턴
알고리즘을 캡슐화하여 런타임에 교체 가능하게 할 때.
// 정렬 전략 인터페이스
interface SortStrategy {
void sort(int[] arr);
}
class BubbleSort implements SortStrategy {
public void sort(int[] arr) {
System.out.println("버블 정렬 수행");
// 구현 생략
}
}
class QuickSort implements SortStrategy {
public void sort(int[] arr) {
System.out.println("퀵 정렬 수행");
// 구현 생략
}
}
// Context: 전략을 사용하는 클래스
class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
// 런타임에 전략 교체 가능
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] arr) {
strategy.sort(arr);
}
}
// 사용
Sorter sorter = new Sorter(new BubbleSort());
sorter.sort(new int[]{5, 3, 1, 4, 2}); // 버블 정렬 수행
// 데이터가 많아지면 전략 교체
sorter.setStrategy(new QuickSort());
sorter.sort(new int[]{5, 3, 1, 4, 2}); // 퀵 정렬 수행
5. 자바 표준 라이브러리의 디자인 패턴
| 패턴 | 자바 표준 라이브러리 예시 |
|---|---|
| Singleton | Runtime.getRuntime(), System |
| Factory Method | Calendar.getInstance(), List.of(), Optional.of() |
| Builder | StringBuilder, Stream.Builder, HttpRequest.newBuilder() |
| Iterator | Iterator<E>, for-each 루프 |
| Observer | EventListener, PropertyChangeListener |
| Strategy | Comparator<T>, Runnable, Callable |
| Decorator | BufferedReader(new FileReader(...)), Collections.unmodifiableList() |
| Proxy | java.lang.reflect.Proxy, Spring AOP |
| Adapter | Arrays.asList(), InputStreamReader |
| Template Method | HttpServlet.service(), AbstractList |
| Command | Runnable, Future |
| Composite | java.awt.Component (UI 트리) |
6. 스프링 프레임워크의 디자인 패턴
// IoC Container → Dependency Injection (DIP 원칙 실현)
@Component
class UserService {
private final UserRepository userRepository;
@Autowired // Strategy 패턴: 구현체를 외부에서 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// AOP → Proxy 패턴
@Aspect
@Component
class LoggingAspect {
@Around("@annotation(Loggable)") // Proxy로 메서드 감싸기
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전");
Object result = joinPoint.proceed();
System.out.println("메서드 실행 후");
return result;
}
}
// JdbcTemplate → Template Method 패턴
// 공통 로직(Connection 열기/닫기, Exception 처리)은 JdbcTemplate이 담당
// 실제 SQL만 개발자가 구현
jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) ->
new User(rs.getLong("id"), rs.getString("name"))
);
7. 패턴을 남용하면 안 되는 이유
YAGNI 원칙 (You Aren't Gonna Need It)
"지금 당장 필요하지 않은 패턴을 미리 적용하지 마세요."
과설계(Over-engineering) 예시
// 나쁜 예: 단순 덧셈에 Strategy 패턴 적용 (과설계)
interface AddStrategy {
int add(int a, int b);
}
class SimpleAddStrategy implements AddStrategy {
public int add(int a, int b) { return a + b; }
}
class Calculator {
private AddStrategy strategy = new SimpleAddStrategy();
public int calculate(int a, int b) { return strategy.add(a, b); }
}
// 좋은 예: 그냥 메서드로 구현
class SimpleCalculator {
public int add(int a, int b) { return a + b; }
}
패턴 적용 판단 기준
| 질문 | 예 | 아니오 |
|---|---|---|
| 이 코드가 변경될 것인가? | 패턴 고려 | 단순하게 구현 |
| 여러 구현체가 필요한가? | 패턴 적용 | 단일 구현으로 충분 |
| 팀원이 이 패턴을 알고 있는가? | 사용 가능 | 학습 비용 고려 |
| 패턴 없이 유지보수 어려운가? | 패턴 필요 | 단순 코드 유지 |
8. 패턴 적용 전/후 코드 비교
Before: 패턴 없는 알림 시스템
class OrderService {
public void processOrder(Order order) {
// 비즈니스 로직
order.process();
// 알림 코드가 비즈니스 로직과 뒤섞임
// 새 알림 수단 추가 시 이 메서드를 수정해야 함
EmailSender email = new EmailSender();
email.send(order.getUserEmail(), "주문 완료");
SmsSender sms = new SmsSender();
sms.send(order.getUserPhone(), "주문 완료");
// 푸시 알림 추가 시 → 이 메서드 수정 필요 (OCP 위반)
}
}
After: Observer 패턴 적용
class OrderService {
private final List<OrderEventListener> listeners = new ArrayList<>();
public void addListener(OrderEventListener l) { listeners.add(l); }
public void processOrder(Order order) {
order.process(); // 순수 비즈니스 로직만
OrderEvent event = new OrderEvent(order, "ORDER_COMPLETED");
listeners.forEach(l -> l.onOrderEvent(event)); // 알림은 리스너에 위임
}
}
// 새 알림 추가 시 OrderService 수정 없이 리스너만 추가
class PushNotificationListener implements OrderEventListener {
public void onOrderEvent(OrderEvent event) {
System.out.println("[PUSH] 주문 완료 알림 전송");
}
}
고수 팁: 패턴 학습 순서 권장
- Singleton- 가장 간단, 자주 쓰임
- Strategy- OCP의 핵심, 스프링에서 필수
- Observer- 이벤트 기반 시스템의 기초
- Factory Method / Builder- 객체 생성 패턴
- Decorator / Proxy- 스프링 AOP 이해에 필수
- Template Method- 스프링 JdbcTemplate, 테스트 등
나머지 패턴은 실제 필요할 때 학습해도 늦지 않습니다.