GoF(Gang of Four) 디자인 패턴은 소프트웨어 설계의 바이블이다. 1994년 출판된 “Design Patterns: Elements of Reusable Object-Oriented Software”에서 에리히 감마, 리처드 헬름, 랄프 존슨, 존 블리시데스가 정리한 23가지 패턴을 Java 코드와 함께 살펴본다.


디자인 패턴이란?

디자인 패턴은 소프트웨어 설계에서 반복적으로 나타나는 문제에 대한 재사용 가능한 해결책이다.

패턴의 구성 요소

요소 설명
이름 패턴을 식별하는 명칭
문제 패턴이 해결하는 상황
해결 설계 요소들의 구조와 관계
결과 패턴 적용의 장단점

패턴 분류

┌─────────────────────────────────────────────────────────────┐
│                    GoF 디자인 패턴 (23)                      │
├───────────────┬───────────────┬─────────────────────────────┤
│ 생성 패턴 (5) │ 구조 패턴 (7) │      행동 패턴 (11)         │
├───────────────┼───────────────┼─────────────────────────────┤
│ Singleton     │ Adapter       │ Strategy      │ Observer    │
│ Factory Method│ Bridge        │ Template Method│ State      │
│ Abstract Factory│ Composite   │ Command       │ Mediator   │
│ Builder       │ Decorator     │ Iterator      │ Memento    │
│ Prototype     │ Facade        │ Chain of Resp.│ Visitor    │
│               │ Flyweight     │ Interpreter   │            │
│               │ Proxy         │               │            │
└───────────────┴───────────────┴───────────────┴────────────┘

생성 패턴 (Creational Patterns)

객체 생성 메커니즘을 다루며, 시스템이 어떤 구체 클래스를 사용하는지 감추고 객체 생성의 유연성을 높인다.


Singleton (싱글턴)

클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 전역 접근점을 제공한다.

문제 상황

  • 데이터베이스 연결 풀, 로깅, 설정 관리 등 하나의 인스턴스만 필요한 경우
  • 여러 인스턴스가 생성되면 리소스 낭비나 일관성 문제 발생

구현 방식

1. Eager Initialization (즉시 초기화)

public class EagerSingleton {
    // 클래스 로딩 시점에 인스턴스 생성
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {
        // private 생성자로 외부 생성 방지
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

2. Lazy Initialization with Double-Checked Locking

public class LazyDoubleCheckSingleton {
    // volatile: 메모리 가시성 보장
    private static volatile LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) {                    // 1차 검사 (락 없이)
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {            // 2차 검사 (락 획득 후)
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

3. Initialization-on-demand Holder (권장)

public class HolderSingleton {
    private HolderSingleton() {}

    // static inner class는 외부 클래스 로딩 시 초기화되지 않음
    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;  // 이 시점에 Holder 클래스 로딩 및 초기화
    }
}

4. Enum Singleton (가장 안전)

public enum EnumSingleton {
    INSTANCE;

    private final Connection connection;

    EnumSingleton() {
        // 초기화 로직
        this.connection = createConnection();
    }

    private Connection createConnection() {
        // 데이터베이스 연결 생성
        return null;
    }

    public Connection getConnection() {
        return connection;
    }
}

// 사용
EnumSingleton.INSTANCE.getConnection();

실무 예시: Spring의 싱글턴 빈

@Configuration
public class AppConfig {

    @Bean  // 기본적으로 싱글턴 스코프
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        return new HikariDataSource(config);
    }
}

주의사항

장점:
- 인스턴스가 하나임을 보장
- 전역 접근점 제공
- 지연 초기화 가능

단점:
- 전역 상태로 인한 테스트 어려움
- 멀티스레드 환경에서 동기화 필요
- 단일 책임 원칙 위반 가능성

Factory Method (팩토리 메서드)

객체 생성을 서브클래스에 위임하여, 생성할 객체의 클래스를 서브클래스가 결정하게 한다.

문제 상황

// 문제: 클라이언트 코드가 구체 클래스에 의존
public class OrderService {
    public void processOrder(String type) {
        Notification notification;

        // 타입에 따라 직접 객체 생성 - OCP 위반
        if (type.equals("email")) {
            notification = new EmailNotification();
        } else if (type.equals("sms")) {
            notification = new SmsNotification();
        } else if (type.equals("push")) {
            notification = new PushNotification();
        }

        notification.send();
    }
}

팩토리 메서드 적용

// 1. Product 인터페이스
public interface Notification {
    void send(String message);
}

// 2. Concrete Products
public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("이메일 발송: " + message);
    }
}

public class SmsNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("SMS 발송: " + message);
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("푸시 알림: " + message);
    }
}

// 3. Creator (추상 팩토리)
public abstract class NotificationFactory {

    // 팩토리 메서드 - 서브클래스에서 구현
    public abstract Notification createNotification();

    // 템플릿 메서드처럼 사용 가능
    public void notify(String message) {
        Notification notification = createNotification();
        notification.send(message);
    }
}

// 4. Concrete Creators
public class EmailNotificationFactory extends NotificationFactory {
    @Override
    public Notification createNotification() {
        return new EmailNotification();
    }
}

public class SmsNotificationFactory extends NotificationFactory {
    @Override
    public Notification createNotification() {
        return new SmsNotification();
    }
}

// 5. 사용
public class Client {
    public static void main(String[] args) {
        NotificationFactory factory = new EmailNotificationFactory();
        factory.notify("주문이 완료되었습니다.");

        factory = new SmsNotificationFactory();
        factory.notify("배송이 시작되었습니다.");
    }
}

Simple Factory (변형)

// 정적 팩토리 메서드
public class NotificationSimpleFactory {

    public static Notification create(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailNotification();
            case "sms" -> new SmsNotification();
            case "push" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// 사용
Notification notification = NotificationSimpleFactory.create("email");

실무 예시: Spring의 BeanFactory

public interface BeanFactory {
    Object getBean(String name);
    <T> T getBean(Class<T> requiredType);
    // ...
}

// ApplicationContext가 BeanFactory의 구현체 역할
@Component
public class OrderService {

    @Autowired
    private ApplicationContext context;

    public void process() {
        // 팩토리를 통한 빈 조회
        PaymentProcessor processor = context.getBean(PaymentProcessor.class);
        processor.process();
    }
}

Abstract Factory (추상 팩토리)

관련된 객체들의 가족(family)을 생성하기 위한 인터페이스를 제공한다.

팩토리 메서드 vs 추상 팩토리

Factory Method:
- 하나의 제품을 생성
- 상속을 통한 객체 생성

Abstract Factory:
- 관련된 제품군을 생성
- 구성(Composition)을 통한 객체 생성

구현 예시: UI 컴포넌트 팩토리

// 1. 추상 제품들
public interface Button {
    void render();
    void onClick();
}

public interface Checkbox {
    void render();
    void toggle();
}

public interface TextField {
    void render();
    String getValue();
}

// 2. Windows 구현
public class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Windows 스타일 버튼 렌더링");
    }

    @Override
    public void onClick() {
        System.out.println("Windows 버튼 클릭");
    }
}

public class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Windows 스타일 체크박스 렌더링");
    }

    @Override
    public void toggle() {
        System.out.println("Windows 체크박스 토글");
    }
}

// 3. MacOS 구현
public class MacButton implements Button {
    @Override
    public void render() {
        System.out.println("Mac 스타일 버튼 렌더링");
    }

    @Override
    public void onClick() {
        System.out.println("Mac 버튼 클릭");
    }
}

public class MacCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Mac 스타일 체크박스 렌더링");
    }

    @Override
    public void toggle() {
        System.out.println("Mac 체크박스 토글");
    }
}

// 4. 추상 팩토리
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
    TextField createTextField();
}

// 5. 구체 팩토리들
public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }

    @Override
    public TextField createTextField() {
        return new WindowsTextField();
    }
}

public class MacUIFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }

    @Override
    public TextField createTextField() {
        return new MacTextField();
    }
}

