본문으로 건너뛰기

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, 테스트 등

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