본문으로 건너뛰기

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 ResponsibilitySRP클래스는 하나의 책임만 가져야 한다
Open-ClosedOCP확장에는 열려있고, 수정에는 닫혀있어야 한다
Liskov SubstitutionLSP자식 클래스는 부모 클래스를 대체할 수 있어야 한다
Interface SegregationISP사용하지 않는 인터페이스에 의존하지 않아야 한다
Dependency InversionDIP구체 클래스가 아닌 추상에 의존해야 한다
// 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. 자바 표준 라이브러리의 디자인 패턴

패턴자바 표준 라이브러리 예시
SingletonRuntime.getRuntime(), System
Factory MethodCalendar.getInstance(), List.of(), Optional.of()
BuilderStringBuilder, Stream.Builder, HttpRequest.newBuilder()
IteratorIterator<E>, for-each 루프
ObserverEventListener, PropertyChangeListener
StrategyComparator<T>, Runnable, Callable
DecoratorBufferedReader(new FileReader(...)), Collections.unmodifiableList()
Proxyjava.lang.reflect.Proxy, Spring AOP
AdapterArrays.asList(), InputStreamReader
Template MethodHttpServlet.service(), AbstractList
CommandRunnable, Future
Compositejava.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] 주문 완료 알림 전송");
}
}
고수 팁: 패턴 학습 순서 권장
  1. Singleton- 가장 간단, 자주 쓰임
  2. Strategy- OCP의 핵심, 스프링에서 필수
  3. Observer- 이벤트 기반 시스템의 기초
  4. Factory Method / Builder- 객체 생성 패턴
  5. Decorator / Proxy- 스프링 AOP 이해에 필수
  6. Template Method- 스프링 JdbcTemplate, 테스트 등

나머지 패턴은 실제 필요할 때 학습해도 늦지 않습니다.