// 6. 클라이언트 코드
public class Application {
    private final Button button;
    private final Checkbox checkbox;

    public Application(UIFactory factory) {
        // 구체 클래스에 의존하지 않음
        this.button = factory.createButton();
        this.checkbox = factory.createCheckbox();
    }

    public void render() {
        button.render();
        checkbox.render();
    }
}

// 7. 사용
public class Main {
    public static void main(String[] args) {
        UIFactory factory;

        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("win")) {
            factory = new WindowsUIFactory();
        } else {
            factory = new MacUIFactory();
        }

        Application app = new Application(factory);
        app.render();
    }
}

실무 예시: JDBC DriverManager

// DriverManager는 추상 팩토리 패턴을 사용
// 각 DB 벤더가 Connection, Statement, ResultSet 등 관련 객체군을 생성

Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mydb", "user", "password"
);

// MySQL Driver가 MySQL용 Connection, Statement 등을 생성
Statement stmt = conn.createStatement();  // MySQLStatement
ResultSet rs = stmt.executeQuery("SELECT * FROM users");  // MySQLResultSet

Builder (빌더)

복잡한 객체의 생성 과정과 표현을 분리하여, 같은 생성 과정에서 다른 표현을 만들 수 있게 한다.

문제 상황

// 생성자 파라미터가 많은 경우
public class User {
    public User(String firstName, String lastName, int age,
                String phone, String address, String email,
                boolean isActive, LocalDate birthDate,
                String department, String position) {
        // 파라미터 순서 혼동 가능
        // 선택적 파라미터 처리 어려움
    }
}

// 텔레스코핑 생성자 안티패턴
public User(String firstName, String lastName) { ... }
public User(String firstName, String lastName, int age) { ... }
public User(String firstName, String lastName, int age, String phone) { ... }
// 생성자가 기하급수적으로 증가

Builder 패턴 적용

public class User {
    // 필수 필드
    private final String firstName;
    private final String lastName;

    // 선택적 필드
    private final int age;
    private final String phone;
    private final String email;
    private final String address;
    private final boolean isActive;
    private final LocalDate birthDate;

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.email = builder.email;
        this.address = builder.address;
        this.isActive = builder.isActive;
        this.birthDate = builder.birthDate;
    }

    public static class Builder {
        // 필수
        private final String firstName;
        private final String lastName;

        // 선택 (기본값 설정)
        private int age = 0;
        private String phone = "";
        private String email = "";
        private String address = "";
        private boolean isActive = true;
        private LocalDate birthDate = null;

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder active(boolean isActive) {
            this.isActive = isActive;
            return this;
        }

        public Builder birthDate(LocalDate birthDate) {
            this.birthDate = birthDate;
            return this;
        }

        public User build() {
            // 유효성 검증
            validate();
            return new User(this);
        }

        private void validate() {
            if (firstName == null || firstName.isBlank()) {
                throw new IllegalStateException("firstName is required");
            }
            if (age < 0) {
                throw new IllegalStateException("age cannot be negative");
            }
        }
    }

    // Getters...
}

// 사용
User user = new User.Builder("홍", "길동")
    .age(30)
    .email("hong@example.com")
    .phone("010-1234-5678")
    .active(true)
    .build();

Lombok @Builder

@Builder
@Getter
public class User {
    private final String firstName;
    private final String lastName;

    @Builder.Default
    private int age = 0;

    @Builder.Default
    private boolean isActive = true;

    private String email;
    private String phone;
}

// 사용
User user = User.builder()
    .firstName("홍")
    .lastName("길동")
    .email("hong@example.com")
    .build();

Director 패턴

// 복잡한 빌드 과정을 Director가 관리
public class UserDirector {

    public User createDefaultAdmin(User.Builder builder) {
        return builder
            .active(true)
            .build();
    }

    public User createGuestUser() {
        return new User.Builder("Guest", "User")
            .active(false)
            .build();
    }
}

실무 예시: StringBuilder, Stream.Builder

// StringBuilder
String result = new StringBuilder()
    .append("Hello")
    .append(" ")
    .append("World")
    .toString();

// Stream.Builder
Stream<String> stream = Stream.<String>builder()
    .add("one")
    .add("two")
    .add("three")
    .build();

// HttpRequest.Builder (Java 11+)
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .timeout(Duration.ofSeconds(10))
    .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
    .build();

Prototype (프로토타입)

기존 객체를 복제하여 새로운 객체를 생성한다.

사용 시점

  • 객체 생성 비용이 높을 때 (DB 조회, 네트워크 요청 등)
  • 런타임에 객체 타입이 결정될 때
  • 클래스 계층이 복잡할 때

구현

// 1. Prototype 인터페이스
public interface Prototype<T> extends Cloneable {
    T clone();
}

// 2. 구체 프로토타입
public class Document implements Prototype<Document> {
    private String title;
    private String content;
    private List<String> authors;
    private Map<String, Object> metadata;

    public Document(String title, String content) {
        this.title = title;
        this.content = content;
        this.authors = new ArrayList<>();
        this.metadata = new HashMap<>();
    }

    // 복사 생성자
    private Document(Document source) {
        this.title = source.title;
        this.content = source.content;
        // 깊은 복사
        this.authors = new ArrayList<>(source.authors);
        this.metadata = new HashMap<>(source.metadata);
    }

    @Override
    public Document clone() {
        return new Document(this);
    }

    // Getters and Setters...
}

// 3. 사용
Document original = new Document("제목", "내용");
original.getAuthors().add("작성자1");

Document copy = original.clone();
copy.setTitle("복사본 제목");
copy.getAuthors().add("작성자2");

// original.authors: [작성자1]
// copy.authors: [작성자1, 작성자2]

프로토타입 레지스트리

public class DocumentRegistry {
    private final Map<String, Document> prototypes = new HashMap<>();

    public void register(String key, Document prototype) {
        prototypes.put(key, prototype);
    }

    public Document create(String key) {
        Document prototype = prototypes.get(key);
        if (prototype == null) {
            throw new IllegalArgumentException("Unknown prototype: " + key);
        }
        return prototype.clone();
    }
}

// 사용
DocumentRegistry registry = new DocumentRegistry();

// 템플릿 등록
Document reportTemplate = new Document("월간 보고서", "");
reportTemplate.getMetadata().put("type", "report");
registry.register("monthly-report", reportTemplate);

// 복제하여 사용
Document januaryReport = registry.create("monthly-report");
januaryReport.setTitle("2024년 1월 보고서");

얕은 복사 vs 깊은 복사

public class ShallowVsDeep {

    // 얕은 복사 - 참조만 복사
    public static Document shallowCopy(Document source) {
        Document copy = new Document(source.getTitle(), source.getContent());
        copy.setAuthors(source.getAuthors());  // 같은 List 참조
        return copy;
    }

    // 깊은 복사 - 객체 전체 복사
    public static Document deepCopy(Document source) {
        Document copy = new Document(source.getTitle(), source.getContent());
        copy.setAuthors(new ArrayList<>(source.getAuthors()));  // 새 List 생성

        // 중첩 객체도 복사
        Map<String, Object> metaCopy = new HashMap<>();
        for (Map.Entry<String, Object> entry : source.getMetadata().entrySet()) {
            // 값이 컬렉션이면 그것도 복사
            if (entry.getValue() instanceof List) {
                metaCopy.put(entry.getKey(), new ArrayList<>((List<?>) entry.getValue()));
            } else {
                metaCopy.put(entry.getKey(), entry.getValue());
            }
        }
        copy.setMetadata(metaCopy);

        return copy;
    }
}

구조 패턴 (Structural Patterns)

클래스나 객체를 조합하여 더 큰 구조를 만드는 방법을 다룬다.


Adapter (어댑터)

호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있게 한다.

문제 상황

// 기존 시스템의 인터페이스
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

// 새로 도입하려는 외부 라이브러리
public class VlcPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }
}

public class Mp4Player {
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }
}

어댑터 적용 (Object Adapter)

// 고급 미디어 플레이어 인터페이스
public interface AdvancedMediaPlayer {
    void playAdvanced(String fileName);
}

// VLC 어댑터
public class VlcPlayerAdapter implements AdvancedMediaPlayer {
    private final VlcPlayer vlcPlayer;

    public VlcPlayerAdapter() {
        this.vlcPlayer = new VlcPlayer();
    }

    @Override
    public void playAdvanced(String fileName) {
        vlcPlayer.playVlc(fileName);
    }
}

// MP4 어댑터
public class Mp4PlayerAdapter implements AdvancedMediaPlayer {
    private final Mp4Player mp4Player;

    public Mp4PlayerAdapter() {
        this.mp4Player = new Mp4Player();
    }

    @Override
    public void playAdvanced(String fileName) {
        mp4Player.playMp4(fileName);
    }
}

// 통합 미디어 플레이어
public class AudioPlayer implements MediaPlayer {

    @Override
    public void play(String audioType, String fileName) {
        AdvancedMediaPlayer player = switch (audioType.toLowerCase()) {
            case "vlc" -> new VlcPlayerAdapter();
            case "mp4" -> new Mp4PlayerAdapter();
            default -> throw new IllegalArgumentException("Unsupported format: " + audioType);
        };
        player.playAdvanced(fileName);
    }
}

Class Adapter (상속 방식)

// 다중 상속이 불가능한 Java에서는 인터페이스 + 상속 조합
public class VlcClassAdapter extends VlcPlayer implements AdvancedMediaPlayer {

    @Override
    public void playAdvanced(String fileName) {
        // 부모 클래스의 메서드 호출
        super.playVlc(fileName);
    }
}

실무 예시: Spring HandlerAdapter

// Spring MVC의 HandlerAdapter
public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request,
                        HttpServletResponse response,
                        Object handler) throws Exception;
}

// 각 핸들러 타입에 맞는 어댑터
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof HandlerMethod;
    }

    @Override
    public ModelAndView handle(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler) throws Exception {
        // @RequestMapping 메서드 실행
        return invokeHandlerMethod(request, response, (HandlerMethod) handler);
    }
}

실무 예시: Enumeration to Iterator

// Java의 Collections.enumeration()과 반대 방향
public class EnumerationIteratorAdapter<T> implements Iterator<T> {
    private final Enumeration<T> enumeration;

    public EnumerationIteratorAdapter(Enumeration<T> enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    @Override
    public T next() {
        return enumeration.nextElement();
    }
}

Bridge (브리지)

구현에서 추상을 분리하여 독립적으로 변형할 수 있게 한다.

문제 상황

상속으로 확장하면 조합 폭발:
- Shape: Circle, Square, Triangle
- Color: Red, Blue, Green

상속: RedCircle, BlueCircle, GreenCircle, RedSquare, BlueSquare...
→ 3 x 3 = 9개 클래스

브리지: Shape + Color 조합
→ 3 + 3 = 6개 클래스

브리지 패턴 적용

// 1. Implementor - 구현 계층
public interface Color {
    String fill();
}

public class Red implements Color {
    @Override
    public String fill() {
        return "빨간색";
    }
}

public class Blue implements Color {
    @Override
    public String fill() {
        return "파란색";
    }
}

// 2. Abstraction - 추상 계층
public abstract class Shape {
    protected Color color;  // Bridge: 구현을 참조

    protected Shape(Color color) {
        this.color = color;
    }

    public abstract void draw();
}

// 3. Refined Abstraction
public class Circle extends Shape {
    private final int radius;

    public Circle(int radius, Color color) {
        super(color);
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println(color.fill() + " 원 (반지름: " + radius + ")");
    }
}

public class Square extends Shape {
    private final int side;

    public Square(int side, Color color) {
        super(color);
        this.side = side;
    }

    @Override
    public void draw() {
        System.out.println(color.fill() + " 사각형 (변: " + side + ")");
    }
}

// 4. 사용
Shape redCircle = new Circle(10, new Red());
Shape blueSquare = new Square(20, new Blue());

redCircle.draw();   // 빨간색 원 (반지름: 10)
blueSquare.draw();  // 파란색 사각형 (변: 20)

실무 예시: 리모컨과 디바이스

// Device 구현
public interface Device {
    void turnOn();
    void turnOff();
    void setVolume(int volume);
    int getVolume();
}

public class TV implements Device {
    private boolean on = false;
    private int volume = 30;

    @Override
    public void turnOn() { on = true; }

    @Override
    public void turnOff() { on = false; }

    @Override
    public void setVolume(int volume) { this.volume = volume; }

    @Override
    public int getVolume() { return volume; }
}

public class Radio implements Device {
    // 비슷한 구현...
}

// Remote 추상화
public abstract class Remote {
    protected Device device;

    protected Remote(Device device) {
        this.device = device;
    }

    public void togglePower() {
        // device 상태에 따라 on/off
    }

    public void volumeUp() {
        device.setVolume(device.getVolume() + 10);
    }

    public void volumeDown() {
        device.setVolume(device.getVolume() - 10);
    }
}

// 고급 리모컨
public class AdvancedRemote extends Remote {
    public AdvancedRemote(Device device) {
        super(device);
    }

    public void mute() {
        device.setVolume(0);
    }
}

// 사용
Device tv = new TV();
Remote remote = new AdvancedRemote(tv);
remote.volumeUp();
((AdvancedRemote) remote).mute();

실무 예시: JDBC Driver

// DriverManager(추상) + Driver(구현)의 브리지 구조
// 추상: java.sql.Connection, Statement, ResultSet
// 구현: MySQL Driver, Oracle Driver, PostgreSQL Driver

// 동일한 JDBC API로 다양한 DB 사용
Connection mysqlConn = DriverManager.getConnection("jdbc:mysql://...");
Connection oracleConn = DriverManager.getConnection("jdbc:oracle://...");

Composite (컴포지트)

객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 다룬다.

구현

// 1. Component
public interface FileSystemComponent {
    String getName();
    long getSize();
    void print(String prefix);
}

// 2. Leaf
public class File implements FileSystemComponent {
    private final String name;
    private final long size;

    public File(String name, long size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() { return name; }

    @Override
    public long getSize() { return size; }

    @Override
    public void print(String prefix) {
        System.out.println(prefix + "📄 " + name + " (" + size + " bytes)");
    }
}

// 3. Composite
public class Directory implements FileSystemComponent {
    private final String name;
    private final List<FileSystemComponent> children = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void add(FileSystemComponent component) {
        children.add(component);
    }

    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public String getName() { return name; }

    @Override
    public long getSize() {
        return children.stream()
            .mapToLong(FileSystemComponent::getSize)
            .sum();
    }

    @Override
    public void print(String prefix) {
        System.out.println(prefix + "📁 " + name + " (" + getSize() + " bytes)");
        for (FileSystemComponent child : children) {
            child.print(prefix + "  ");
        }
    }
}

// 4. 사용
Directory root = new Directory("root");
Directory src = new Directory("src");
Directory test = new Directory("test");

src.add(new File("Main.java", 1000));
src.add(new File("Utils.java", 500));

test.add(new File("MainTest.java", 800));

root.add(src);
root.add(test);
root.add(new File("README.md", 200));

root.print("");
// 출력:
// 📁 root (2500 bytes)
//   📁 src (1500 bytes)
//     📄 Main.java (1000 bytes)
//     📄 Utils.java (500 bytes)
//   📁 test (800 bytes)
//     📄 MainTest.java (800 bytes)
//   📄 README.md (200 bytes)

실무 예시: 조직도

public interface OrganizationComponent {
    String getName();
    void showDetails();
    int getHeadCount();
}

public class Employee implements OrganizationComponent {
    private final String name;
    private final String position;

    // Leaf 구현...

    @Override
    public int getHeadCount() { return 1; }
}

public class Department implements OrganizationComponent {
    private final String name;
    private final List<OrganizationComponent> members = new ArrayList<>();

    // Composite 구현...

    @Override
    public int getHeadCount() {
        return members.stream()
            .mapToInt(OrganizationComponent::getHeadCount)
            .sum();
    }
}

Decorator (데코레이터)

객체에 동적으로 새로운 책임을 추가한다. 상속 없이 기능을 확장할 수 있다.

상속 vs 데코레이터

상속의 문제:
- 컴파일 타임에 고정
- 조합 폭발 (MilkCoffee, SugarCoffee, MilkSugarCoffee...)
- 단일 상속 제한

데코레이터의 장점:
- 런타임에 동적 조합
- 단일 책임 원칙 준수
- 기존 코드 변경 없이 확장

구현

// 1. Component
public interface Coffee {
    String getDescription();
    double getCost();
}

// 2. Concrete Component
public class Espresso implements Coffee {
    @Override
    public String getDescription() { return "에스프레소"; }

    @Override
    public double getCost() { return 2000; }
}

public class Americano implements Coffee {
    @Override
    public String getDescription() { return "아메리카노"; }

    @Override
    public double getCost() { return 3000; }
}

// 3. Decorator (추상)
public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee coffee;

    protected CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

// 4. Concrete Decorators
public class Milk extends CoffeeDecorator {
    public Milk(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", 우유";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 500;
    }
}

public class Whip extends CoffeeDecorator {
    public Whip(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", 휘핑크림";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 700;
    }
}

public class Shot extends CoffeeDecorator {
    public Shot(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", 샷 추가";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 500;
    }
}

// 5. 사용
Coffee order = new Espresso();
order = new Milk(order);
order = new Whip(order);
order = new Shot(order);

System.out.println(order.getDescription());  // 에스프레소, 우유, 휘핑크림, 샷 추가
System.out.println(order.getCost());         // 3700.0

// Fluent 방식
Coffee order2 = new Shot(new Whip(new Milk(new Americano())));

실무 예시: Java I/O

// Java I/O는 데코레이터 패턴의 대표적 예
InputStream is = new FileInputStream("file.txt");

// BufferedInputStream이 FileInputStream을 데코레이트
InputStream buffered = new BufferedInputStream(is);

// DataInputStream이 BufferedInputStream을 데코레이트
DataInputStream data = new DataInputStream(buffered);

// 한 줄로 체이닝
DataInputStream stream = new DataInputStream(
    new BufferedInputStream(
        new FileInputStream("file.txt")
    )
);

실무 예시: Spring의 HandlerInterceptor

// 인터셉터들이 핸들러를 데코레이트하는 것처럼 동작
public class LoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) {
        log.info("Request: {} {}", request.getMethod(), request.getRequestURI());
        return true;
    }
}

Facade (퍼사드)

서브시스템에 대한 통합된 인터페이스를 제공하여 서브시스템을 더 쉽게 사용할 수 있게 한다.

구현

// 복잡한 서브시스템들
public class VideoFile {
    private final String name;
    private final String codecType;

    public VideoFile(String name) {
        this.name = name;
        this.codecType = name.substring(name.lastIndexOf('.') + 1);
    }
    // ...
}

public class CodecFactory {
    public static Codec extract(VideoFile file) {
        String type = file.getCodecType();
        if (type.equals("mp4")) {
            return new MPEG4CompressionCodec();
        } else {
            return new OggCompressionCodec();
        }
    }
}

public class BitrateReader {
    public static VideoFile read(VideoFile file, Codec codec) {
        System.out.println("BitrateReader: 파일 읽기...");
        return file;
    }

    public static VideoFile convert(VideoFile buffer, Codec codec) {
        System.out.println("BitrateReader: 변환 중...");
        return buffer;
    }
}

public class AudioMixer {
    public File fix(VideoFile result) {
        System.out.println("AudioMixer: 오디오 믹싱...");
        return new File("");
    }
}

// Facade
public class VideoConversionFacade {

    public File convertVideo(String fileName, String format) {
        System.out.println("VideoConversionFacade: 변환 시작");

        VideoFile file = new VideoFile(fileName);
        Codec sourceCodec = CodecFactory.extract(file);

        Codec destinationCodec;
        if (format.equals("mp4")) {
            destinationCodec = new MPEG4CompressionCodec();
        } else {
            destinationCodec = new OggCompressionCodec();
        }

        VideoFile buffer = BitrateReader.read(file, sourceCodec);
        VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);

        File result = new AudioMixer().fix(intermediateResult);

        System.out.println("VideoConversionFacade: 변환 완료");
        return result;
    }
}

// 클라이언트 코드 - 매우 단순해짐
public class Client {
    public static void main(String[] args) {
        VideoConversionFacade converter = new VideoConversionFacade();
        File mp4 = converter.convertVideo("video.ogg", "mp4");
    }
}

실무 예시: SLF4J

// SLF4J는 다양한 로깅 프레임워크에 대한 Facade
// Log4j, Logback, JUL 등 실제 구현을 감춤

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public void doSomething() {
        log.info("작업 시작");  // 내부적으로 어떤 로거가 쓰이는지 모름
    }
}

실무 예시: Spring의 JdbcTemplate

// JdbcTemplate은 JDBC의 복잡함을 숨기는 Facade
@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public User findById(Long id) {
        // Connection 획득, PreparedStatement 생성, ResultSet 처리,
        // 예외 변환, 자원 해제 등을 모두 JdbcTemplate이 처리
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            (rs, rowNum) -> new User(
                rs.getLong("id"),
                rs.getString("name")
            ),
            id
        );
    }
}

Flyweight (플라이웨이트)

많은 수의 유사한 객체를 효율적으로 공유하여 메모리 사용을 최소화한다.

개념

Intrinsic State (내재 상태):
- 객체 간에 공유되는 불변 데이터
- Flyweight 객체 내부에 저장

Extrinsic State (외재 상태):
- 각 객체마다 다른 컨텍스트 의존 데이터
- 클라이언트가 전달

구현

// 1. Flyweight - 공유되는 객체
public class TreeType {
    private final String name;
    private final String color;
    private final String texture;  // 용량이 큰 텍스처 데이터

    public TreeType(String name, String color, String texture) {
        this.name = name;
        this.color = color;
        this.texture = texture;
    }

    public void draw(int x, int y) {
        System.out.printf("%s 나무를 (%d, %d)에 그립니다. 색상: %s%n",
            name, x, y, color);
    }
}

// 2. Flyweight Factory
public class TreeFactory {
    private static final Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + "_" + color + "_" + texture;

        if (!treeTypes.containsKey(key)) {
            treeTypes.put(key, new TreeType(name, color, texture));
            System.out.println("새로운 TreeType 생성: " + key);
        }

        return treeTypes.get(key);
    }

    public static int getTypeCount() {
        return treeTypes.size();
    }
}

// 3. Context - 외재 상태를 가진 객체
public class Tree {
    private final int x;
    private final int y;
    private final TreeType type;  // Flyweight 참조

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void draw() {
        type.draw(x, y);
    }
}

// 4. 숲 (클라이언트)
public class Forest {
    private final List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, String color, String texture) {
        TreeType type = TreeFactory.getTreeType(name, color, texture);
        Tree tree = new Tree(x, y, type);
        trees.add(tree);
    }

    public void draw() {
        for (Tree tree : trees) {
            tree.draw();
        }
    }
}

// 5. 사용
Forest forest = new Forest();

// 100만 그루의 나무를 심어도 TreeType은 몇 개만 생성
for (int i = 0; i < 1_000_000; i++) {
    int x = (int) (Math.random() * 1000);
    int y = (int) (Math.random() * 1000);

    // 나무 종류는 3가지뿐
    String[] names = {"소나무", "참나무", "단풍나무"};
    String[] colors = {"초록", "진초록", "빨강"};

    int typeIndex = i % 3;
    forest.plantTree(x, y, names[typeIndex], colors[typeIndex], "texture_" + typeIndex);
}

System.out.println("TreeType 개수: " + TreeFactory.getTypeCount());  // 3
// 100만 Tree 객체가 3개의 TreeType만 공유

실무 예시: String Pool

// Java의 String Pool이 Flyweight 패턴
String s1 = "hello";  // Pool에서 가져옴
String s2 = "hello";  // 같은 객체 참조

System.out.println(s1 == s2);  // true

String s3 = new String("hello");  // 새 객체 생성
System.out.println(s1 == s3);  // false

String s4 = s3.intern();  // Pool로 이동
System.out.println(s1 == s4);  // true

실무 예시: Integer Cache

// Integer -128 ~ 127은 캐시됨
Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false

Proxy (프록시)

다른 객체에 대한 대리자를 제공하여 접근을 제어한다.

프록시 종류

종류 목적
Virtual Proxy 지연 로딩 (비용이 큰 객체)
Protection Proxy 접근 권한 제어
Remote Proxy 원격 객체 접근
Caching Proxy 결과 캐싱
Logging Proxy 로깅 추가

Virtual Proxy (지연 로딩)

// 1. Subject
public interface Image {
    void display();
}

// 2. Real Subject
public class HighResolutionImage implements Image {
    private final String filename;
    private byte[] imageData;

    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadFromDisk();  // 무거운 작업
    }

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
        // 실제로는 파일 읽기, 디코딩 등 시간이 걸림
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        this.imageData = new byte[1024 * 1024];  // 1MB 이미지 데이터
    }

    @Override
    public void display() {
        System.out.println("Displaying: " + filename);
    }
}

// 3. Proxy
public class ImageProxy implements Image {
    private final String filename;
    private HighResolutionImage realImage;  // 지연 생성

    public ImageProxy(String filename) {
        this.filename = filename;
        // 실제 이미지는 아직 로딩하지 않음
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new HighResolutionImage(filename);
        }
        realImage.display();
    }
}

// 4. 사용
Image image1 = new ImageProxy("photo1.jpg");
Image image2 = new ImageProxy("photo2.jpg");
Image image3 = new ImageProxy("photo3.jpg");

// 아직 아무것도 로딩되지 않음
System.out.println("Images created");

// 이 시점에만 photo1.jpg 로딩
image1.display();

Protection Proxy (접근 제어)

public interface Document {
    void read();
    void write(String content);
}

public class SecureDocument implements Document {
    private String content;

    @Override
    public void read() {
        System.out.println("Content: " + content);
    }

    @Override
    public void write(String content) {
        this.content = content;
    }
}

public class DocumentProxy implements Document {
    private final SecureDocument document;
    private final String currentUser;
    private final Set<String> admins = Set.of("admin", "manager");

    public DocumentProxy(SecureDocument document, String currentUser) {
        this.document = document;
        this.currentUser = currentUser;
    }

    @Override
    public void read() {
        document.read();  // 읽기는 모두 허용
    }

    @Override
    public void write(String content) {
        if (admins.contains(currentUser)) {
            document.write(content);
        } else {
            throw new SecurityException("권한이 없습니다: " + currentUser);
        }
    }
}

Caching Proxy

public interface UserService {
    User findById(Long id);
}

public class UserServiceImpl implements UserService {
    @Override
    public User findById(Long id) {
        System.out.println("DB 조회: " + id);
        // 실제 DB 조회
        return new User(id, "User " + id);
    }
}

public class CachingUserServiceProxy implements UserService {
    private final UserService target;
    private final Map<Long, User> cache = new ConcurrentHashMap<>();

    public CachingUserServiceProxy(UserService target) {
        this.target = target;
    }

    @Override
    public User findById(Long id) {
        return cache.computeIfAbsent(id, target::findById);
    }

    public void evict(Long id) {
        cache.remove(id);
    }
}

실무 예시: Spring AOP Proxy

// Spring은 @Transactional, @Cacheable 등을 프록시로 구현

@Service
public class OrderService {

    @Transactional  // 프록시가 트랜잭션 관리
    public void createOrder(Order order) {
        // 비즈니스 로직
    }

    @Cacheable("users")  // 프록시가 캐시 관리
    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}

// Spring이 생성하는 프록시 (개념적)
public class OrderService$$Proxy extends OrderService {

    @Override
    public void createOrder(Order order) {
        TransactionStatus tx = txManager.getTransaction();
        try {
            super.createOrder(order);
            txManager.commit(tx);
        } catch (Exception e) {
            txManager.rollback(tx);
            throw e;
        }
    }
}

JDK Dynamic Proxy

public class LoggingInvocationHandler implements InvocationHandler {
    private final Object target;

    public LoggingInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After: " + method.getName());
        return result;
    }
}

// 프록시 생성
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[] { UserService.class },
    new LoggingInvocationHandler(new UserServiceImpl())
);

proxy.findById(1L);
// Before: findById
// DB 조회: 1
// After: findById

행동 패턴 (Behavioral Patterns)

객체 간의 책임 분배와 알고리즘을 다룬다.


Strategy (전략)

알고리즘을 캡슐화하여 런타임에 교체할 수 있게 한다.

구현

// 1. Strategy 인터페이스
@FunctionalInterface
public interface PaymentStrategy {
    void pay(int amount);
}

// 2. Concrete Strategies
public class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 신용카드로 결제 (카드번호: " + cardNumber + ")");
    }
}

public class KakaoPayPayment implements PaymentStrategy {
    private final String phoneNumber;

    public KakaoPayPayment(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 카카오페이로 결제 (전화번호: " + phoneNumber + ")");
    }
}

public class NaverPayPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 네이버페이로 결제");
    }
}

// 3. Context
public class ShoppingCart {
    private final List<Item> items = new ArrayList<>();

    public void addItem(Item item) {
        items.add(item);
    }

    public int calculateTotal() {
        return items.stream()
            .mapToInt(Item::getPrice)
            .sum();
    }

    public void checkout(PaymentStrategy paymentStrategy) {
        int total = calculateTotal();
        paymentStrategy.pay(total);
    }
}

// 4. 사용
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("키보드", 50000));
cart.addItem(new Item("마우스", 30000));

// 결제 방식 선택
cart.checkout(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(new KakaoPayPayment("010-1234-5678"));

// 람다로 간단히
cart.checkout(amount -> System.out.println("포인트로 " + amount + "원 결제"));

실무 예시: 정렬 전략

public class SortingContext<T> {
    private Comparator<T> strategy;

    public void setStrategy(Comparator<T> strategy) {
        this.strategy = strategy;
    }

    public List<T> sort(List<T> list) {
        List<T> result = new ArrayList<>(list);
        result.sort(strategy);
        return result;
    }
}

// 사용
List<Product> products = getProducts();
SortingContext<Product> context = new SortingContext<>();

// 가격순
context.setStrategy(Comparator.comparing(Product::getPrice));
List<Product> byPrice = context.sort(products);

// 이름순
context.setStrategy(Comparator.comparing(Product::getName));
List<Product> byName = context.sort(products);

// 복합 정렬
context.setStrategy(
    Comparator.comparing(Product::getCategory)
              .thenComparing(Product::getPrice)
);

Spring에서의 Strategy

// 인터페이스로 전략 정의
public interface DiscountPolicy {
    int calculateDiscount(Order order);
}

@Component("fixedDiscount")
public class FixedDiscountPolicy implements DiscountPolicy {
    @Override
    public int calculateDiscount(Order order) {
        return 1000;
    }
}

@Component("rateDiscount")
public class RateDiscountPolicy implements DiscountPolicy {
    @Override
    public int calculateDiscount(Order order) {
        return order.getTotal() * 10 / 100;
    }
}

// 전략 주입
@Service
public class OrderService {
    private final DiscountPolicy discountPolicy;

    public OrderService(@Qualifier("rateDiscount") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

// 또는 Map으로 모든 전략 주입
@Service
public class OrderService {
    private final Map<String, DiscountPolicy> policies;

    public OrderService(Map<String, DiscountPolicy> policies) {
        this.policies = policies;
    }

    public int discount(String policyName, Order order) {
        return policies.get(policyName).calculateDiscount(order);
    }
}

Template Method (템플릿 메서드)

알고리즘의 골격을 정의하고, 일부 단계를 서브클래스에서 구현하게 한다.

구현

// 1. Abstract Class with Template Method
public abstract class DataParser {

    // 템플릿 메서드 - final로 알고리즘 골격 보호
    public final List<Record> parse(String filePath) {
        String rawData = readFile(filePath);
        String cleanedData = cleanData(rawData);
        List<Record> records = parseData(cleanedData);
        records = processRecords(records);  // Hook
        logResult(records);
        return records;
    }

    // 공통 구현
    private String readFile(String filePath) {
        System.out.println("파일 읽기: " + filePath);
        // 파일 읽기 로직
        return "raw data";
    }

    // 추상 메서드 - 서브클래스에서 구현
    protected abstract String cleanData(String rawData);
    protected abstract List<Record> parseData(String data);

    // Hook - 선택적 오버라이드
    protected List<Record> processRecords(List<Record> records) {
        return records;  // 기본: 아무것도 안 함
    }

    // 공통 구현
    private void logResult(List<Record> records) {
        System.out.println("처리 완료: " + records.size() + "건");
    }
}

// 2. Concrete Classes
public class CsvParser extends DataParser {

    @Override
    protected String cleanData(String rawData) {
        return rawData.trim().replaceAll("\\s+", " ");
    }

    @Override
    protected List<Record> parseData(String data) {
        List<Record> records = new ArrayList<>();
        String[] lines = data.split("\n");
        for (String line : lines) {
            String[] fields = line.split(",");
            records.add(new Record(fields));
        }
        return records;
    }
}

public class JsonParser extends DataParser {

    @Override
    protected String cleanData(String rawData) {
        return rawData.strip();
    }

    @Override
    protected List<Record> parseData(String data) {
        // JSON 파싱 로직
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(data, new TypeReference<>() {});
    }

    @Override
    protected List<Record> processRecords(List<Record> records) {
        // JSON 특화 처리
        return records.stream()
            .filter(Record::isValid)
            .collect(Collectors.toList());
    }
}

// 3. 사용
DataParser csvParser = new CsvParser();
csvParser.parse("data.csv");

DataParser jsonParser = new JsonParser();
jsonParser.parse("data.json");

실무 예시: Spring의 JdbcTemplate

// JdbcTemplate 내부 구현 (개념적)
public abstract class JdbcTemplate {

    public <T> T execute(ConnectionCallback<T> action) {
        Connection con = DataSourceUtils.getConnection(getDataSource());
        try {
            return action.doInConnection(con);  // 콜백
        } catch (SQLException ex) {
            throw translateException(ex);
        } finally {
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

    public <T> T query(String sql, ResultSetExtractor<T> rse) {
        return execute((Connection con) -> {
            try (Statement stmt = con.createStatement();
                 ResultSet rs = stmt.executeQuery(sql)) {
                return rse.extractData(rs);  // 서브클래스/콜백에서 구현
            }
        });
    }
}

Strategy vs Template Method

Template Method:
- 상속 기반
- 알고리즘 골격 고정, 일부 단계만 변경
- 컴파일 타임에 결정

Strategy:
- 구성(Composition) 기반
- 전체 알고리즘을 교체
- 런타임에 변경 가능

Command (커맨드)

요청을 객체로 캡슐화하여 파라미터화, 큐잉, 로깅, 취소 기능을 제공한다.

구현

// 1. Command 인터페이스
public interface Command {
    void execute();
    void undo();  // 실행 취소
}

// 2. Receiver
public class TextEditor {
    private StringBuilder content = new StringBuilder();

    public void write(String text) {
        content.append(text);
    }

    public void delete(int length) {
        if (length > content.length()) {
            length = content.length();
        }
        content.delete(content.length() - length, content.length());
    }

    public String getContent() {
        return content.toString();
    }
}

// 3. Concrete Commands
public class WriteCommand implements Command {
    private final TextEditor editor;
    private final String text;

    public WriteCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
    }

    @Override
    public void execute() {
        editor.write(text);
    }

    @Override
    public void undo() {
        editor.delete(text.length());
    }
}

public class DeleteCommand implements Command {
    private final TextEditor editor;
    private final int length;
    private String deletedText;

    public DeleteCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.length = length;
    }

    @Override
    public void execute() {
        String content = editor.getContent();
        deletedText = content.substring(
            Math.max(0, content.length() - length)
        );
        editor.delete(length);
    }

    @Override
    public void undo() {
        editor.write(deletedText);
    }
}

// 4. Invoker
public class CommandHistory {
    private final Deque<Command> history = new ArrayDeque<>();
    private final Deque<Command> redoStack = new ArrayDeque<>();

    public void execute(Command command) {
        command.execute();
        history.push(command);
        redoStack.clear();  // 새 명령 실행 시 redo 스택 초기화
    }

    public void undo() {
        if (!history.isEmpty()) {
            Command command = history.pop();
            command.undo();
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            command.execute();
            history.push(command);
        }
    }
}

// 5. 사용
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();

history.execute(new WriteCommand(editor, "Hello "));
history.execute(new WriteCommand(editor, "World"));
System.out.println(editor.getContent());  // Hello World

history.undo();
System.out.println(editor.getContent());  // Hello

history.redo();
System.out.println(editor.getContent());  // Hello World

실무 예시: 트랜잭션 스크립트

public interface TransactionCommand {
    void execute() throws Exception;
    void rollback();
}

public class CreateOrderCommand implements TransactionCommand {
    private final OrderService orderService;
    private final Order order;
    private Long createdOrderId;

    @Override
    public void execute() throws Exception {
        createdOrderId = orderService.create(order);
    }

    @Override
    public void rollback() {
        if (createdOrderId != null) {
            orderService.delete(createdOrderId);
        }
    }
}

// 여러 커맨드를 트랜잭션으로 묶기
public class TransactionExecutor {

    public void executeAll(List<TransactionCommand> commands) {
        List<TransactionCommand> executed = new ArrayList<>();

        try {
            for (TransactionCommand command : commands) {
                command.execute();
                executed.add(command);
            }
        } catch (Exception e) {
            // 역순으로 롤백
            Collections.reverse(executed);
            for (TransactionCommand command : executed) {
                command.rollback();
            }
            throw new RuntimeException("Transaction failed", e);
        }
    }
}

Observer (옵저버)

객체 상태가 변경되면 의존 객체들에게 자동으로 알린다.

구현

// 1. Subject
public interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

// 2. Observer
public interface Observer {
    void update(String message);
}

// 3. Concrete Subject
public class NewsPublisher implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    private String latestNews;

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(latestNews);
        }
    }

    public void publishNews(String news) {
        this.latestNews = news;
        notifyObservers();
    }
}

// 4. Concrete Observers
public class EmailSubscriber implements Observer {
    private final String email;

    public EmailSubscriber(String email) {
        this.email = email;
    }

    @Override
    public void update(String message) {
        System.out.println("이메일 발송 to " + email + ": " + message);
    }
}

public class PushSubscriber implements Observer {
    private final String deviceId;

    public PushSubscriber(String deviceId) {
        this.deviceId = deviceId;
    }

    @Override
    public void update(String message) {
        System.out.println("푸시 알림 to " + deviceId + ": " + message);
    }
}

// 5. 사용
NewsPublisher publisher = new NewsPublisher();

publisher.attach(new EmailSubscriber("user@example.com"));
publisher.attach(new PushSubscriber("device-123"));

publisher.publishNews("속보: 새로운 Java 버전 출시!");

Java 내장 기능 활용

// PropertyChangeSupport 활용
public class Stock {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private String symbol;
    private double price;

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }

    public void setPrice(double newPrice) {
        double oldPrice = this.price;
        this.price = newPrice;
        support.firePropertyChange("price", oldPrice, newPrice);
    }
}

// 사용
Stock apple = new Stock("AAPL", 150.0);
apple.addPropertyChangeListener(event -> {
    System.out.println("주가 변동: " + event.getOldValue() + " -> " + event.getNewValue());
});

apple.setPrice(155.0);  // 주가 변동: 150.0 -> 155.0

실무 예시: Spring ApplicationEvent

// 1. 이벤트 정의
public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;

    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }

    public Order getOrder() { return order; }
}

// 2. 이벤트 발행
@Service
public class OrderService {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
        return order;
    }
}

// 3. 이벤트 리스너
@Component
public class OrderEventListener {

    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println("주문 생성됨: " + event.getOrder().getId());
    }

    @Async
    @EventListener
    public void sendNotification(OrderCreatedEvent event) {
        // 비동기 알림 발송
        notificationService.send(event.getOrder().getCustomerEmail());
    }
}

State (상태)

객체의 내부 상태에 따라 행동을 변경한다. 상태를 객체로 표현한다.

구현

// 1. State 인터페이스
public interface OrderState {
    void next(Order order);
    void prev(Order order);
    void printStatus();
}

// 2. Concrete States
public class OrderedState implements OrderState {
    @Override
    public void next(Order order) {
        order.setState(new ShippedState());
    }

    @Override
    public void prev(Order order) {
        System.out.println("주문 상태가 가장 처음입니다.");
    }

    @Override
    public void printStatus() {
        System.out.println("주문 완료 - 배송 준비 중");
    }
}

public class ShippedState implements OrderState {
    @Override
    public void next(Order order) {
        order.setState(new DeliveredState());
    }

    @Override
    public void prev(Order order) {
        order.setState(new OrderedState());
    }

    @Override
    public void printStatus() {
        System.out.println("배송 중");
    }
}

public class DeliveredState implements OrderState {
    @Override
    public void next(Order order) {
        System.out.println("이미 배송 완료되었습니다.");
    }

    @Override
    public void prev(Order order) {
        order.setState(new ShippedState());
    }

    @Override
    public void printStatus() {
        System.out.println("배송 완료");
    }
}

// 3. Context
public class Order {
    private OrderState state = new OrderedState();

    public void setState(OrderState state) {
        this.state = state;
    }

    public void nextState() {
        state.next(this);
    }

    public void prevState() {
        state.prev(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

// 4. 사용
Order order = new Order();
order.printStatus();  // 주문 완료 - 배송 준비 중

order.nextState();
order.printStatus();  // 배송 중

order.nextState();
order.printStatus();  // 배송 완료

order.nextState();    // 이미 배송 완료되었습니다.

State vs Strategy

State:
- 상태에 따라 행동이 자동으로 변경
- 상태 전이 로직이 상태 객체 내부에 있음
- 클라이언트는 상태 변경을 직접 하지 않음

Strategy:
- 클라이언트가 전략을 직접 선택/변경
- 전략 간 의존성 없음
- 알고리즘 선택에 초점

Chain of Responsibility (책임 연쇄)

요청을 처리할 수 있는 기회를 체인의 각 객체에게 순차적으로 제공한다.

구현

// 1. Handler
public abstract class SupportHandler {
    protected SupportHandler nextHandler;

    public void setNext(SupportHandler handler) {
        this.nextHandler = handler;
    }

    public abstract void handle(SupportTicket ticket);
}

// 2. Concrete Handlers
public class Level1Support extends SupportHandler {
    @Override
    public void handle(SupportTicket ticket) {
        if (ticket.getSeverity() == Severity.LOW) {
            System.out.println("Level 1 지원팀에서 처리: " + ticket.getDescription());
        } else if (nextHandler != null) {
            nextHandler.handle(ticket);
        }
    }
}

public class Level2Support extends SupportHandler {
    @Override
    public void handle(SupportTicket ticket) {
        if (ticket.getSeverity() == Severity.MEDIUM) {
            System.out.println("Level 2 지원팀에서 처리: " + ticket.getDescription());
        } else if (nextHandler != null) {
            nextHandler.handle(ticket);
        }
    }
}

public class Level3Support extends SupportHandler {
    @Override
    public void handle(SupportTicket ticket) {
        if (ticket.getSeverity() == Severity.HIGH) {
            System.out.println("Level 3 지원팀(엔지니어)에서 처리: " + ticket.getDescription());
        } else if (nextHandler != null) {
            nextHandler.handle(ticket);
        } else {
            System.out.println("처리할 수 없는 티켓: " + ticket.getDescription());
        }
    }
}

// 3. 체인 구성 및 사용
SupportHandler level1 = new Level1Support();
SupportHandler level2 = new Level2Support();
SupportHandler level3 = new Level3Support();

level1.setNext(level2);
level2.setNext(level3);

// 요청 처리
level1.handle(new SupportTicket("비밀번호 재설정", Severity.LOW));
// Level 1 지원팀에서 처리: 비밀번호 재설정

level1.handle(new SupportTicket("결제 오류", Severity.MEDIUM));
// Level 2 지원팀에서 처리: 결제 오류

level1.handle(new SupportTicket("시스템 장애", Severity.HIGH));
// Level 3 지원팀(엔지니어)에서 처리: 시스템 장애

실무 예시: Servlet Filter

public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        System.out.println("Request 로깅");
        chain.doFilter(request, response);  // 다음 핸들러로
        System.out.println("Response 로깅");
    }
}

public class AuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        if (isAuthenticated(request)) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse) response).sendError(401);
        }
    }
}

Mediator (중재자)

객체들 간의 상호작용을 캡슐화하여 객체들이 서로를 직접 참조하지 않게 한다.

구현

// 1. Mediator
public interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

// 2. Concrete Mediator
public class ChatRoom implements ChatMediator {
    private final List<User> users = new ArrayList<>();

    @Override
    public void addUser(User user) {
        users.add(user);
    }

    @Override
    public void sendMessage(String message, User sender) {
        for (User user : users) {
            // 발신자 제외하고 메시지 전달
            if (user != sender) {
                user.receive(message, sender.getName());
            }
        }
    }
}

// 3. Colleague
public abstract class User {
    protected ChatMediator mediator;
    protected String name;

    public User(ChatMediator mediator, String name) {
        this.mediator = mediator;
        this.name = name;
    }

    public String getName() { return name; }

    public abstract void send(String message);
    public abstract void receive(String message, String from);
}

// 4. Concrete Colleagues
public class ChatUser extends User {
    public ChatUser(ChatMediator mediator, String name) {
        super(mediator, name);
    }

    @Override
    public void send(String message) {
        System.out.println(name + " 전송: " + message);
        mediator.sendMessage(message, this);
    }

    @Override
    public void receive(String message, String from) {
        System.out.println(name + " 수신 [" + from + "]: " + message);
    }
}

// 5. 사용
ChatMediator chatRoom = new ChatRoom();

User alice = new ChatUser(chatRoom, "Alice");
User bob = new ChatUser(chatRoom, "Bob");
User charlie = new ChatUser(chatRoom, "Charlie");

chatRoom.addUser(alice);
chatRoom.addUser(bob);
chatRoom.addUser(charlie);

alice.send("안녕하세요!");
// Alice 전송: 안녕하세요!
// Bob 수신 [Alice]: 안녕하세요!
// Charlie 수신 [Alice]: 안녕하세요!

Memento (메멘토)

객체의 상태를 저장하고 복원할 수 있게 한다.

구현

// 1. Memento
public class EditorMemento {
    private final String content;
    private final int cursorPosition;
    private final LocalDateTime savedAt;

    public EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
        this.savedAt = LocalDateTime.now();
    }

    // Package-private: Originator만 접근 가능
    String getContent() { return content; }
    int getCursorPosition() { return cursorPosition; }

    // Public: 표시용
    public LocalDateTime getSavedAt() { return savedAt; }
}

// 2. Originator
public class TextEditor {
    private String content = "";
    private int cursorPosition = 0;

    public void write(String text) {
        content = content.substring(0, cursorPosition)
                  + text
                  + content.substring(cursorPosition);
        cursorPosition += text.length();
    }

    public EditorMemento save() {
        return new EditorMemento(content, cursorPosition);
    }

    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }

    public String getContent() { return content; }
}

// 3. Caretaker
public class EditorHistory {
    private final Deque<EditorMemento> history = new ArrayDeque<>();
    private static final int MAX_HISTORY = 50;

    public void save(EditorMemento memento) {
        if (history.size() >= MAX_HISTORY) {
            history.removeLast();
        }
        history.push(memento);
    }

    public EditorMemento undo() {
        if (history.isEmpty()) {
            return null;
        }
        return history.pop();
    }

    public List<EditorMemento> getHistory() {
        return new ArrayList<>(history);
    }
}

// 4. 사용
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory();

editor.write("Hello");
history.save(editor.save());

editor.write(" World");
history.save(editor.save());

editor.write("!");
System.out.println(editor.getContent());  // Hello World!

// Undo
EditorMemento previous = history.undo();
editor.restore(previous);
System.out.println(editor.getContent());  // Hello World

Iterator (반복자)

컬렉션의 내부 구조를 노출하지 않고 요소에 순차적으로 접근한다.

구현

// 1. Iterator 인터페이스
public interface Iterator<T> {
    boolean hasNext();
    T next();
}

// 2. Aggregate
public interface IterableCollection<T> {
    Iterator<T> createIterator();
}

// 3. Concrete Iterator
public class BookIterator implements Iterator<Book> {
    private final List<Book> books;
    private int position = 0;

    public BookIterator(List<Book> books) {
        this.books = books;
    }

    @Override
    public boolean hasNext() {
        return position < books.size();
    }

    @Override
    public Book next() {
        return books.get(position++);
    }
}

// 4. Concrete Aggregate
public class BookShelf implements IterableCollection<Book> {
    private final List<Book> books = new ArrayList<>();

    public void addBook(Book book) {
        books.add(book);
    }

    @Override
    public Iterator<Book> createIterator() {
        return new BookIterator(books);
    }

    // 특수 이터레이터
    public Iterator<Book> createReverseIterator() {
        return new Iterator<>() {
            private int position = books.size() - 1;

            @Override
            public boolean hasNext() { return position >= 0; }

            @Override
            public Book next() { return books.get(position--); }
        };
    }
}

// 5. 사용
BookShelf shelf = new BookShelf();
shelf.addBook(new Book("Design Patterns"));
shelf.addBook(new Book("Clean Code"));
shelf.addBook(new Book("Effective Java"));

Iterator<Book> it = shelf.createIterator();
while (it.hasNext()) {
    System.out.println(it.next().getTitle());
}

Visitor (방문자)

객체 구조를 변경하지 않고 새로운 연산을 추가한다.

구현

// 1. Element
public interface DocumentElement {
    void accept(DocumentVisitor visitor);
}

// 2. Concrete Elements
public class Paragraph implements DocumentElement {
    private final String text;

    public Paragraph(String text) { this.text = text; }
    public String getText() { return text; }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

public class Image implements DocumentElement {
    private final String url;
    private final int width;
    private final int height;

    public Image(String url, int width, int height) {
        this.url = url;
        this.width = width;
        this.height = height;
    }

    // Getters...

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

public class Table implements DocumentElement {
    private final List<List<String>> data;

    public Table(List<List<String>> data) { this.data = data; }
    public List<List<String>> getData() { return data; }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

// 3. Visitor
public interface DocumentVisitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
    void visit(Table table);
}

// 4. Concrete Visitors
public class HtmlExportVisitor implements DocumentVisitor {
    private final StringBuilder html = new StringBuilder();

    @Override
    public void visit(Paragraph paragraph) {
        html.append("<p>").append(paragraph.getText()).append("</p>\n");
    }

    @Override
    public void visit(Image image) {
        html.append(String.format("<img src=\"%s\" width=\"%d\" height=\"%d\" />\n",
            image.getUrl(), image.getWidth(), image.getHeight()));
    }

    @Override
    public void visit(Table table) {
        html.append("<table>\n");
        for (List<String> row : table.getData()) {
            html.append("  <tr>");
            for (String cell : row) {
                html.append("<td>").append(cell).append("</td>");
            }
            html.append("</tr>\n");
        }
        html.append("</table>\n");
    }

    public String getResult() { return html.toString(); }
}

public class MarkdownExportVisitor implements DocumentVisitor {
    private final StringBuilder markdown = new StringBuilder();

    @Override
    public void visit(Paragraph paragraph) {
        markdown.append(paragraph.getText()).append("\n\n");
    }

    @Override
    public void visit(Image image) {
        markdown.append(String.format("![image](%s)\n\n", image.getUrl()));
    }

    @Override
    public void visit(Table table) {
        // Markdown 테이블 형식으로 출력
    }

    public String getResult() { return markdown.toString(); }
}

// 5. 사용
List<DocumentElement> document = List.of(
    new Paragraph("제목입니다"),
    new Image("logo.png", 100, 50),
    new Table(List.of(
        List.of("이름", "나이"),
        List.of("홍길동", "30")
    ))
);

HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
for (DocumentElement element : document) {
    element.accept(htmlVisitor);
}
System.out.println(htmlVisitor.getResult());

Interpreter (인터프리터)

언어의 문법을 클래스로 표현하고 해석한다.

구현

// 간단한 수식 인터프리터
public interface Expression {
    int interpret();
}

public class NumberExpression implements Expression {
    private final int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret() {
        return number;
    }
}

public class AddExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}

public class MultiplyExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public MultiplyExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret() {
        return left.interpret() * right.interpret();
    }
}

// 파서
public class ExpressionParser {
    public Expression parse(String expression) {
        // "(3 + 5) * 2" -> MultiplyExpression(AddExpression(3, 5), 2)
        // 실제로는 토큰화, AST 구축 필요
        return null;
    }
}

// 사용: (3 + 5) * 2 = 16
Expression expr = new MultiplyExpression(
    new AddExpression(
        new NumberExpression(3),
        new NumberExpression(5)
    ),
    new NumberExpression(2)
);

System.out.println(expr.interpret());  // 16

패턴 선택 가이드

패턴별 사용 시점

문제 상황 추천 패턴
객체가 하나만 필요 Singleton
복잡한 객체 생성 Builder
객체 생성을 서브클래스에 위임 Factory Method
관련 객체군 생성 Abstract Factory
기존 객체 복제 Prototype
인터페이스 변환 Adapter
구현과 추상 분리 Bridge
트리 구조 Composite
동적 기능 추가 Decorator
복잡한 서브시스템 단순화 Facade
메모리 절약 (공유) Flyweight
접근 제어, 지연 로딩 Proxy
알고리즘 교체 Strategy
알고리즘 골격 정의 Template Method
요청을 객체로 Command
상태 변경 알림 Observer
상태에 따른 행동 변경 State
요청 처리 체인 Chain of Responsibility
객체 간 통신 중앙화 Mediator
상태 저장/복원 Memento
순회 추상화 Iterator
연산 추가 (구조 변경 없이) Visitor
DSL, 문법 해석 Interpreter

패턴 조합

자주 함께 사용되는 패턴:

Factory + Singleton: 팩토리를 싱글턴으로
Abstract Factory + Factory Method: 팩토리 메서드로 제품 생성
Builder + Fluent Interface: 메서드 체이닝
Composite + Iterator: 트리 순회
Decorator + Strategy: 동적 알고리즘 적용
Observer + Mediator: 이벤트 중앙 관리
Command + Memento: Undo/Redo 구현
State + Strategy: 상태별 전략

정리

디자인 패턴은 도구다. 모든 상황에 패턴을 적용하려 하지 말고, 문제가 있을 때 적절한 패턴을 선택해야 한다.

패턴 사용 원칙:

1. 문제 먼저, 패턴은 그 다음
2. 단순한 해결책이 있으면 그것을 사용
3. 패턴은 의사소통 도구
4. 패턴을 위한 패턴은 금물
5. SOLID 원칙과 함께 적용

“Design patterns should be used with caution. Don’t use a pattern just because it exists.” — Gang of Four