Java 객체지향 설계 시 사용되는 디자인 패턴 종류 및 예시 코드

디자인 패턴 학습 시 UML 다이어그램은 클래스 관계, 상속, 의존성 등 정적인 구조를 표현하지만,
객체의 동적 흐름 표현이 부족해서 객체지향 패턴의 본질을 보여주기 어렵다고 합니다.

이 글에서는 Java 예제 코드를 이용하여 각 패턴의 의도와 동작 원리를 학습할 수 있습니다.


생성 패턴

싱글톤 패턴 (Singleton)

프로그램에서 특정 클래스의 인스턴스가 단 하나만 존재해야 하는 경우 사용되는 패턴입니다.

싱글톤 패턴으로 생성된 클래스는 getInstance 함수를 통해 인스턴스를 하나만 생성하고,
프로그램 전체에서 동일한 인스턴스를 공유하여 일관된 전역 상태를 유지합니다.

싱글톤 패턴 예시 1

// 싱글톤 패턴으로 설계된 테마 클래스
public class Theme {
  // 유일한 테마 인스턴스가 저장되는 변수
  // 모든 스레드가 변수 최신 값을 CPU 캐시가 아닌, 메인 메모리에서 읽도록 volatile 키워드 추가
  private static volatile Theme instance;

  private String themeColor;

  // 생성자 함수를 private으로 지정하여, 외부에서 신규 인스턴스 생성 불가
  private Theme() {
    // 기본 테마 색상 초기값 설정
    this.themeColor = "light";
  }

  // 테마 인스턴스 조회 함수 (정적 메소드)
  public static Theme getInstance() {
    // 아직 인스턴스가 만들어지지 않았는지 1차 체크
    if (instance == null) {
      // 멀티스레드 환경에서 여러 스레드가 동시에 만들지 못하도록 잠금 (lock)
      // Theme.class는 클래스 로딩 시 JVM이 자동으로 생성하는 클래스 객체 (클래스 정보 저장용)
      // 이후 synchronized (Theme.class) 으로 잠그려는 다른 스레드는 대기 상태가 됨
      synchronized (Theme.class) {
        // 다른 스레드가 인스턴스를 이미 생성하였는지 2차 체크
        if (instance == null) {
          // 테마 인스턴스 생성 및 할당 (초기 1회)
          instance = new Theme();
        }
      }
      // synchronized 블록이 끝나면 잠금 해제 됨
    }

    // 유일한 테마 인스턴스 반환
    return instance;
  }

  // 테마 색상 조회 함수
  public String getThemeColor() {
    return themeColor;
  }

  // 테마 색상 변경 함수
  public void setThemeColor(String themeColor) {
    this.themeColor = themeColor;
  }
}

// 테마 인스턴스를 사용하는 UI 요소 클래스 1 : 버튼
public class Button {
  private String label;

  public Button(String label) {
    this.label = label;
  }

  public void display() {
    // 프로그램에서 초기 1회 생성된 테마 인스턴스를 통해 색상 조회
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("Button [" + label + "] displayed in " + themeColor + " theme.");
  }
}

// 테마 인스턴스를 사용하는 UI 요소 클래스 2 : 입력창
public class TextField {
  private String text;

  public TextField(String text) {
    this.text = text;
  }

  public void display() {
    // 프로그램에서 초기 1회 생성된 테마 인스턴스를 통해 색상 조회
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("TextField [" + text + "] displayed in " + themeColor + " theme.");
  }
}

// 테마 인스턴스를 사용하는 UI 요소 클래스 3 : 라벨
public class Label {
  private String text;

  public Label(String text) {
    this.text = text;
  }

  public void display() {
    // 프로그램에서 초기 1회 생성된 테마 인스턴스를 통해 색상 조회
    String themeColor = Theme.getInstance().getThemeColor();
    System.out.println("Label [" + text + "] displayed in " + themeColor + " theme.");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // UI 요소 클래스들 객체 인스턴스 생성
    Button button = new Button("Submit");
    TextField textField = new TextField("Enter your name");
    Label label = new Label("Username");

    // 각 클래스 함수로 테마 클래스의 싱글톤 객체 색상 조회
    button.display();
    textField.display();
    label.display();

    // 테마 색상 변경
    Theme.getInstance().setThemeColor("dark");

    button.display();
    textField.display();
    label.display();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Button [Submit] displayed in light theme.
TextField [Enter your name] displayed in light theme.
Label [Username] displayed in light theme.

Button [Submit] displayed in dark theme.
TextField [Enter your name] displayed in dark theme.
Label [Username] displayed in dark theme.

팩토리 메서드 패턴 (Factory Method)

단일 제품 객체 생성을 팩토리에 위임하여, 확장 가능한 프로그램을 설계할 수 있는 패턴입니다.

클라이언트에서 구체 제품 클래스가 아닌 추상 Creator 타입에 의존하면,
제품 클래스가 추가되어도 클라이언트 공통 로직 수정 없이 확장할 수 있습니다.

Creator 추상 클래스에 제품 객체를 생성하는 팩토리 메서드를 정의하고,
구체 팩토리 클래스에서 팩토리 메서드를 오버라이딩하여 제품 객체 생성 책임을 가집니다.

팩토리 메서드 패턴 예시 1

// 제품 인터페이스
// 팩토리 메서드가 생성할 객체들의 공통 타입
interface Vehicle {
  void drive();
}

// 제품 인터페이스를 구현한 제품 클래스 1
class Car implements Vehicle {
  @Override
  public void drive() {
    System.out.println("Driving a car");
  }
}

// 제품 인터페이스를 구현한 제품 클래스 2
class Motorcycle implements Vehicle {
  @Override
  public void drive() {
    System.out.println("Riding a motorcycle");
  }
}

// Creator 추상 클래스
abstract class VehicleFactory {
  // 팩토리 메서드 (호출 시 제품 인터페이스 타입 반환)
  // 각 팩토리 클래스에서 오버라이딩하여 제품 객체 반환
  abstract Vehicle createVehicle();
  
  // 팩토리 메서드를 호출하여 인터페이스에 의존하는 공통 로직
  public void deliverVehicle() {
    // 팩토리 메소드 호출 후 생성된 제품 객체 저장
    Vehicle vehicle = createVehicle();
    System.out.println("Delivering the vehicle:");

    // 제품 인터페이스에 정의된 메서드 호출
    vehicle.drive();
  }
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스 1
class CarFactory extends VehicleFactory {
  // 팩토리 메서드 오버라이딩
  @Override
  Vehicle createVehicle() {
    // 제품 클래스 1 객체 생성 및 반환
    return new Car();
  }
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스 2
class MotorcycleFactory extends VehicleFactory {
  // 팩토리 메서드 오버라이딩
  @Override
  Vehicle createVehicle() {
    // 제품 클래스 2 객체 생성 및 반환
    return new Motorcycle();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 구체 팩토리 클래스 1 객체 생성
    VehicleFactory carFactory = new CarFactory();
    // 팩토리 메서드를 사용하는 공통 로직 호출
    carFactory.deliverVehicle();
    
    // 구체 팩토리 클래스 2 객체 생성
    VehicleFactory motorcycleFactory = new MotorcycleFactory();
    // 팩토리 메서드를 사용하는 공통 로직 호출
    motorcycleFactory.deliverVehicle();
  }
}

클라이언트는 추상 클래스 타입에만 의존하며, 제품 객체를 직접 생성하지 않습니다.
선택된 구체 팩토리 클래스의 팩토리 메서드에서 생성할 제품 객체가 결정됩니다.
위 코드의 실행 결과는 아래와 같습니다.

Delivering the vehicle:
Driving a car
Delivering the vehicle:
Riding a motorcycle

팩토리 메서드 패턴 예시 2

// 제품 인터페이스
public interface Product {
  void create();
}

// 제품 인터페이스를 구현한 제품 클래스 1
public class Electronics implements Product {
  @Override
  public void create() {
    System.out.println("Electronics product created.");
  }
}

// 제품 인터페이스를 구현한 제품 클래스 2
public class Clothing implements Product {
  @Override
  public void create() {
    System.out.println("Clothing product created.");
  }
}

// 제품 인터페이스를 구현한 제품 클래스 3
public class Book implements Product {
  @Override
  public void create() {
    System.out.println("Book product created.");
  }
}

// Creator 추상 클래스
public abstract class ProductFactory {
  // 팩토리 메서드
  public abstract Product createProduct(String type);

  // 팩토리 메서드를 호출하여 인터페이스에 의존하는 공통 로직
  public Product orderProduct(String type) {
    // 팩토리 메소드 호출 후 생성된 제품 객체 저장
    Product product = createProduct(type);

    // 제품 인터페이스에 정의된 메서드 호출
    product.create();

    // 제품 객체 반환
    return product;
  }
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스
public class ConcreteProductFactory extends ProductFactory {
  @Override
  public Product createProduct(String type) {
    if (type.equalsIgnoreCase("electronics")) {
      // 제품 클래스 1 객체 생성 및 반환
      return new Electronics();
    } else if (type.equalsIgnoreCase("clothing")) {
      // 제품 클래스 2 객체 생성 및 반환
      return new Clothing();
    } else if (type.equalsIgnoreCase("book")) {
      // 제품 클래스 3 객체 생성 및 반환
      return new Book();
    } else {
      throw new IllegalArgumentException("Unknown product type.");
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 구체 팩토리 클래스 객체 생성
    ProductFactory factory = new ConcreteProductFactory();

    // 팩토리 메서드를 사용하는 공통 로직 호출하여 제품 클래스 1 객체 생성
    Product electronics = factory.orderProduct("electronics");

    // 팩토리 메서드를 사용하는 공통 로직 호출하여 제품 클래스 2 객체 생성
    Product clothing = factory.orderProduct("clothing");

    // 팩토리 메서드를 사용하는 공통 로직 호출하여 제품 클래스 3 객체 생성
    Product book = factory.orderProduct("book");
  }
}

하나의 팩토리 클래스에서 type 값에 따라 생성할 제품 객체를 분기하므로,
팩토리 메서드 패턴보다는 단순 팩토리 패턴에 가까운 예시입니다.
팩토리 메서드 패턴으로 구현하려면, 제품 종류별로 구체 팩토리 클래스를 만드는 것이 좋습니다.
위 코드의 실행 결과는 아래와 같습니다.

Electronics product created.
Clothing product created.
Book product created.

팩토리 메서드 패턴 예시 3

// 제품 인터페이스
interface Payment {
  void processPayment(double amount);
}

// 제품 인터페이스를 구현한 제품 클래스 1 (신용카드 결제)
class CreditCardPayment implements Payment {
  private String creditCardNumber;

  // 생성자
  public CreditCardPayment(String creditCardNumber) {
    this.creditCardNumber = creditCardNumber;
  }
  
  @Override
  public void processPayment(double amount) {
    System.out.println("Credit card: $" + amount);
  }
}

// 제품 인터페이스를 구현한 제품 클래스 2 (페이팔 결제)
class PayPalPayment implements Payment {
  private String payPalEmail;

  // 생성자
  public PayPalPayment(String payPalEmail) {
    this.payPalEmail = payPalEmail;
  }
  
  @Override
  public void processPayment(double amount) {
    System.out.println("PayPal: $" + amount);
  }
}

// 제품 인터페이스를 구현한 제품 클래스 3 (은행 계좌이체 결제)
class BankTransferPayment implements Payment {
  private String bankAccountNumber;

  // 생성자
  public BankTransferPayment(String bankAccountNumber) {
    this.bankAccountNumber = bankAccountNumber;
  }

  @Override
  public void processPayment(double amount) {
    System.out.println("Bank transfer: $" + amount);
  }
}

// 사용자의 금융 관련 정보를 저장하는 클래스
// 팩토리 메서드 매개변수로 사용 예정
class FinancialInfo {
  String creditCardNumber;
  String payPalEmail;
  String bankAccountNumber;

  public FinancialInfo(
    String creditCardNumber, 
    String payPalEmail, 
    String bankAccountNumber
  ) {
    this.creditCardNumber = creditCardNumber;
    this.payPalEmail = payPalEmail;
    this.bankAccountNumber = bankAccountNumber;
  }

  // Getter, Setter 함수 생략
}

// Creator 추상 클래스
abstract class PaymentFactory {
  // 팩토리 메서드
  abstract Payment createPayment(FinancialInfo info);
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스 1 (신용카드 결제 팩토리)
class CreditCardPaymentFactory extends PaymentFactory {
  // 팩토리 메서드 오버라이딩
  @Override
  Payment createPayment(FinancialInfo info) {
    // 제품 클래스 1 객체 생성 및 반환
    return new CreditCardPayment(info.creditCardNumber);
  }
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스 2 (페이팔 결제 팩토리)
class PayPalPaymentFactory extends PaymentFactory {
  // 팩토리 메서드 오버라이딩
  @Override
  Payment createPayment(FinancialInfo info) {
    // 제품 클래스 2 객체 생성 및 반환
    return new PayPalPayment(info.payPalEmail);
  }
}

// Creator 추상 클래스를 상속한 구체 팩토리 클래스 3 (은행 계좌이체 결제 팩토리)
class BankTransferPaymentFactory extends PaymentFactory {
  // 팩토리 메서드 오버라이딩
  @Override
  Payment createPayment(FinancialInfo info) {
    // 제품 클래스 3 객체 생성 및 반환
    return new BankTransferPayment(info.bankAccountNumber);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 사용자의 금융 관련 정보를 저장하는 클래스 객체 생성
    FinancialInfo userInfo = new FinancialInfo(
      "1234-5678-9012-3456", "user@example.com", "987654321"
    );

    // 구체 팩토리 클래스 1 객체 생성
    PaymentFactory factory = new CreditCardPaymentFactory();
    // 팩토리 메서드 호출하여 제품 클래스 1 객체 생성
    Payment payment = factory.createPayment(userInfo);
    // 제품 클래스 1 함수 호출
    payment.processPayment(100.0);

    // 구체 팩토리 클래스 2 객체 생성
    factory = new PayPalPaymentFactory();
    // 팩토리 메서드 호출하여 제품 클래스 2 객체 생성
    payment = factory.createPayment(userInfo);
    // 제품 클래스 2 함수 호출
    payment.processPayment(200.0);

    // 구체 팩토리 클래스 3 객체 생성
    factory = new BankTransferPaymentFactory();
    // 팩토리 메서드 호출하여 제품 클래스 3 객체 생성
    payment = factory.createPayment(userInfo);
    // 제품 클래스 3 함수 호출
    payment.processPayment(300.0);
  }
}

제품 클래스의 생성자가 변경되어도 클라이언트 코드에 영향을 주지 않는 예시입니다.
팩토리 메서드가 생성자 호출을 감싸고 있으므로, 해당 구체 팩토리 클래스만 수정하면 됩니다.
위 코드의 실행 결과는 아래와 같습니다.

Credit card: $100.0
PayPal: $200.0
Bank transfer: $300.0

추상 팩토리 패턴 (Abstract Factory)

하나의 제품군에 속한 제품 객체들을 하나의 팩토리에서 일괄 생성하여,
일관된 방식으로 제품 그룹을 쉽게 교체할 수 있는 패턴입니다.

구체 클래스에 의존하지 않으므로 유연성과 확장성을 높일 수 있습니다.

추상 팩토리 패턴 예시 1

// 추상 제품 인터페이스 1 : 버튼
interface Button {
  void paint();
}

// 추상 제품 인터페이스 2 : 체크박스
interface Checkbox {
  void paint();
}

// 구체 제품 클래스 1-1 : 윈도우 스타일 버튼
class WindowsButton implements Button {
  @Override
  public void paint() {
    System.out.println("Rendering a button in Windows style");
  }
}

// 구체 제품 클래스 1-2 : 윈도우 스타일 체크박스
class WindowsCheckbox implements Checkbox {
  @Override
  public void paint() {
    System.out.println("Rendering a checkbox in Windows style");
  }
}

// 구체 제품 클래스 2-1 : 맥 스타일 버튼
class MacOSButton implements Button {
  @Override
  public void paint() {
    System.out.println("Rendering a button in MacOS style");
  }
}

// 구체 제품 클래스 2-2 : 맥 스타일 체크박스
class MacOSCheckbox implements Checkbox {
  @Override
  public void paint() {
    System.out.println("Rendering a checkbox in MacOS style");
  }
}

// 추상 팩토리 인터페이스
interface GUIFactory {
  Button createButton();
  Checkbox createCheckbox();
}

// 구체 팩토리 클래스 1 : 윈도우 스타일 제품들을 생성하는 팩토리
class WindowsFactory implements GUIFactory {
  @Override
  public Button createButton() {
    // 구체 제품 클래스 1-1 생성 및 반환
    return new WindowsButton();
  }

  @Override
  public Checkbox createCheckbox() {
    // 구체 제품 클래스 1-2 생성 및 반환
    return new WindowsCheckbox();
  }
}

// 구체 팩토리 클래스 2 : 맥 스타일 제품들을 생성하는 팩토리
class MacOSFactory implements GUIFactory {
  @Override
  public Button createButton() {
    // 구체 제품 클래스 2-1 생성 및 반환
    return new MacOSButton();
  }

  @Override
  public Checkbox createCheckbox() {
    // 구체 제품 클래스 2-2 생성 및 반환
    return new MacOSCheckbox();
  }
}

// 애플리케이션 코드
class Application {
  private Button button;
  private Checkbox checkbox;

  // 생성자 : 전달받은 팩토리의 메서드를 사용해서 관련 제품들 생성
  public Application(GUIFactory factory) {
    button = factory.createButton();
    checkbox = factory.createCheckbox();
  }

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

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 윈도우 스타일 팩토리 객체 생성
    GUIFactory windowsFactory = new WindowsFactory();
    // 애플리케이션 객체 생성 시,
    // 윈도우 스타일 팩토리를 사용해서 윈도우 스타일 제품들 생성
    Application windowsApp = new Application(windowsFactory);
    windowsApp.paint();

    System.out.println();

    // 맥 스타일 팩토리 객체 생성
    GUIFactory macFactory = new MacOSFactory();
    // 애플리케이션 객체 생성 시,
    // 맥 스타일 팩토리를 사용해서 맥 스타일 제품들 생성
    Application macApp = new Application(macFactory);
    macApp.paint();
  }
}

추상 팩토리 인터페이스를 구현한 구체 팩토리 클래스가 구체 제품 클래스들을 생성합니다.
위 코드의 실행 결과는 아래와 같습니다.

Rendering a button in Windows style
Rendering a checkbox in Windows style

Rendering a button in MacOS style
Rendering a checkbox in MacOS style

추상 팩토리 패턴 예시 2

// 추상 제품 인터페이스 1 : 데이터베이스 연결
interface Connection {
  void open();
  void close();
}

// 추상 제품 인터페이스 2 : SQL 명령 실행
interface Command {
  void execute(String query);
}

// 추상 제품 인터페이스 3 : 조회 결과
interface ResultSet {
  void getResults();
}

// 구체 제품 클래스 1-1 : MySQL 연결
class MySQLConnection implements Connection {
  public void open() {
    System.out.println("Opening MySQL connection");
  }

  public void close() {
    System.out.println("Closing MySQL connection");
  }
}

// 구체 제품 클래스 1-2 : MySQL 명령
class MySQLCommand implements Command {
  public void execute(String query) {
    System.out.println("Executing MySQL query: " + query);
  }
}

// 구체 제품 클래스 1-3 : MySQL 조회 결과
class MySQLResultSet implements ResultSet {
  public void getResults() {
    System.out.println("Getting results from MySQL database");
  }
}

// 구체 제품 클래스 2-1 : PostgreSQL 연결
class PostgreSQLConnection implements Connection {
  public void open() {
    System.out.println("Opening PostgreSQL connection");
  }

  public void close() {
    System.out.println("Closing PostgreSQL connection");
  }
}

// 구체 제품 클래스 2-2 : PostgreSQL 명령
class PostgreSQLCommand implements Command {
  public void execute(String query) {
    System.out.println("Executing PostgreSQL query: " + query);
  }
}

// 구체 제품 클래스 2-3 : PostgreSQL 조회 결과
class PostgreSQLResultSet implements ResultSet {
  public void getResults() {
    System.out.println("Getting results from PostgreSQL database");
  }
}

// 추상 팩토리 인터페이스
interface DatabaseFactory {
  Connection createConnection();
  Command createCommand();
  ResultSet createResultSet();
}

// 구체 팩토리 클래스 1 : MySQL 관련 제품들을 생성하는 팩토리
class MySQLFactory implements DatabaseFactory {
  public Connection createConnection() {
    // 구체 제품 클래스 1-1 생성 및 반환
    return new MySQLConnection();
  }

  public Command createCommand() {
    // 구체 제품 클래스 1-2 생성 및 반환
    return new MySQLCommand();
  }

  public ResultSet createResultSet() {
    // 구체 제품 클래스 1-3 생성 및 반환
    return new MySQLResultSet();
  }
}

// 구체 팩토리 클래스 2 : PostgreSQL 관련 제품들을 생성하는 팩토리
class PostgreSQLFactory implements DatabaseFactory {
  public Connection createConnection() {
    // 구체 제품 클래스 2-1 생성 및 반환
    return new PostgreSQLConnection();
  }

  public Command createCommand() {
    // 구체 제품 클래스 2-2 생성 및 반환
    return new PostgreSQLCommand();
  }

  public ResultSet createResultSet() {
    // 구체 제품 클래스 2-3 생성 및 반환
    return new PostgreSQLResultSet();
  }
}

// 데이터베이스를 사용하는 클라이언트 코드
class DatabaseClient {
  private Connection connection;
  private Command command;
  private ResultSet resultSet;

  // 생성자 : 전달받은 팩토리의 메서드를 사용해서 관련 DB 제품들 생성
  public DatabaseClient(DatabaseFactory factory) {
    connection = factory.createConnection();
    command = factory.createCommand();
    resultSet = factory.createResultSet();
  }

  // DB 작업 수행 함수
  public void performDatabaseOperations() {
    connection.open();
    command.execute("SELECT * FROM users");
    resultSet.getResults();
    connection.close();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // MySQL 팩토리를 사용해서 MySQL 관련 제품들 생성
    DatabaseClient mysqlClient = new DatabaseClient(new MySQLFactory());
    // MySQL DB 작업 수행
    mysqlClient.performDatabaseOperations();

    System.out.println("\nSwitching to PostgreSQL...\n");

    // PostgreSQL 팩토리를 사용해서 PostgreSQL 관련 제품들 생성
    DatabaseClient postgresClient = new DatabaseClient(new PostgreSQLFactory());
    // PostgreSQL DB 작업 수행
    postgresClient.performDatabaseOperations();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Opening MySQL connection
Executing MySQL query: SELECT * FROM users
Getting results from MySQL database
Closing MySQL connection

Switching to PostgreSQL...

Opening PostgreSQL connection
Executing PostgreSQL query: SELECT * FROM users
Getting results from PostgreSQL database
Closing PostgreSQL connection

빌더 패턴 (Builder)

빌더 패턴 예시 1

// Product class
class Pizza {
  private String dough;
  private String sauce;
  private String topping;

  // Private constructor to enforce the use of Builder
  private Pizza(PizzaBuilder builder) {
    this.dough = builder.dough;
    this.sauce = builder.sauce;
    this.topping = builder.topping;
  }

  @Override
  public String toString() {
    return "Pizza with " + dough + " dough, "
      + sauce + " sauce, and " + topping + " topping.";
  }

  public static class PizzaBuilder {
    private String dough;
    private String sauce;
    private String topping;

    public PizzaBuilder dough(String dough) {
      this.dough = dough;
      return this;
    }

    public PizzaBuilder sauce(String sauce) {
      this.sauce = sauce;
      return this;
    }
    
    public PizzaBuilder topping(String topping) {
      this.topping = topping;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Pizza myPizza = new Pizza.PizzaBuilder()
          .dough("Thin Crust")
          .sauce("Tomato")
          .topping("Cheese")
          .build();

    System.out.println(myPizza);

    String orderType = "Veggie";

    Pizza.PizzaBuilder pizzaBuilder = new Pizza.PizzaBuilder().dough("Regular");

    pizzaBuilder.sauce("Pesto");

    if (orderType.equals("Veggie")) {
      pizzaBuilder.topping("Vegetables");
    } else {
      pizzaBuilder.topping("Pepperoni");
    }

    Pizza customPizza = pizzaBuilder.build();
    System.out.println(customPizza);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Pizza with Thin Crust dough, Tomato sauce, and Cheese topping.
Pizza with Regular dough, Pesto sauce, and Vegetables topping.

빌더 패턴 예시 2

// Product class
public class HttpRequest {
  private String method;
  private String url;
  private Map<String, String> headers;
  private Map<String, String> parameters;
  private String body;
  
  // private constructor
  private HttpRequest(Builder builder) {
    this.method = builder.method;
    this.url = builder.url;
    this.headers = builder.headers;
    this.parameters = builder.parameters;
    this.body = builder.body;
  }
  
  @Override
  public String toString() {
    return "HttpRequest [method=" + method + ", url=" + url + 
          ", headers=" + headers + ", parameters=" + parameters + 
          ", body=" + body + "]";
  }

  public static class Builder {
    private String method;
    private String url;
    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> parameters = new HashMap<>();
    private String body;

    public Builder(String method, String url) {
      this.method = method;
      this.url = url;
    }

    public Builder addHeader(String key, String value) {
      this.headers.put(key, value);
      return this; }

    public Builder addParameter(String key, String value) {
      this.parameters.put(key, value);
      return this; }

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

    public HttpRequest build() {
      return new HttpRequest(this); }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    HttpRequest getRequest = new HttpRequest.Builder(
      "GET", "https://example.com/api")
      .addHeader("Authorization", "Bearer token")
      .addParameter("query", "builder-pattern")
      .build();

    HttpRequest postRequest = new HttpRequest.Builder(
      "POST", "https://example.com/api")
      .addHeader("Authorization", "Bearer token")
      .setBody("{ \"name\": \"John\", \"age\": 30 }")
      .build();
      
    System.out.println(getRequest);
    System.out.println(postRequest);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

HttpRequest [method=GET, url=https://example.com/api,
 headers={Authorization=Bearer token}, parameters={query=builder-pattern},
 body=null]
 
HttpRequest [method=POST, url=https://example.com/api,
 headers={Authorization=Bearer token}, parameters={},
 body={ "name": "John", "age": 30 }]

프로토타입 패턴 (Prototype)

프로토타입 패턴 예시 1

interface Prototype {
    Prototype clone();
}

class Person implements Prototype {
  private String name;
  private int age;
  private String address;

  public Person(
    String name, int age, String address
  ) {
    this.name = name;
    this.age = age;
    this.address = address;
  }

  public Person(Person other) {
    this.name = other.name;
    this.age = other.age;
    this.address = other.address;
  }

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

  public void setAddress(String newAddress) {
    this.address = newAddress;
  }

  public void displayInfo() {
    System.out.println("Name: " + name + ", Age: " + age+ ", Address: " + address);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Person original = new Person("John", 30, "123 Main St");
    original.displayInfo();

    Person cloned = original.clone();
    cloned.setAddress("456 Clone St");

    System.out.println("\nAfter cloning and modifying the clone:");
    original.displayInfo();
    cloned.displayInfo();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Name: John, Age: 30, Address: 123 Main St

After cloning and modifying the clone:
Name: John, Age: 30, Address: 123 Main St
Name: John, Age: 30, Address: 456 Clone St

프로토타입 패턴 예시 2

// Simple Prototype interface
interface Prototype {
  Prototype clone();
}

// Document interface extending Prototype
interface Document extends Prototype {
  void setContent(String content);
  String getContent();
}

// Concrete document class
class TextDocument implements Document {
  private String content;

  public TextDocument(String content) {
    this.content = content;
  }

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

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

  @Override
  public String getContent() {
    return content;
  }
}

// Template manager
class DocumentTemplateManager {
  private static final Map<String, Document> templates
    = new HashMap<>();

  public static void addTemplate(String name, Document doc) {
    templates.put(name, doc);
  }

  public static Document createDocument(String templateName) {
    Document template = templates.get(templateName);
    if (template == null) {
      throw new IllegalArgumentException(
        "Template not found: " + templateName);
    }
    return (Document) template.clone();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    DocumentTemplateManager.addTemplate(
      "welcome",
      new TextDocument("Welcome, {name}!"));
    DocumentTemplateManager.addTemplate(
      "meeting",
      new TextDocument(
          "Meeting scheduled on {date} at {time}"));

    Document welcomeDoc = DocumentTemplateManager
      .createDocument("welcome");
    welcomeDoc.setContent(
      welcomeDoc
      .getContent()
      .replace("{name}", "John Doe"));
    
    System.out.println("Welcome document: " + welcomeDoc.getContent());

    Document meetingDoc = DocumentTemplateManager
      .createDocument("meeting");
    meetingDoc.setContent(meetingDoc.getContent()
          .replace("{date}", "2024-10-01")
          .replace("{time}", "14:00"));
            
    System.out.println("Meeting document: " + meetingDoc.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Welcome document: Welcome, John Doe!
Meeting document: Meeting scheduled on 2024-10-01 at 14:00

구조 패턴

어댑터 패턴 (Adapter)

함수 및 파라미터 규격이 달라 호환되지 않는 인터페이스를 가진 기존 클래스들을
코드 수정 없이 사용할 수 있도록, 어댑터 객체를 통해 인터페이스를 변환해주는 패턴입니다.

어댑터 패턴 예시 1

// 수정하기 어려운, 기존 인터페이스 (Adaptee 규격)
interface OldMessageSender {
  int send(String[] messageData);
}

// 수정하기 어려운, 기존 클래스 (Adaptee)
class OldMessageSystem implements OldMessageSender {
  @Override
  public int send(String[] messageData) {
    System.out.println("OldMessageSystem: Sending message: " + messageData[0] + " to " + messageData[1]);
    return 1; // 성공 코드
  }
}

// 어댑터 타겟 인터페이스
interface ModernMessageSender {
  void sendMessage(String message, String recipient);
}

// 어댑터 클래스
class MessageAdapter implements ModernMessageSender {
  private OldMessageSender oldSystem;

  // 생성자에서 기존 클래스 전달받아 저장
  public MessageAdapter(OldMessageSender oldSystem) {
    this.oldSystem = oldSystem;
  }

  @Override
  public void sendMessage(String message, String recipient) {
    String[] messageData = {message, recipient};

    // 어댑터 클래스에서 기존 클래스 함수 호출
    int result = oldSystem.send(messageData);

    if (result != 1) {
      System.out.println("Failed to send message");
    }
  }
}

// 클라이언트 (호출부)
class Main {
  public static void main(String[] args) {
    OldMessageSender oldSystem = new OldMessageSystem();
    ModernMessageSender adapter = new MessageAdapter(oldSystem);

    adapter.sendMessage("Hello, World!", "john@example.com");
  }
}

어댑터 패턴 예시 2

// 수정하기 어려운, 기존 USB 클래스 (Adaptee)
class USB {
  void connectWithUsbCable(String data) {
    System.out.println("Displaying via USB with data: " + data);
  }
}

// 수정하기 어려운, 기존 HDMI 클래스 (Adaptee)
class HDMI {
  void connectWithHdmiCable(int resolution) {
    System.out.println(
    "Displaying via HDMI with resolution: " + resolution + "p"
    );
  }
}

// 수정하기 어려운, 기존 VGA 클래스 (Adaptee)
class VGA {
  void connectWithVgaCable(boolean highQuality) {
    System.out.println(
    "Displaying via VGA with high quality: " + highQuality
    );
  }
}

// 어댑터 타겟 인터페이스
interface DisplayAdapter {
  void display();
}

// USB 어댑터 클래스
class USBAdapter implements DisplayAdapter {
  private USB usb;
  private String data;
  public USBAdapter(USB usb, String data) {
    this.usb = usb;
    this.data = data;
  }

  @Override
  public void display() {
    // 타겟 인터페이스 함수 내에서 기존 USB 클래스 함수 실행
    usb.connectWithUsbCable(data);
  }
}

// HDMI 어댑터 클래스
class HDMIAdapter implements DisplayAdapter {
  private HDMI hdmi;
  private int resolution;
  public HDMIAdapter(HDMI hdmi, int resolution) {
    this.hdmi = hdmi;
    this.resolution = resolution;
  }

  @Override
  public void display() {
    // 타겟 인터페이스 함수 내에서 기존 HDMI 클래스 함수 실행
    hdmi.connectWithHdmiCable(resolution);
  }
}

// VGA 어댑터 클래스
class VGAAdapter implements DisplayAdapter {
  private VGA vga;
  private boolean highQuality;
  public VGAAdapter(VGA vga, boolean highQuality) {
    this.vga = vga;
    this.highQuality = highQuality;
  }

  @Override
  public void display() {
    // 타겟 인터페이스 함수 내에서 기존 VGA 클래스 함수 실행
    vga.connectWithVgaCable(highQuality);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 기존 클래스 객체 생성
    USB usb = new USB();
    HDMI hdmi = new HDMI();
    VGA vga = new VGA();

    // 타겟 인터페이스를 적용한 클래스들을 리스트에 추가
    List<DisplayAdapter> adapters = new ArrayList<>();
    adapters.add(new USBAdapter(usb, "Video Data"));
    adapters.add(new HDMIAdapter(hdmi, 1080));
    adapters.add(new VGAAdapter(vga, true));

    // 리스트를 순회하며 어댑터 인터페이스 함수 호출
    for (DisplayAdapter adapter : adapters) {
      adapter.display();
    }
  }
}

같은 역할이지만 서로 다른 파라미터를 받는 클래스들을 어댑터 타겟 인터페이스로 연결합니다.
어댑터 클래스만 구현해서 변경하면, 서로 다른 구현을 하나의 인터페이스로 일관되게 사용할 수 있습니다.

브리지 패턴 (Bridge)

추상화, 구현을 분리하고 구성(has-a) 관계로 연결하여 각각을 독립적으로 확장할 수 있는 패턴입니다.
상속 대신 객체 내부에 구현을 참조하고 위임하는 구조가 특징입니다.

브리지 패턴 예시 1

// 구현용 인터페이스
interface Device {
  void turnOn();
  void turnOff();
  void setVolume(int volume);
  boolean isEnabled();
}

// 구현 클래스 : TV
class TV implements Device {
  private boolean on = false;
  private int volume = 30;

  @Override
  public void turnOn() {
    on = true;
    System.out.println("TV is now ON.");
  }

  @Override
  public void turnOff() {
    on = false;
    System.out.println("TV is now OFF.");
  }

  @Override
  public void setVolume(int volume) {
    this.volume = volume;
    System.out.println("TV volume set to " + volume);
  }

  @Override
  public boolean isEnabled() {
    return on;
  }
}

// 구현 클래스 : 라디오
class Radio implements Device {
  private boolean on = false;
  private int volume = 30;

  @Override
  public void turnOn() {
    on = true;
    System.out.println("Radio is now ON.");
  }

  @Override
  public void turnOff() {
    on = false;
    System.out.println("Radio is now OFF.");
  }

  @Override
  public void setVolume(int volume) {
    this.volume = volume;
    System.out.println("Radio volume set to " + volume);
  }

  @Override
  public boolean isEnabled() {
    return on;
  }
}

// 추상 클래스
abstract class Remote {
  protected Device device;

  // 생성자에서 구현용 인터페이스에 의존 ★
  protected Remote(Device device) {
    this.device = device;
  }

  // 추상 메서드
  public abstract void power();

  // 구상 메서드에서 인터페이스 함수 호출
  public void volumeUp() {
    device.setVolume(device.isEnabled() ? 1 : 0);
  }

  // 구상 메서드에서 인터페이스 함수 호출
  public void volumeDown() {
    device.setVolume(device.isEnabled() ? -1 : 0);
  }
}

// 추상화 클래스 1 : 기본 리모컨
class BasicRemote extends Remote {
  public BasicRemote(Device device) {
    super(device);
  }

  @Override
  public void power() {
    if (device.isEnabled()) {
      device.turnOff();
    } else {
      device.turnOn();
    }
  }
}

// 추상화 클래스 2 : 고급 리모컨
class AdvancedRemote extends Remote {
  public AdvancedRemote(Device device) {
    super(device);
  }

  @Override
  public void power() {
    if (device.isEnabled()) {
      device.turnOff();
    } else {
      device.turnOn();
    }
  }

  // 음소거 기능 추가 구현
  public void mute() {
    device.setVolume(0);
    System.out.println("Device is muted.");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Device tv = new TV();
    Remote basicRemote = new BasicRemote(tv);
    basicRemote.power();
    basicRemote.volumeUp();
    
    System.out.println();

    Device radio = new Radio();
    AdvancedRemote advancedRemote = new AdvancedRemote(radio);
    advancedRemote.power();
    advancedRemote.mute();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

TV is now ON.
TV volume set to 1

Radio is now ON.
Radio volume set to 0
Device is muted.

브리지 패턴 예시 2

// 구현용 인터페이스
interface MessageSender {
  void sendMessage(String message);
}

// 구현 클래스 1 : EmailSender
class EmailSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending email with message: " + message);
  }
}

// 구현 클래스 2 : SMSSender
class SMSSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending SMS with message: " + message);
  }
}

// 추상 클래스
abstract class Message {
  protected MessageSender messageSender;

  // 생성자에서 구현용 인터페이스에 의존 ★
  protected Message(MessageSender messageSender) {
    this.messageSender = messageSender;
  }

  public abstract void send(String message);
}

// 추상화 클래스 1 : 텍스트 메시지
class TextMessage extends Message {
  public TextMessage(MessageSender messageSender) {
    super(messageSender);
  }

  @Override
  public void send(String message) {
    messageSender.sendMessage("Text Message: " + message);
  }
}

// 추상화 클래스 2 : 암호화된 메시지
class EncryptedMessage extends Message {
  public EncryptedMessage(MessageSender messageSender) {
    super(messageSender);
  }

  @Override
  public void send(String message) {
    String encryptedMessage = encrypt(message);
    messageSender.sendMessage(
      "Encrypted Message: " + encryptedMessage);
  }

  // 암호화 기능 추가 구현
  private String encrypt(String message) {
    return new StringBuilder(message).reverse().toString();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    MessageSender emailSender = new EmailSender();
    MessageSender smsSender = new SMSSender();

    Message textMessage = new TextMessage(emailSender);
    textMessage.send("Hello World!");

    Message encryptedMessage = new EncryptedMessage(smsSender);
    encryptedMessage.send("Hello World!");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Sending email with message: Text Message: Hello World!

Sending SMS with message: Encrypted Message: !dlroW olleH

복합체 패턴 (Composite)

복합체 패턴 예시 1

// Component
interface FileSystemComponent {
  void printName();
  int getSize();
  String getName();
}

// Leaf
class File implements FileSystemComponent {
  private String name;
  private int size;

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

  @Override
  public void printName() {
    System.out.println("File: " + name);
  }

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

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

// Composite
class Directory implements FileSystemComponent {
  private String name;
  private List<FileSystemComponent> components
    = new ArrayList<>();

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

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

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

  public void remove(String name) {
    components.removeIf(component -> component.getName().equals(name));
  }

  @Override
  public void printName() {
    System.out.println("Directory: " + name);
    for (FileSystemComponent component : components) {
      component.printName();
    }
  }

  @Override
  public int getSize() {
    int totalSize = 0;
    for (FileSystemComponent component : components) {
      totalSize += component.getSize();
    }
    return totalSize;
  }

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

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    File file1 = new File("Document.txt", 100);
    File file2 = new File("Image.jpg", 200);

    Directory subDir = new Directory("SubFolder");
    subDir.add(new File("SubFile.txt", 50));

    Directory rootDir = new Directory("RootFolder");
    rootDir.add(file1);
    rootDir.add(file2);
    rootDir.add(subDir);

    System.out.println("Initial structure:");
    rootDir.printName();
    System.out.println("Total size: " + rootDir.getSize() + " KB");

    System.out.println("\nRemoving Image.jpg:");
    rootDir.remove("Image.jpg");
    rootDir.printName();
    System.out.println("Total " + rootDir.getSize() + " KB");

    System.out.println("\nRemoving SubFolder:");
    rootDir.remove(subDir);
    rootDir.printName();
    System.out.println("Total size: " + rootDir.getSize() + " KB");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Initial structure:
Directory: RootFolder
File: Document.txt
File: Image.jpg
Directory: SubFolder
File: SubFile.txt
Total size: 350 KB

Removing Image.jpg:
Directory: RootFolder
File: Document.txt
Directory: SubFolder
File: SubFile.txt
Total size: 150 KB

Removing SubFolder:
Directory: RootFolder
File: Document.txt
Total size: 100 KB

복합체 패턴 예시 2

interface UIComponent {
  void render();
  void add(UIComponent component);
  void remove(UIComponent component);
}

// Leaves
class Button implements UIComponent {
  private String label;

  public Button(String label) {
    this.label = label;
  }

  @Override
  public void render() {
    System.out.println("Button: " + label);
  }

  @Override
  public void add(UIComponent component) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void remove(UIComponent component) {
    throw new UnsupportedOperationException();
  }
}

class TextBox implements UIComponent {
  private String text;

  public TextBox(String text) {
    this.text = text;
  }

  @Override
  public void render() {
    System.out.println("TextBox: " + text);
  }

  @Override
  public void add(UIComponent component) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void remove(UIComponent component) {
    throw new UnsupportedOperationException();
  }
}

// Composites
class Panel implements UIComponent {
  private String name;
  private List<UIComponent> components = new ArrayList<>();

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

  @Override
  public void render() {
    System.out.println("Panel: " + name);
    components.forEach(UIComponent::render);
  }

  @Override
  public void add(UIComponent component) {
    components.add(component);
  }

  @Override
  public void remove(UIComponent component) {
    components.remove(component);
  }
}

class Window implements UIComponent {
  private String title;
  private List<UIComponent> components = new ArrayList<>();

  public Window(String title) {
    this.title = title;
  }

  @Override
  public void render() {
    System.out.println("Window: " + title);
    components.forEach(UIComponent::render);
  }

  @Override
  public void add(UIComponent component) {
    components.add(component);
  }

  @Override
  public void remove(UIComponent component) {
    components.remove(component);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Button submitButton = new Button("Submit");
    Button cancelButton = new Button("Cancel");
    TextBox usernameField = new TextBox("Username");

    Panel formPanel = new Panel("Form");
    formPanel.add(submitButton);
    formPanel.add(cancelButton);
    formPanel.add(usernameField);

    Window mainWindow = new Window("Main");
    mainWindow.add(formPanel);
    mainWindow.render();

    System.out.println();

    formPanel.remove(cancelButton);
    mainWindow.render();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Window: Main
Panel: Form
Button: Submit
Button: Cancel
TextBox: Username

Window: Main
Panel: Form
Button: Submit
TextBox: Username

데코레이터 패턴 (Decorator)

데코레이터 패턴 예시 1

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

// ConcreteComponent class
class SimpleCoffee implements Coffee {
  @Override
  public String getDescription() {
    return "Simple coffee";
  }

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

// Decorator class
class CoffeeDecorator implements Coffee {
  protected Coffee decoratedCoffee;

  public CoffeeDecorator(Coffee coffee) {
    this.decoratedCoffee = coffee;
  }

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

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

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
  public MilkDecorator(Coffee coffee) {
    super(coffee);
  }

  @Override
  public String getDescription() {
    return super.getDescription() + ", Milk";
  }

  @Override
  public double getCost() {
    return super.getCost() + 1.5;
  }
}

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

  @Override
  public String getDescription() {
    return super.getDescription() + ", Sugar";
  }

  @Override
  public double getCost() {
    return super.getCost() + 0.5;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    Coffee simpleCoffee = new SimpleCoffee();
    System.out.println(simpleCoffee.getDescription() + " $" + simpleCoffee.getCost());
    
    Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
    System.out.println(milkCoffee.getDescription() + " $" + milkCoffee.getCost());

    Coffee milkAndSugarCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
    
    System.out.println(milkAndSugarCoffee.getDescription() + " $" + milkAndSugarCoffee.getCost());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Simple coffee $5.0
Simple coffee, Milk $6.5
Simple coffee, Milk, Sugar $7.0

데코레이터 패턴 예시 2

// Base Component
interface Text {
  String getContent();
}

// Concrete Component
class PlainText implements Text {
  private String content;

  public PlainText(String content) {
    this.content = content;
  }

  @Override
  public String getContent() {
    return content;
  }
}

// Base Decorator
abstract class TextDecorator implements Text {
  protected Text decoratedText;

  public TextDecorator(Text text) {
    this.decoratedText = text;
  }

  @Override
  public String getContent() {
    return decoratedText.getContent();
  }
}

// Concrete Decorators
class BoldDecorator extends TextDecorator {
  public BoldDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<b>" + super.getContent() + "</b>";
  }
}

class ItalicDecorator extends TextDecorator {
  public ItalicDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<i>" + super.getContent() + "</i>";
  }
}

class UnderlineDecorator extends TextDecorator {
  public UnderlineDecorator(Text text) {
    super(text);
  }

  @Override
  public String getContent() {
    return "<u>" + super.getContent() + "</u>";
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Create a plain text
    Text text = new PlainText("Hello, Decorator Pattern!");
    System.out.println("Plain text: " + text.getContent());

    text = new BoldDecorator(text);
    System.out.println("Bold text: " + text.getContent());

    text = new ItalicDecorator(text);
    System.out.println("Bold and italic text: " + text.getContent());

    text = new UnderlineDecorator(text);
    System.out.println("Bold, italic, and underlined text: " + text.getContent());

    Text anotherText = new UnderlineDecorator(new ItalicDecorator(new PlainText("Another example")));
    System.out.println("Underlined and italic text: " + anotherText.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Plain text: Hello, Decorator Pattern!
Bold text: <b>Hello, Decorator Pattern!</b>
Bold and italic text: <i><b>Hello, Decorator Pattern!</b></i>
Bold, italic, and underlined text:
 <u><i><b>Hello, Decorator Pattern!</b></i></u>
Underlined and italic text: <u><i>Another example</i></u>

파사드 패턴 (Facade)

여러 개의 복잡한 서브시스템 클래스들을 하나의 파사드 클래스로 묶습니다.
클라이언트가 서브시스템을 직접 호출하지 않고, 파사드 클래스만 호출하여 기능들을 쉽게 이용할 수 있는 패턴입니다.

파사드 클래스는 내부 구조의 복잡성을 은폐하고, 단순한 인터페이스를 제공합니다.
클라이언트는 파사드 클래스에만 의존하므로 결합도가 낮아집니다.
서브시스템 변경이 생기면 파사드 클래스 내부만 수정하면 되어서, 클라이언트 코드 변경이 최소화됩니다.

파사드 패턴 예시 1 : 스마트홈 아침 루틴

// 서브시스템 클래스 1 : 온도 조절기
public class Thermostat {
  public void setTemperature(int temperature) {
    System.out.println("집 온도 : " + temperature + "로 설정");
  }
}

// 서브시스템 클래스 2 : 전등
public class Lights {
  public void on() {
    System.out.println("전등 켜기");
  }

  public void off() {
    System.out.println("전등 끄기");
  }
}

// 서브시스템 클래스 3 : 커피머신
public class CoffeeMaker {
  public void brewCoffee() {
    System.out.println("커피 내리기");
  }
}

// 파사드 클래스
public class SmartHomeFacade {
  private Thermostat thermostat;
  private Lights lights;
  private CoffeeMaker coffeeMaker;

  public SmartHomeFacade(Thermostat thermostat, Lights lights, CoffeeMaker coffeeMaker) {
    this.thermostat = thermostat;
    this.lights = lights;
    this.coffeeMaker = coffeeMaker;
  }

  // 기상 후 루틴 함수
  public void wakeUp() {
    // 각 서브시스템 함수 호출
    thermostat.setTemperature(22);
    lights.on();
    coffeeMaker.brewCoffee();
  }

  // 외출 시 루틴 함수
  public void leaveHome() {
    // 각 서브시스템 함수 호출
    thermostat.setTemperature(18);
    lights.off();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 서브시스템 객체들 생성
    Thermostat thermostat = new Thermostat();
    Lights lights = new Lights();
    CoffeeMaker coffeeMaker = new CoffeeMaker();
  
    // 파사드 클래스 생성자로 서브시스템 객체들 전달
    SmartHomeFacade smartHome = new SmartHomeFacade(thermostat, lights, coffeeMaker);

    // 기상 후 루틴 함수 호출
    smartHome.wakeUp();
    
    System.out.println();

    // 외출 시 루틴 함수 호출
    smartHome.leaveHome();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

집 온도 : 22로 설정
전등 켜기
커피 내리기

집 온도 : 18로 설정
전등 끄기

파사드 패턴 예시 2 : 파일 시스템 작업

// 서브시스템 클래스 1 : 파일 읽기
class FileReader {
  public String readFile(String filePath) throws IOException {
    // 파일 읽기 기능
    return new String(Files.readAllBytes(Paths.get(filePath)));
  }
}

// 서브시스템 클래스 2 : 파일 쓰기
class FileWriter {
  public void writeFile(String filePath, String content) throws IOException {
    // 파일 쓰기 기능
    Files.write(Paths.get(filePath), content.getBytes());
  }
}

// 서브시스템 클래스 3 : 파일 삭제
class FileDeleter {
  public void deleteFile(String filePath) throws IOException {
    // 파일 삭제 기능
    Files.delete(Paths.get(filePath));
  }
}

// 파사드 클래스
class FileSystemFacade {
  private FileReader fileReader;
  private FileWriter fileWriter;
  private FileDeleter fileDeleter;

  // 생성자에서 서브시스템 객체들 생성
  public FileSystemFacade() {
    this.fileReader = new FileReader();
    this.fileWriter = new FileWriter();
    this.fileDeleter = new FileDeleter();
  }

  // 파일 읽기 함수
  public String readFile(String filePath) {
    try {
      // 파일 읽기 서브시스템 함수 호출
      return fileReader.readFile(filePath);
    } catch (IOException e) {
      System.err.println("Error reading file: " + e.getMessage());
      return null;
    }
  }

  // 파일 쓰기 함수
  public boolean writeFile(String filePath, String content) {
    try {
      // 파일 쓰기 서브시스템 함수 호출
      fileWriter.writeFile(filePath, content);
      return true;
    } catch (IOException e) {
      System.err.println("Error writing file: " + e.getMessage());
      return false;
    }
  }

  // 파일 삭제 함수
  public boolean deleteFile(String filePath) {
    try {
      // 파일 삭제 서브시스템 함수 호출
      fileDeleter.deleteFile(filePath);
      return true;
    } catch (IOException e) {
      System.err.println("Error deleting file: " + e.getMessage());
      return false;
    }
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 파사드 클래스 객체 생성
    FileSystemFacade fs = new FileSystemFacade();

    // 파일 쓰기 함수 호출
    boolean writeSuccess = fs.writeFile("test.txt", "Hello, Facade Pattern!");
    System.out.println("File write success: " + writeSuccess);

    // 파일 읽기 함수 호출
    String content = fs.readFile("test.txt");
    System.out.println("File content: " + content);

    // 파일 삭제 함수 호출
    boolean deleteSuccess = fs.deleteFile("test.txt");
    System.out.println("File delete success: " + deleteSuccess);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

File write success: true
File content: Hello, Facade Pattern!
File delete success: true

플라이웨이트 패턴 (Flyweight)

같은 객체가 여러 번 필요할 때, 중복 객체 생성을 줄이고 객체를 공유 및 재사용하여 메모리를 절약하는 패턴입니다.

플라이웨이트 패턴 예시 1

// 플라이웨이트 클래스 : 책
class Book {
  // 책 이름이 같으면 같은 책
  private final String title;

  // 생성자
  public Book(String title) {
    this.title = title;
  }

  public void read() {
    System.out.println("Reading the book titled: " + title);
  }
}

// 플라이웨이트 팩토리 클래스 : 책장
class Bookshelf {
  // 책 이름을 키로, 책 객체를 값으로 저장하여 책 중복 방지하는 해시맵
  private static final Map<String, Book> bookshelf = new HashMap<>();

  // 책 조회 함수 (정적 메서드)
  public static Book getBook(String title) {
    // 해시맵에서 책 이름을 키로 가진 책 조회
    Book book = bookshelf.get(title);

    // 저장된 책이 없으면,
    if (book == null) {
      // 신규 책 생성 후 해시맵에 추가
      book = new Book(title);
      bookshelf.put(title, book);
      System.out.println("Adding a new book to the bookshelf: " + title);

    } else {
      // 기존 책 재사용
      System.out.println("Reusing existing book from the bookshelf: " + title);
    }

    // 책 객체 반환
    return book;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 책 조회
    Book book1 = Bookshelf.getBook("Effective Java");
    book1.read(); // 신규 생성한 책 반환

    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 동일한 책 조회
    Book book2 = Bookshelf.getBook("Effective Java");
    book2.read(); // 해시맵에서 기존 책 반환

    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 다른 책 조회
    Book book3 = Bookshelf.getBook("Clean Code");
    book3.read(); // 신규 생성한 책 반환

    // book1, book2가 서로 같은 객체 주소값을 갖고 있는지 체크
    System.out.println(book1 == book2 ? "Same book for 'Effective Java'." : "Different books for 'Effective Java'.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Adding a new book to the bookshelf: Effective Java
Reading the book titled: Effective Java
Reusing existing book from the bookshelf: Effective Java
Reading the book titled: Effective Java
Adding a new book to the bookshelf: Clean Code
Reading the book titled: Clean Code
Same book for 'Effective Java'.

플라이웨이트 패턴 예시 2

// 플라이웨이트 인터페이스
interface Font {
    void apply(String text);
}

// 플라이웨이트 인터페이스를 구현한 플라이웨이트 클래스
class ConcreteFont implements Font {
  // 모든 필드 값이 같으면 같은 폰트
  private String font;
  private int size;
  private String color;

  // 생성자
  public ConcreteFont(String font, int size, String color) {
    this.font = font;
    this.size = size;
    this.color = color;
  }

  // 폰트 사용 함수
  @Override
  public void apply(String text) {
    System.out.println("Text: '" + text + "' with Font: " + font + ", Size: " + size + ", Color: " + color);
  }
}

// 플라이웨이트 팩토리 클래스
class FontFactory {
  // 폰트 정보 조합을 키로, 폰트 객체를 값으로 저장하여 폰트 중복 방지하는 해시맵
  private static final HashMap<String, Font> fontMap = new HashMap<>();

  // 폰트 조회 함수 (정적 메서드)
  public static Font getFont(String font, int size, String color) {
    // 폰트 정보 조합을 키로 기존 폰트 객체 조회
    String key = font + ":" + size + ":" + color;
    Font fontObject = fontMap.get(key);

    // 저장된 폰트가 없으면,
    if (fontObject == null) {
      // 신규 폰트 생성 후 해시맵에 추가
      fontObject = new ConcreteFont(font, size, color);
      fontMap.put(key, fontObject);
      System.out.println("Creating font: " + key);

    } else {
      // 기존 폰트 재사용
      System.out.println("Reusing font: " + key);
    }

    // 폰트 객체 반환
    return fontObject;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 폰트 조회
    Font font1 = FontFactory.getFont("Arial", 12, "Black");
    font1.apply("Hello, World!"); // 신규 생성한 폰트 사용

    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 동일한 폰트 조회
    Font font2 = FontFactory.getFont("Arial", 12, "Black");
    font2.apply("Flyweight Pattern"); // 기존 폰트 재사용

    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 다른 폰트 조회
    Font font3 = FontFactory.getFont("Times New Roman", 14, "Blue");
    font3.apply("Design Patterns"); // 신규 생성한 폰트 사용

    // 플라이웨이트 팩토리 클래스의 정적 메서드를 호출하여 기존 폰트 조회
    Font font4 = FontFactory.getFont("Arial", 12, "Black");
    font4.apply("Another Text"); // 기존 폰트 재사용
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Creating font: Arial:12:Black
Text: 'Hello, World!' with Font: Arial, Size: 12, Color: Black
Reusing font: Arial:12:Black
Text: 'Flyweight Pattern' with Font: Arial, Size: 12, Color: Black
Creating font: Times New Roman:14:Blue
Text: 'Design Patterns' with Font: Times New Roman, Size: 14, Color: Blue
Reusing font: Arial:12:Black
Text: 'Another Text' with Font: Arial, Size: 12, Color: Black

프록시 패턴 (Proxy)

실제 객체에 대한 접근을 대리하는 프록시 객체를 사용하는 패턴입니다.

프록시 패턴 사용 예시

  • 무거운 작업이 포함된 실제 객체 생성을 필요한 시점까지 지연
  • 생성한 객체 또는 작업 결과를 캐싱
  • 접근 제어가 필요한 로직을 분리 관리

프록시 패턴 예시 1 : 대용량 이미지 파일

// 주제 인터페이스
interface Image {
  void display();
  String getFileName();
}

// 주제 인터페이스를 구현한 실제 주제 클래스
class RealImage implements Image {
  private String fileName;

  public RealImage(String fileName) {
    this.fileName = fileName;

    // 인스턴스 생성 시점에 대용량 이미지 로드
    loadFromDisk();
  }

  // 디스크에서 대용량 이미지를 로드하는 무거운 작업
  private void loadFromDisk() {
    System.out.println("Loading " + fileName);
  }

  // 프록시 객체 함수를 통해 호출될 함수
  @Override
  public void display() {
    System.out.println("Displaying " + fileName);
  }

  @Override
  public String getFileName() {
    return fileName;
  }
}

// 주제 인터페이스를 구현한 프록시 클래스
class ProxyImage implements Image {
  // 실제 주제 클래스 필드
  private RealImage realImage;
  private String fileName;

  public ProxyImage(String fileName) {
    this.fileName = fileName;
  }

  @Override
  public void display() {
    // 실제 객체가 없다면,
    if (realImage == null) {
      // 실제 객체 생성 (대용량 이미지 파일 로드)
      realImage = new RealImage(fileName);
    }

    // 실제 객체 함수 호출
    realImage.display();
  }

  @Override
  public String getFileName() {
    // 프록시 클래스에서 직접 반환
    return fileName;
  }

  // 파일 확장자를 알려주는 부가 기능 추가
  public String getFileExtension() {
    int lastIndex = fileName.lastIndexOf('.');
    if (lastIndex == -1) {
      return "";
    }
    return fileName.substring(lastIndex + 1);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 프록시 객체 생성
    ProxyImage image = new ProxyImage("test_image.jpg");

    // 프록시 객체 함수 호출
    System.out.println("File name: " + image.getFileName());
    System.out.println("File extension: " + image.getFileExtension());

    // 프록시 객체 함수를 통해 실제 객체 생성 후 함수 호출
    image.display();

    // 기존 실제 객체 재사용
    image.display();
  }
}

실제 객체 생성 비용이 큰 경우, 필요한 순간까지 객체 생성을 지연시키는 가상 프록시 예시입니다.
클라이언트에서는 실제 객체를 직접 생성하지 않고 프록시 객체만 사용하며,
실제 객체가 필요한 시점에는 프록시 객체 내부에서 생성합니다.
위 코드의 실행 결과는 아래와 같습니다.

File name: test_image.jpg
File extension: jpg
Loading test_image.jpg // 최초 1회만 로드
Displaying test_image.jpg
Displaying test_image.jpg

프록시 패턴 예시 2

// 주제 인터페이스
interface BankAccount {
  void withdraw(double amount);
  void deposit(double amount);
}

// 주제 인터페이스를 구현한 실제 주제 클래스
class RealBankAccount implements BankAccount {
  private double balance;
  
  public RealBankAccount(double initialBalance) {
    this.balance = initialBalance;
  }

  // 출금 함수
  @Override
  public void withdraw(double amount) {
    if (balance >= amount) {
      balance -= amount;
      System.out.println(amount + " withdrawn. Current balance: " + balance);
    } else {
      System.out.println("Insufficient balance.");
    }
  }

  // 입금 함수
  @Override
  public void deposit(double amount) {
    balance += amount;
    System.out.println(amount + " deposited. Current balance: " + balance);
  }
}

// 주제 인터페이스를 구현한 프록시 클래스
class BankAccountProxy implements BankAccount {
  // 실제 주제 클래스 필드
  private RealBankAccount realBankAccount;
  private String userRole;
  
  // 생성자에서 유저 권한, 초기 잔액 저장
  public BankAccountProxy(String userRole, double initialBalance) {
    this.userRole = userRole;
    this.realBankAccount = new RealBankAccount(initialBalance);
  }

  // 어드민 권한을 가진 유저인지 체크
  private boolean hasAccess() {
    return "Admin".equalsIgnoreCase(userRole);
  }

  @Override
  public void withdraw(double amount) {
    // 어드민 권한을 가진 유저면,
    if (hasAccess()) {
      // 실제 주제 클래스 출금 함수 호출
      realBankAccount.withdraw(amount);
    } else {
      System.out.println("Access denied. Only Admin can withdraw.");
    }
  }

  @Override
  public void deposit(double amount) {
    // 입금 함수 호출
    realBankAccount.deposit(amount);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 프록시 클래스로 어드민 권한 유저 객체 생성
    BankAccount adminAccount = new BankAccountProxy("Admin", 1000);
    adminAccount.deposit(500);   // 입금  
    adminAccount.withdraw(300);  // 출금

    // 프록시 클래스로 일반 유저 객체 생성
    BankAccount userAccount = new BankAccountProxy("User", 1000);
    userAccount.deposit(500);    // 입금
    userAccount.withdraw(300);   // 출금 불가
  }
}

프록시 객체로 출금 권한을 제어하는 보호 프록시 예제입니다.
보안 정책이 변경되어도 실제 클래스 코드에 영향이 가지 않습니다.
위 코드의 실행 결과는 아래와 같습니다.

500.0 deposited. Current balance: 1500.0
300.0 withdrawn. Current balance: 1200.0
500.0 deposited. Current balance: 1500.0
Access denied. Only Admin can withdraw.

행위 패턴

책임 연쇄 패턴 (Chain of Responsibility)

책임 연쇄 패턴 예시 1

abstract class Handler {
    protected Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public void handle(int number) {
        process(number);
        if (next != null) next.handle(number);
    }

    protected abstract void process(int number);
}

class PositiveHandler extends Handler {
    @Override
    protected void process(int number) {
        if (number > 0) {
            System.out.println(number + " is a positive number");
        }
    }
}

class EvenHandler extends Handler {
    @Override
    protected void process(int number) {
        if (number % 2 == 0) {
            System.out.println(number + " is an even number");
        }
    }
}

class DivisibleBy3Handler extends Handler {
    @Override
    protected void process(int number) {
        if (number % 3 == 0) {
            System.out.println(number + " is divible by 3");
        }
    }
}

// 클라이언트 (호출부)
public class Main {
    public static void main(String[] args) {
        Handler positive = new PositiveHandler();
        Handler even = new EvenHandler();
        Handler divisibleBy3 = new DivisibleBy3Handler();

        positive.setNext(even);
        even.setNext(divisibleBy3);

        positive.handle(-2);
        positive.handle(4);
        positive.handle(6);
    }
}

위 코드의 실행 결과는 아래와 같습니다.

-2 is an even number

4 is a positive number
4 is an even number

6 is a positive number
6 is an even number
6 is divible by 3

책임 연쇄 패턴 예시 2

enum LogLevel {
  INFO, DEBUG, WARN
}

abstract class Logger {
  protected LogLevel level;
  protected Logger nextLogger;

  public void setNextLogger(Logger nextLogger) {
    this.nextLogger = nextLogger;
  }

  public void logMessage(LogLevel level, String message) {
    if (this.level.ordinal() <= level.ordinal()) {
      write(message);
    }
    if (nextLogger != null) {
      nextLogger.logMessage(level, message);
    }
  }

  protected abstract void write(String message);
}

class ConsoleLogger extends Logger {
  public ConsoleLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("Console::Logger: " + message);
  }
}

class FileLogger extends Logger {
  public FileLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("File::Logger: " + message);
  }
}

class NetworkLogger extends Logger {
  public NetworkLogger(LogLevel level) {
    this.level = level;
  }

  @Override
  protected void write(String message) {
    System.out.println("Network::Logger: " + message);
  }
}

// 클라이언트 (호출부)
public class Main {
  private static Logger getChainOfLoggers() {
    Logger networkLogger = new NetworkLogger(LogLevel.WARN);
    Logger fileLogger = new FileLogger(LogLevel.DEBUG);
    Logger consoleLogger = new ConsoleLogger(LogLevel.INFO);

    networkLogger.setNextLogger(fileLogger);
    fileLogger.setNextLogger(consoleLogger);

    return networkLogger;
  }

  public static void main(String[] args) {
    Logger loggerChain = getChainOfLoggers();

    loggerChain.logMessage(LogLevel.INFO, 
        "This is an information.");
    
    loggerChain.logMessage(LogLevel.DEBUG,
        "This is a debug level information.");
    
    loggerChain.logMessage(LogLevel.WARN,
        "This is a warning information.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Console::Logger: This is an information.

File::Logger: This is a debug level information.
Console::Logger: This is a debug level information.

Network::Logger: This is a warning information.
File::Logger: This is a warning information.
Console::Logger: This is a warning information.

커맨드 패턴 (Command)

커맨드 패턴 예시 1

// Receiver Class
public class Light {
  public void turnOn() {
    System.out.println("Light is ON");
  }

  public void turnOff() {
    System.out.println("Light is OFF");
  }
}

// Command Interface
public interface Command {
  void execute();
}

// Concrete Commands
public class LightOnCommand implements Command {
  private Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  @Override
  public void execute() {
    light.turnOn();
  }
}

public class LightOffCommand implements Command {
  private Light light;

  public LightOffCommand(Light light) {
    this.light = light;
  }

  @Override
  public void execute() {
    light.turnOff();
  }
}

// Invoker Class
public class RemoteControl {
  private Command command;

  public void setCommand(Command command) {
    this.command = command;
  }

  public void pressButton() {
    command.execute();
  }
}

public class Client {
  public static void main(String[] args) {
    Light livingRoomLight = new Light();
    
    Command lightOn = new LightOnCommand(livingRoomLight);
    
    Command lightOff = new LightOffCommand(livingRoomLight);
    
    RemoteControl remote = new RemoteControl();
    
    remote.setCommand(lightOn);
    remote.pressButton(); // "Light is ON"
    
    remote.setCommand(lightOff);
    remote.pressButton(); // "Light is OFF"
  }
}

// Receiver
public class TextEditor {
  private StringBuilder content;

  public TextEditor() {
    this.content = new StringBuilder();
  }

  public void insertText(String text, int position) {
    content.insert(position, text);
  }

  public void deleteText(int position, int length) {
    content.delete(position, position + length);
  }

  public String getTextSubstring(int start, int end) {
    return content.substring(start, end);
  }

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

// Command interface
public interface Command {
  void execute();
  void undo();
}

// Concrete commands
public class InsertTextCommand implements Command {
  private TextEditor editor;
  private String text;
  private int position;
  
  public InsertTextCommand(
    TextEditor editor, String text, int position) 
  {
    this.editor = editor;
    this.text = text;
    this.position = position;
  }

  @Override
  public void execute() {
    editor.insertText(text, position);
  }
  @Override
  public void undo() {
    editor.deleteText(position, text.length());
  }
}

public class DeleteTextCommand implements Command {
  private TextEditor editor;
  private String deletedText;
  private int position;

  public DeleteTextCommand(
    TextEditor editor, int position, int length) 
  {
    this.editor = editor;
    this.position = position;
    this.deletedText = editor.getTextSubstring(
        position, position + length);
  }

  @Override
  public void execute() {
    editor.deleteText(position, deletedText.length());
  }
  @Override
  public void undo() {
    editor.insertText(deletedText, position);
  }
}

// Invoker
public class TextEditorInvoker {
  private Stack<Command> undoStack = new Stack<>();
  private Stack<Command> redoStack = new Stack<>();

  public void executeCommand(Command command) {
    command.execute();
    undoStack.push(command);
    redoStack.clear();
  }

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

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

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    TextEditor editor = new TextEditor();
    TextEditorInvoker invoker = new TextEditorInvoker();

    Command insertHello = new InsertTextCommand(editor, "Hello, ", 0);
    invoker.executeCommand(insertHello);

    Command insertWorld = new InsertTextCommand(editor, "World!", 7);
    invoker.executeCommand(insertWorld);

    System.out.println("Current text: " + editor.getContent());

    invoker.undo();
    System.out.println("After undo: " + editor.getContent());

    invoker.redo();
    System.out.println("After redo: " + editor.getContent());

    Command deleteCommand = new DeleteTextCommand(editor, 0, 7);
    invoker.executeCommand(deleteCommand);
    System.out.println("After delete: " + editor.getContent());

    invoker.undo();
    System.out.println("Final text: " + editor.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Current text: Hello, World!
After undo: Hello, 
After redo: Hello, World!
After delete: World!
Final text: Hello, World!

인터프리터 패턴 (Interpreter)

인터프리터 패턴 예시 1

public interface Expression {
  int interpret();
}

public class Number implements Expression {
  private int number;

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

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

public class Add implements Expression {
  private Expression leftExpression;
  private Expression rightExpression;

  public Add(
    Expression leftExpression,
    Expression rightExpression
  ) {
    this.leftExpression = leftExpression;
    this.rightExpression = rightExpression;
  }

  @Override
  public int interpret() {
      return leftExpression.interpret() + rightExpression.interpret();
  }
}

public class Subtract implements Expression {
  private Expression leftExpression;
  private Expression rightExpression;

  public Subtract(
    Expression leftExpression,
    Expression rightExpression
  ) {
    this.leftExpression = leftExpression;
    this.rightExpression = rightExpression;
  }

  @Override
  public int interpret() {
    return leftExpression.interpret() - rightExpression.interpret();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
  
    Expression five = new Number(5);
    Expression two = new Number(2);
    Expression three = new Number(3);

    Expression addExpression = new Add(five, two);
    
    Expression subtractExpression = new Subtract(addExpression, three);

    System.out.println("(5 + 2) - 3 = " + subtractExpression.interpret());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

(5 + 2) - 3 = 4

인터프리터 패턴 예시 2

// Context
class Context {
  private Map<String, List<Map<String, String>>> tables;

  public Context() {
    this.tables = new HashMap<>();

    // Initialize sample data
    List<Map<String, String>> users = new ArrayList<>();

    users.add(new HashMap<String, String>() );

    users.add(new HashMap<String, String>() );

    tables.put("users", users);
  }

  public List<Map<String, String>> getTable(String name) {
    return tables.get(name);
  }

  public void setTable(
    String name, List<Map<String, String>> table
  ) {
    tables.put(name, table);
  }
}

// Abstract Expression
interface Expression {
  List<Map<String, String>> interpret(Context context);
}

// WHERE clause expression
class WhereExpression implements Expression {
  private String column;
  private String operator;
  private String value;
  private String tableName;

  public WhereExpression(
    String tableName, String column,
    String operator, String value
  ) {
    this.tableName = tableName;
    this.column = column;
    this.operator = operator;
    this.value = value;
  }

  @Override
  public List<Map<String, String>> interpret(Context context) {
    List<Map<String, String>> result = new ArrayList<>();
    List<Map<String, String>> table = context.getTable(tableName);
    for (Map<String, String> row : table) {
      if (evaluate(row.get(column), operator, value)) {
        result.add(row);
      }
    }
    return result;
  }

  private boolean evaluate(
    String columnValue, String operator, String value
  ) {
    switch (operator) {
      case "=":
        return columnValue.equals(value);
      case ">":
        return Integer.parseInt(columnValue)
          > Integer.parseInt(value);
      case "<":
        return Integer.parseInt(columnValue)
          < Integer.parseInt(value);
      default:
        return false;
    }
  }
}

// SELECT statement expression
class SelectExpression implements Expression {
  private String tableName;
  private List<String> columns;
  private Expression whereClause;

  public SelectExpression(String tableName, List<String> columns, Expression whereClause) {
    this.tableName = tableName;
    this.columns = columns;
    this.whereClause = whereClause;
  }

  @Override
  public List<Map<String, String>> interpret(Context context) {
    List<Map<String, String>> table = context.getTable(tableName);
    List<Map<String, String>> result = new ArrayList<>();

    for (Map<String, String> row : table) {
      Context rowContext = new Context();
      rowContext.setTable(
        tableName, Collections.singletonList(row));

      if (whereClause == null || !whereClause.interpret(rowContext).isEmpty()) {
        Map<String, String> newRow = new HashMap<>();
        
        for (String column : columns) {
          if (column.equals("*")) {
            newRow.putAll(row);
          } else {
            newRow.put(column, row.get(column));
          }
        }
        result.add(newRow);
      }
    }

    return result;
  }
}

// SQL Parser
class SQLParser {
  public static Expression parse(String query) {
    String[] parts = query.split("\\s+");
    if (!parts[0].equalsIgnoreCase("SELECT")) {
      throw new RuntimeException(
        "Only SELECT statements are supported");
    }

    List<String> columns = Arrays.asList(parts[1].split(","));
    String tableName = parts[3];

    Expression whereClause = null;
    if (parts.length > 4 
      && parts[4].equalsIgnoreCase("WHERE")) {
      whereClause = new WhereExpression(
        tableName, parts[5], parts[6], parts[7]);
    }

    return new SelectExpression(tableName, columns, whereClause);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Context context = new Context();

    // Test query 1: Select all columns from users
    String query1 = "SELECT * FROM users";
    Expression expr1 = SQLParser.parse(query1);
    List<Map<String, String>> result1 = expr1.interpret(context);
    
    System.out.println("Result of query: " + query1);
    for (Map<String, String> row : result1) {
      System.out.println(row);
    }

    // Test query 2: Select name and age of users older than 27
    String query2 = "SELECT name,age FROM users WHERE age > 27";
    Expression expr2 = SQLParser.parse(query2);
    List<Map<String, String>> result2 = expr2.interpret(context);
    
    System.out.println("\nResult of query: " + query2);
    for (Map<String, String> row : result2) {
      System.out.println(row);
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Result of query: SELECT * FROM users
{name=John, id=1, age=30}
{name=Jane, id=2, age=25}

Result of query: SELECT name,age FROM users WHERE age > 27
{name=John, age=30}

반복자 패턴 (Iterator)

반복자 패턴 예시 1

// Iterator Interface
interface MyIterator {
  boolean hasNext();
  Object next();
}

// Aggregate Interface
interface Collection {
  MyIterator createIterator();
}

// Concrete Aggregate
class MyList implements Collection {
  private Object[] items;
  private int last = 0;

  public MyList(int size) {
    items = new Object[size];
  }

  public void add(Object item) {
    if (last < items.length) {
      items[last] = item;
      last++;
    }
  }

  public Object get(int index) {
    return items[index];
  }

  public int size() {
    return last;
  }

  @Override
  public MyIterator createIterator() {
    return new MyListIterator(this);
  }
}

// ConcreteIterator
class MyListIterator implements MyIterator {
  private MyList list;
  private int index;

  public MyListIterator(MyList list) {
    this.list = list;
    this.index = 0;
  }

  @Override
  public boolean hasNext() {
    return index < list.size();
  }

  @Override
  public Object next() {
    if (this.hasNext()) {
      return list.get(index++);
    }
    return null;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    MyList list = new MyList(3);
    list.add("A");
    list.add("B");
    list.add("C");

    MyIterator iterator = list.createIterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

A
B
C

반복자 패턴 예시 2

// FileSystemItem interface
interface FileSystemItem {
  String getName();
}

// File class
class File implements FileSystemItem {
  private String name;

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

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

// Directory class
class Directory implements FileSystemItem {
  private String name;
  private List<FileSystemItem> contents = new ArrayList<>();

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

  public void add(FileSystemItem item) {
    contents.add(item);
  }

  public List<FileSystemItem> getContents() {
    return contents;
  }

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

// FileSystemIterator interface
interface FileSystemIterator {
  boolean hasNext();
  FileSystemItem next();
}

// DepthFirstIterator class
class DepthFirstIterator implements FileSystemIterator {
  private Stack<FileSystemItem> stack = new Stack<>();

  public DepthFirstIterator(Directory root) {
    stack.push(root);
  }

  @Override
  public boolean hasNext() {
    return !stack.isEmpty();
  }

  @Override
  public FileSystemItem next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    FileSystemItem current = stack.pop();
    if (current instanceof Directory) {
      List<FileSystemItem> contents = ((Directory) current).getContents();
      for (int i = contents.size() - 1; i >= 0; i--) {
        stack.push(contents.get(i));
      }
    }
    return current;
  }
}

// BreadthFirstIterator class
class BreadthFirstIterator implements FileSystemIterator {
  private Queue<FileSystemItem> queue = new LinkedList<>();

  public BreadthFirstIterator(Directory root) {
    queue.offer(root);
  }

  @Override
  public boolean hasNext() {
    return !queue.isEmpty();
  }

  @Override
  public FileSystemItem next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    FileSystemItem current = queue.poll();
    if (current instanceof Directory) {
      queue.addAll(((Directory) current).getContents());
    }
    return current;
  }
}

// FileSystem class
class FileSystem {
  private Directory root;

  public FileSystem(Directory root) {
    this.root = root;
  }

  public FileSystemIterator depthFirstIterator() {
    return new DepthFirstIterator(root);
  }

  public FileSystemIterator breadthFirstIterator() {
    return new BreadthFirstIterator(root);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Directory root = new Directory("root");
    Directory home = new Directory("home");
    Directory user = new Directory("user");
    File file1 = new File("file1.txt");
    File file2 = new File("file2.txt");
    File file3 = new File("file3.txt");

    root.add(home);
    home.add(user);
    user.add(file1);
    user.add(file2);
    home.add(file3);

    FileSystem fileSystem = new FileSystem(root);

    System.out.println("Depth-First Traversal:");
    FileSystemIterator depthIterator = fileSystem.depthFirstIterator();
    while (depthIterator.hasNext()) {
      System.out.println(depthIterator.next().getName());
    }

    System.out.println("\nBreadth-First Traversal:");
    FileSystemIterator breadthIterator = fileSystem.breadthFirstIterator();
    while (breadthIterator.hasNext()) {
      System.out.println(breadthIterator.next().getName());
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Depth-First Traversal:
root
home
user
file1.txt
file2.txt
file3.txt

Breadth-First Traversal:
root
home
user
file3.txt
file1.txt
file2.txt

중재자 패턴 (Mediator)

여러 객체들이 서로 직접 참조하지 않고, 중재자 객체를 통해 간접적으로 소통하는 패턴입니다.

객체 간 직접적인 의존성을 줄여 결합도를 낮추고 유지보수성을 향상시킬 수 있습니다.

중재자 패턴 예시 1

// 중재자 인터페이스
public interface ChatMediator {
  void sendMessage(String message, User user);
  void addUser(User user);
}

// 중재자 인터페이스를 구현한 구체 중재자 클래스
public class ChatMediatorImpl implements ChatMediator {
  // 유저 리스트 필드
  private List<User> users;

  public ChatMediatorImpl() {
    this.users = new ArrayList<>();
  }

  // 중재자에 유저 등록 함수
  @Override
  public void addUser(User user) {
    this.users.add(user);
  }

  // 메시지 발송 함수
  @Override
  public void sendMessage(String message, User user) {
    // 각 유저들의 메시지 수신 함수 호출
    for (User u : this.users) {
      // 메시지 발송한 본인 제외
      if (u != user) {
        u.receive(message);
      }
    }
  }
}

// 유저 추상 클래스
public abstract class User {
  // 중재자 인터페이스 필드
  protected ChatMediator mediator;
  protected String name;

  // 생성자 : 중재자, 유저명 등록
  public User(ChatMediator mediator, String name) {
    this.mediator = mediator;
    this.name = name;
  }

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

// 유저 추상 클래스를 구현한 구체 유저 클래스
public class UserImpl extends User {
  // 중재자 인터페이스, 유저명 필드 상속 받음

  public UserImpl(ChatMediator mediator, String name) {
    super(mediator, name);
  }

  // 메시지 발송 함수
  @Override
  public void send(String message) {
    System.out.println(this.name + ": Sending Message = " + message);
    // 중재자를 통해 메시지 발송
    mediator.sendMessage(message, this);
  }

  // 메시지 수신 함수
  @Override
  public void receive(String message) {
    System.out.println(this.name + ": Received Message = " + message);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 중재자 객체 생성
    ChatMediator mediator = new ChatMediatorImpl();

    // 같은 중재자를 사용하는 유저 객체들 생성
    User user1 = new UserImpl(mediator, "John");
    User user2 = new UserImpl(mediator, "Jane");
    User user3 = new UserImpl(mediator, "Bob");
    User user4 = new UserImpl(mediator, "Alice");

    // 중재자에 유저 등록 (채팅방 입장)
    mediator.addUser(user1);
    mediator.addUser(user2);
    mediator.addUser(user3);
    mediator.addUser(user4);

    // 유저1이 메시지 전송 시,
    // 중재자가 각 유저들에게 메시지 전달
    user1.send("Hi All");
  }
}

중재자 클래스를 통해 유저 간 소통이 간접적으로 이루어지는 중재자 패턴 예시입니다.
위 코드의 실행 결과는 아래와 같습니다.

John: Sending Message = Hi All
Jane: Received Message = Hi All
Bob: Received Message = Hi All
Alice: Received Message = Hi All

중재자 패턴 예시 2

// 중재자 인터페이스
interface AirportMediator {
  boolean isRunwayAvailable();
  void setRunwayAvailability(boolean status);
}

// 중재자 인터페이스를 구현한 구체 중재자 클래스 : 관제탑
class AirportControlTower implements AirportMediator {
  // 활주로 사용 가능 여부 필드
  private boolean isRunwayAvailable = true;

  // 활주로 사용 가능 여부 반환
  public boolean isRunwayAvailable() {
    return isRunwayAvailable;
  }

  // 활주로 사용 가능 여부 변경
  public void setRunwayAvailability(boolean status) {
    isRunwayAvailable = status;
  }
}

// 동료 클래스 1 : 비행기
class Flight {
  private AirportMediator mediator;
  private String flightNumber;

  // 생성자 : 중재자 및 비행기 번호 설정
  public Flight(AirportMediator mediator, String flightNumber) {
    this.mediator = mediator;
    this.flightNumber = flightNumber;
  }

  // 착륙 요청 함수
  public void land() {
    // 중재자를 통해, 활주로 사용 가능 여부 확인
    if (mediator.isRunwayAvailable()) {
      // 활주로가 사용 가능하면,
      // 착륙중 메시지 출력
      System.out.println("Flight " + flightNumber + " is landing.");

      // 중재자를 통해, 활주로를 사용 불가 상태로 변경
      mediator.setRunwayAvailability(false);

    } else {
      // 착륙 대기중 메시지 출력
      System.out.println("Flight " + flightNumber + " is waiting to land.");
    }
  }
}

// 동료 클래스 2 : 활주로
class Runway {
  private AirportMediator mediator;

  // 생성자 : 중재자 설정
  public Runway(AirportMediator mediator) {
    this.mediator = mediator;
  }

  // 활주로 비우기 함수
  public void clear() {
    System.out.println("Runway is clear.");

    // 중재자를 통해, 활주로를 다시 사용 가능 상태로 변경
    mediator.setRunwayAvailability(true);
  }
}

// 클라이언트 (호출부)
public class AirportSystem {
  public static void main(String[] args) {
    // 중재자 객체 생성 (관제탑 생성)
    AirportMediator controlTower = new AirportControlTower();

    // 같은 관제탑을 공유하는 비행기 및 활주로 객체 생성
    Flight flight1 = new Flight(controlTower, "KE123");
    Flight flight2 = new Flight(controlTower, "OZ456");
    Runway runway = new Runway(controlTower);

    // 비행기 1 착륙
    // 중재자를 통해, 활주로를 사용 불가 상태로 변경
    flight1.land();

    // 비행기 2 착륙 시도
    // 활주로가 사용 중이므로 착륙 대기
    flight2.land();

    // 중재자를 통해, 활주로를 사용 가능 상태로 변경
    runway.clear();

    // 비행기 2 착륙
    flight2.land();
  }
}

활주로 사용 여부를 중재자를 통해 관리하는 중재자 패턴 예시입니다.
이처럼 여러 객체들 사이의 상호작용을 제어할 수도 있습니다.
위 코드의 실행 결과는 아래와 같습니다.

Flight KE123 is landing.
Flight OZ456 is waiting to land.
Runway is clear.
Flight OZ456 is landing.

메멘토 패턴 (Memento)

메멘토 패턴 예시 1

// Memento
class GameMemento {
  private String level;
  private int score;

  public GameMemento(String level, int score) {
    this.level = level;
    this.score = score;
  }

  public String getLevel() {
    return level;
  }

  public int getScore() {
    return score;
  }
}

// Originator
class Game {
  private String level;
  private int score;

  public void set(String level, int score) {
    this.level = level;
    this.score = score;
    System.out.println("Game state set to - Level: " + level + ", Score: " + score);
  }

  public GameMemento save() {
    return new GameMemento(level, score);
  }

  public void restore(GameMemento memento) {
    this.level = memento.getLevel();
    this.score = memento.getScore();
    System.out.println("Game state restored to - Level: " + level + ", Score: " + score);
  }
}

// Caretaker
class GameCaretaker {
  private List<GameMemento> mementoList = new ArrayList<>();

  public void add(GameMemento state) {
    mementoList.add(state);
  }

  public GameMemento get(int index) {
    return mementoList.get(index);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Game game = new Game();
    GameCaretaker caretaker = new GameCaretaker();

    game.set("Level 1", 100);
    caretaker.add(game.save());

    game.set("Level 2", 200);
    caretaker.add(game.save());

    game.set("Level 3", 300);

    game.restore(caretaker.get(1));
    game.restore(caretaker.get(0));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Game state set to - Level: Level 1, Score: 100
Game state set to - Level: Level 2, Score: 200
Game state set to - Level: Level 3, Score: 300
Game state restored to - Level: Level 2, Score: 200
Game state restored to - Level: Level 1, Score: 100

메멘토 패턴 예시 2

// Memento
class DocumentMemento {
  private final String content;

  public DocumentMemento(String content) {
    this.content = content;
  }

  public String getContent() {
    return content;
  }
}

// Originator
class Document {
  private StringBuilder content;

  public Document() {
    this.content = new StringBuilder();
  }

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

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

  public DocumentMemento save() {
    return new DocumentMemento(content.toString());
  }

  public void restore(DocumentMemento memento) {
    this.content = new StringBuilder(memento.getContent());
  }
}

// Caretaker
class DocumentHistory {
  private final Stack<DocumentMemento> history = new Stack<>();

  public void push(DocumentMemento memento) {
    history.push(memento);
  }

  public DocumentMemento pop() {
    if (!history.isEmpty()) {
      return history.pop();
    }
    return null;
  }
}

// Client
class Editor {
  private final Document document;
  private final DocumentHistory history;

  public Editor() {
    this.document = new Document();
    this.history = new DocumentHistory();
  }

  public void write(String text) {
    history.push(document.save());
    document.write(text);
  }

  public void undo() {
    DocumentMemento memento = history.pop();
    if (memento != null) {
      document.restore(memento);
    }
  }

  public String getContent() {
    return document.getContent();
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Editor editor = new Editor();

    editor.write("Hello, ");
    editor.write("this is Memento pattern. ");
    System.out.println(editor.getContent());

    editor.undo();
    System.out.println(editor.getContent());

    editor.write("This is an example implemented in Java.");
    System.out.println(editor.getContent());
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Hello, this is Memento pattern. 
Hello, 
Hello, This is an example implemented in Java.

관찰자 패턴 (Observer)

관찰 대상 1 : 관찰자 N 관계에서 관찰 대상이 변경되면, 구독 중인 관찰자들이 자동으로 통지받는 패턴입니다.
관찰자 추가 및 삭제가 용이하여, 확장성이 높고 동적인 일대다 관계를 구현할 때 사용하면 좋습니다.

관찰자 패턴 예시 1

// 관찰 대상 인터페이스
interface Subject {
  void registerObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers();
}

// 관찰자 인터페이스
interface Observer {
  void update(String news);
}

// 관찰 대상 인터페이스를 구현한 실제 관찰 대상 클래스 (뉴스 발행자)
class NewsAgency implements Subject {
  // 관찰자 리스트 필드
  private List<Observer> observers = new ArrayList<>();
  private String news;

  // 관찰자 추가 함수 (구독)
  @Override
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }

  // 관찰자 삭제 함수 (구독 취소)
  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }

  // 등록된 모든 관찰자에게 뉴스 통지 함수
  @Override
  public void notifyObservers() {
    // 관찰자 객체 순회
    for (Observer observer : observers) {
      // 각 관찰자 객체의 관찰 대상 변경 알림 함수 호출
      observer.update(news);
    }
  }

  // 신규 뉴스 발행 함수 
  public void setNews(String news) {
    // 뉴스 저장
    this.news = news;

    // 모든 관찰자에게 뉴스 통지
    notifyObservers();
  }
}

// 관찰자 인터페이스를 구현한 실제 관찰자 클래스 (뉴스 구독자)
class NewsChannel implements Observer {
  private String name;

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

  // 관찰 대상 변경 알림 함수 오버라이딩
  @Override
  public void update(String news) {
    // 신규 뉴스 출력
    System.out.println(name + " received news: " + news);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 뉴스 발행자 객체 생성
    NewsAgency agency = new NewsAgency();

    // 뉴스 관찰자 (구독자) 객체들 생성
    NewsChannel channel1 = new NewsChannel("Channel 1");
    NewsChannel channel2 = new NewsChannel("Channel 2");

    // 발행자 객체에 관찰자 객체들 추가 (구독)
    agency.registerObserver(channel1);
    agency.registerObserver(channel2);

    // 신규 뉴스 발행
    agency.setNews("Breaking news: Observer Pattern in action!");

    // 관찰자 2 삭제 (구독 취소)
    agency.removeObserver(channel2);

    // 신규 뉴스 발행
    agency.setNews("Another update: Channel 2 unsubscribed.");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Channel 1 received news: Breaking news: Observer Pattern in action!
Channel 2 received news: Breaking news: Observer Pattern in action!
Channel 1 received news: Another update: Channel 2 unsubscribed.

관찰자 패턴 예시 2

// 관찰 대상 인터페이스
interface WeatherStation {
  void registerObserver(WeatherObserver o);
  void removeObserver(WeatherObserver o);
  void notifyObservers();
}

// 관찰자 인터페이스
interface WeatherObserver { 
  void update(float temp, float humidity, float pressure);
}

// 관찰 대상 인터페이스를 구현한 실제 관찰 대상 클래스
class WeatherData implements WeatherStation {
  // 관찰자 리스트 필드
  private List<WeatherObserver> observers = new ArrayList<>();
  private float temperature, humidity, pressure;

  // 신규 날씨 정보 발행 함수
  public void setMeasurements(float temperature, float humidity, float pressure) {
    // 날씨 정보 저장
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;

    // 모든 관찰자에게 날씨 정보 통지
    notifyObservers();
  }

  // 관찰자 추가 함수 (구독)
  @Override
  public void registerObserver(WeatherObserver o) {
    observers.add(o);
  }
  
  // 관찰자 삭제 함수 (구독 취소)
  @Override
  public void removeObserver(WeatherObserver o) {
    observers.remove(o);
  }

  // 등록된 모든 관찰자에게 날씨 정보 통지 함수
  @Override
  public void notifyObservers() {
    // 관찰자 객체 순회
    for (WeatherObserver observer : observers) {
      // 각 관찰자 객체의 관찰 대상 변경 알림 함수 호출
      observer.update(temperature, humidity, pressure);
    }
  }
}

// 관찰자 인터페이스를 구현한 실제 관찰자 클래스 1 (날씨 정보 구독자)
class CurrentConditionsDisplay implements WeatherObserver {
  // 관찰 대상 변경 알림 함수 오버라이딩
  @Override
  public void update(float temp, float humidity, float pressure) {
    // 현재 날씨 출력
    System.out.println("Current: " + temp + "F, " + humidity + "% humidity");
  }
}

// 관찰자 인터페이스를 구현한 실제 관찰자 클래스 2 (날씨 정보 구독자)
class StatisticsDisplay implements WeatherObserver {
  // 관찰 대상 변경 알림 함수 오버라이딩
  @Override
  public void update(float temp, float humidity, float pressure) {
    // 날씨 통계 정보 출력
    System.out.println("Avg/Max/Min temp: " + temp + "/" + (temp + 2) + "/" + (temp - 2));
  }
}

// 관찰자 인터페이스를 구현한 실제 관찰자 클래스 3 (날씨 정보 구독자)
class ForecastDisplay implements WeatherObserver {
  // 관찰 대상 변경 알림 함수 오버라이딩
  @Override
  public void update(float temp, float humidity, float pressure) {
    // 날씨 예측 정보 출력
    System.out.println("Forecast: " + (pressure < 29.92 ? "Rain" : "Sunny"));
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 날씨 정보 발행자 객체 생성
    WeatherData weatherData = new WeatherData();

    // 날씨 정보 관찰자 (구독자) 객체들 생성
    CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
    StatisticsDisplay statisticsDisplay = new StatisticsDisplay();
    ForecastDisplay forecastDisplay = new ForecastDisplay();

    // 발행자 객체에 관찰자 객체들 추가 (구독)
    weatherData.registerObserver(currentDisplay);
    weatherData.registerObserver(statisticsDisplay);
    weatherData.registerObserver(forecastDisplay);

    // 신규 날씨 정보 발행
    weatherData.setMeasurements(80, 65, 30.4f);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Current: 80.0F, 65.0% humidity
Avg/Max/Min temp: 80.0/82.0/78.0
Forecast: Sunny

발행-구독 패턴 (Publisher-Subscriber)

발행-구독 패턴 예시 1

class Message {
  private String content;
  private String topic;

  public Message(String content, String topic) {
    this.content = content;
    this.topic = topic;
  }

  public String getContent() {
    return content;
  }

  public String getTopic() {
    return topic;
  }
}

// Publisher interface
interface Publisher {
  void publish(Message message);
}

// Subscriber interface
interface Subscriber {
  void update(Message message);
}

class Broker {
  private Map<String, List<Subscriber>> subscribers = new HashMap<>();

  public void subscribe(String topic, Subscriber subscriber) {
    subscribers.computeIfAbsent(topic, k -> new ArrayList<>()).add(subscriber);
  }

  public void publish(Message message) {
    List<Subscriber> topicSubscribers = subscribers.get(message.getTopic());
    
    if (topicSubscribers != null) {
      for (Subscriber subscriber : topicSubscribers) {
        subscriber.update(message);
      }
    }
  }
}

// Concrete Publisher
class NewsPublisher implements Publisher {
  private Broker broker;

  public NewsPublisher(Broker broker) {
    this.broker = broker;
  }

  @Override
  public void publish(Message message) {
    System.out.println("Publishing: " + message.getContent() + " on topic: " + message.getTopic());
    broker.publish(message);
  }
}

// Concrete Subscriber
class NewsSubscriber implements Subscriber {
  private String name;

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

  @Override
  public void update(Message message) {
    System.out.println(name + " received: " + message.getContent() + " on topic: " + message.getTopic());
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Broker broker = new Broker();

    NewsPublisher publisher = new NewsPublisher(broker);

    NewsSubscriber subscriber1 = new NewsSubscriber("Subscriber 1");
    NewsSubscriber subscriber2 = new NewsSubscriber("Subscriber 2");

    broker.subscribe("sports", subscriber1);
    broker.subscribe("weather", subscriber2);
    broker.subscribe("sports", subscriber2);

    publisher.publish(new Message("Liverpool won the match", "sports"));
    publisher.publish(new Message("It's sunny today", "weather"));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Publishing: Liverpool won the match on topic: sports
Subscriber 1 received: Liverpool won the match on topic: sports
Subscriber 2 received: Liverpool won the match on topic: sports
Publishing: It's sunny today on topic: weather
Subscriber 2 received: It's sunny today on topic: weather

발행-구독 패턴 예시 2

// Publisher
class MarketingDepartment {
  private EmailDeliveryService emailService;
  private String eventType;

  public MarketingDepartment(
    EmailDeliveryService emailService,
    String eventType
  ) {
    this.emailService = emailService;
    this.eventType = eventType;
  }

  public void launchCampaign(String message) {
    System.out.println("Launching campaign: " + message);
    emailService.sendEmails(eventType, message);
  }
}

// Subscriber Interface
interface Customer {
  void receiveEmail(String message);
}

// Concrete Subscriber
class IndividualCustomer implements Customer {
  private String name;

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

  @Override
  public void receiveEmail(String message) {
    System.out.println(name + " is receiving email async: " + message);
    try {
      Thread.sleep(5000);  // Simulating email reading time
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    System.out.println(name + " finished reading email: " + message);
  }
}

// Broker (Asynchronous)
class EmailDeliveryService {
  private Map<String, List<Customer>> customerGroups = new HashMap<>();
  private ExecutorService executor = Executors.newCachedThreadPool();

  public void subscribe(String eventType, Customer customer) {
    customerGroups.computeIfAbsent(eventType, k -> new ArrayList<>()).add(customer);
  }

  public void sendEmails(String eventType, String message) {
    List<Customer> customers = customerGroups.get(eventType);
    if (customers != null) {
      for (Customer customer : customers) {
        executor.submit(() -> customer.receiveEmail(message));
      }
    }
  }
  public void shutdown() { executor.shutdown(); }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    EmailDeliveryService emailService = new EmailDeliveryService();

    MarketingDepartment marketing = new MarketingDepartment(emailService, "ProductLaunch");

    Customer customer1 = new IndividualCustomer("Customer 1");
    Customer customer2 = new IndividualCustomer("Customer 2");

    emailService.subscribe("ProductLaunch", customer1);
    emailService.subscribe("ProductLaunch", customer2);

    marketing.launchCampaign("New Product");

    Customer customer3 = new IndividualCustomer("Customer 3");
    emailService.subscribe("ProductLaunch", customer3);

    marketing.launchCampaign("Update");

    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    emailService.shutdown();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Launching campaign: New Product
Launching campaign: Update
Customer 2 is receiving email async: New Product
Customer 3 is receiving email async: Update
Customer 2 is receiving email async: Update
Customer 1 is receiving email async: New Product
Customer 1 is receiving email async: Update
Customer 3 finished reading email: Update
Customer 1 finished reading email: Update
Customer 2 finished reading email: Update
Customer 2 finished reading email: New Product
Customer 1 finished reading email: New Product

상태 패턴 (State)

객체 상태별 행동을 인터페이스로 정의하여 공통 규약 및 확장성을 확보하고,
각 상태별 클래스가 상태에 따른 행동을 정의하며, 스스로 상태를 전환할 수 있는 패턴입니다.

상태 패턴을 사용하면 복잡한 조건문 없이 가독성 있는 코드를 작성할 수 있습니다.

상태 패턴 예시 1

// 상태 인터페이스 정의
public interface State {
  void open(Door door);
  void close(Door door);
}

// 문을 닫은 상태 클래스 정의
public class ClosedState implements State {
  @Override
  public void open(Door door) {
    // 문을 연 상태로 변경
    System.out.println("Door is now Open.");
    door.setState(new OpenState());
  }

  @Override
  public void close(Door door) {
    // 문을 닫은 상태에서 다시 닫을 수 없도록 처리
    System.out.println("Door is already Closed.");
  }
}

// 문을 연 상태 클래스 정의
public class OpenState implements State {
  @Override
  public void open(Door door) {
    // 문을 연 상태에서 다시 열 수 없도록 처리
    System.out.println("Door is already Open.");
  }

  @Override
  public void close(Door door) {
    // 문을 닫은 상태로 변경
    System.out.println("Door is now Closed.");
    door.setState(new ClosedState());
  }
}

// 문 클래스 정의
public class Door {
  private State state;
  public Door() {
    // 초기 상태 : 닫힌 상태
    this.state = new ClosedState();
  }

  // 내부 상태 클래스 변경 함수
  public void setState(State state) {
    this.state = state;
  }

  public void open() {
    // 현재 문 객체를 전달하여 현재 상태 클래스의 열기 함수 호출
    state.open(this);
  }

  public void close() {
    // 현재 문 객체를 전달하여 현재 상태 클래스의 닫기 함수 호출
    state.close(this);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Door door = new Door();
    
    door.open();
    door.open();
    door.close();
    door.close();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Door is now Open.
Door is already Open.
Door is now Closed.
Door is already Closed.

상태 패턴 예시 2

// 상태 인터페이스 정의
public interface State {
  void play(VideoPlayer player);
  void stop(VideoPlayer player);
}

// 정지된 상태 클래스 정의
public class StoppedState implements State {
  @Override
  public void play(VideoPlayer player) {
    // 재생중 상태로 변경
    System.out.println("Starting the video.");
    player.setState(new PlayingState());
  }

  @Override
  public void stop(VideoPlayer player) {
    // 정지된 상태에서 다시 정지할 수 없도록 처리
    System.out.println("Video is already stopped.");
  }
}

// 재생중 상태 클래스 정의
public class PlayingState implements State {
  @Override
  public void play(VideoPlayer player) {
    // 재생중 상태에서 다시 재생할 수 없도록 처리
    System.out.println("Video is already playing.");
  }

  @Override
  public void stop(VideoPlayer player) {
    // 정지된 상태로 변경
    System.out.println("Pausing the video.");
    player.setState(new PausedState());
  }
}

// 일시정지 상태 클래스 정의
public class PausedState implements State {
  @Override
  public void play(VideoPlayer player) {
    // 재생중 상태로 변경
    System.out.println("Resuming the video.");
    player.setState(new PlayingState());
  }

  @Override
  public void stop(VideoPlayer player) {
    // 정지된 상태로 변경
    System.out.println("Stopping the video.");
    player.setState(new StoppedState());
  }
}

// 비디오플레이어 클래스 정의
public class VideoPlayer {
  private State state;

  public VideoPlayer() {
    // 초기 상태 : 정지된 상태
    this.state = new StoppedState();
  }

  // 내부 상태 클래스 변경 함수
  public void setState(State state) {
    this.state = state;
  }

  public void play() {
    // 현재 비디오플레이어 객체를 전달하여 현재 상태 클래스의 재생 함수 호출
    state.play(this);
  }

  public void stop() {
    // 현재 비디오플레이어 객체를 전달하여 현재 상태 클래스의 정지 함수 호출
    state.stop(this);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    VideoPlayer player = new VideoPlayer();
    
    player.play();
    player.play();
    player.stop();
    player.play();
    player.stop();
    player.stop();
    player.stop();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Starting the video.
Video is already playing.
Pausing the video.
Resuming the video.
Pausing the video.
Stopping the video.
Video is already stopped.

상태 패턴이 적용되지 않은, 조건문 방식 예시

public class VideoPlayer {
  private String state;

  public VideoPlayer() {
    this.state = "Stopped";
  }

  public void play() {
    if (state.equals("Stopped")) {
      System.out.println("Starting the video.");
      state = "Playing";
    } else if (state.equals("Playing")) {
      System.out.println("Video is already playing.");
    } else if (state.equals("Paused")) {
      System.out.println("Resuming the video.");
      state = "Playing";
    }
  }

  public void stop() {
    if (state.equals("Playing")) {
      System.out.println("Pausing the video.");
      state = "Paused";
    } else if (state.equals("Paused")) {
      System.out.println("Stopping the video.");
      state = "Stopped";
    } else if (state.equals("Stopped")) {
      System.out.println("Video is already stopped.");
    }
  }

  public static void main(String[] args) {
    VideoPlayer player = new VideoPlayer();
    
    player.play();   // "Starting the video."
    player.play();   // "Video is already playing."
    player.stop();   // "Pausing the video."
    player.play();   // "Resuming the video."
    player.stop();   // "Pausing the video."
    player.stop();   // "Stopping the video."
    player.stop();   // "Video is already stopped."
  }
}

상태 패턴을 사용하지 않은 if 분기 처리는 상태 및 조건이 많아질수록 복잡해지고 실수의 여지가 높아집니다.

전략 패턴 (Strategy)

특정 인터페이스를 구현하는 여러 전략 클래스들을 두고, 런타임 시 필요에 따라 갈아끼우는 패턴입니다.

중간 다리인 Context 클래스는 실제 기능 로직을 구현하지 않고 인터페이스에만 의존합니다.
상위 모듈이 하위 모듈의 구현이 아니라 추상화에 의존해야 하는 DIP(의존 역전 원칙)을 충족하게 됩니다.

코드 변경 시 전략 구현체만 수정 및 추가하면 되어서, SOLID 원칙의 개방-폐쇄 원칙도 준수하는 코드입니다.

전략 패턴 예시 1 : 결제 방식

// 결제 전략 인터페이스
interface PaymentStrategy {
  // 결제 전략 함수 정의
  void pay(int amount);
}

// 결제 전략 클래스 1 : 신용카드
class CreditCardPayment implements PaymentStrategy {
  private String name;
  private String cardNumber;

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

  // 결제 전략 함수 구현
  @Override
  public void pay(int amount) {
    System.out.println(amount + " paid with credit card");
  }
}

// 결제 전략 클래스 2 : 페이팔
class PayPalPayment implements PaymentStrategy {
  private String email;

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

  // 결제 전략 함수 구현
  @Override
  public void pay(int amount) {
    System.out.println(amount + " paid using PayPal");
  }
}

// 중간 Context 클래스 : 쇼핑 카트
class ShoppingCart {
  // 결제 전략 인터페이스 필드
  private PaymentStrategy paymentStrategy;

  // 결제 전략 인터페이스 변경 함수 (파라미터로 인터페이스를 구현한 클래스가 올 수 있음)
  public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  // 쇼핑 카트 나갈 때 실행되는 함수
  public void checkout(int amount) {
    // 결제 전략에 따른 계산 기능 수행
    paymentStrategy.pay(amount);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Context 클래스 생성
    ShoppingCart cart = new ShoppingCart();

    // 결제 전략 클래스 교체
    cart.setPaymentStrategy(new CreditCardPayment("John Doe", "1234567890123456"));

    // 신용카드로 100원 결제
    cart.checkout(100);

    // 결제 전략 클래스 교체
    cart.setPaymentStrategy(new PayPalPayment("johndoe@example.com"));

    // 페이팔로 200원 결제
    cart.checkout(200);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

100 paid with credit card
200 paid using PayPal

전략 패턴 예시 2 : 문자열 인코딩 알고리즘

// 문자열 인코딩 전략 인터페이스
interface EncodingStrategy {
  // 인코딩 전략 함수 정의
  String encode(String data);
}

// 인코딩 전략 클래스 1 : 반복되는 문자 횟수 인코딩
class RunLengthEncoding implements EncodingStrategy {

  // 인코딩 전략 함수 구현
  @Override
  public String encode(String data) {
    StringBuilder encoded = new StringBuilder();
    int count = 1;
    for (int i = 1; i <= data.length(); i++) {
      if (i < data.length() && data.charAt(i) == data.charAt(i - 1)) {
        count++;
      } else {
        encoded.append(data.charAt(i - 1));
        encoded.append(count);
        count = 1;
      }
    }
    return encoded.toString();
  }
}

// 인코딩 전략 클래스 2 : 단순 치환 인코딩
class SimpleReplacementEncoding implements EncodingStrategy {

  // 인코딩 전략 함수 구현
  @Override
  public String encode(String data) {
    return data.replace("a", "1")
               .replace("e", "2")
               .replace("i", "3")
               .replace("o", "4")
               .replace("u", "5");
  }
}

// 중간 Context 클래스
class Encoder {
  private EncodingStrategy strategy;

  // 인코딩 전략 교체 함수
  public void setEncodingStrategy(EncodingStrategy strategy) {
    this.strategy = strategy;
  }

  public String encode(String data) {
    // 전략에 따른 인코딩 기능 수행
    return strategy.encode(data);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Context 클래스 생성
    Encoder encoder = new Encoder();

    String data = "aabcccccaaa";

    // 인코딩 전략 교체
    encoder.setEncodingStrategy(new RunLengthEncoding());

    // 문자열 인코딩 : 반복되는 문자 횟수 인코딩
    System.out.println("Run-Length Encoding : " + encoder.encode(data));

    // 인코딩 전략 교체
    encoder.setEncodingStrategy(new SimpleReplacementEncoding());

    // 문자열 인코딩 : 단순 치환 인코딩
    System.out.println("Simple Replacement Encoding : " + encoder.encode(data));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Run-Length Encoding : a2b1c5a3

Simple Replacement Encoding : 11bccccc111

템플릿 메서드 패턴 (Template Method)

전체 과정이 정해진 순서에 따라 실행되어야 하는 경우 사용되는 패턴입니다.

상위 추상 클래스에 전체 순서 템플릿 함수를 정의하고,
각 하위 클래스들에서 추상 메서드를 구현하여 세부 단계를 정의합니다.

템플릿 메서드 패턴 예시 1

// 상위 추상 클래스 : 음료 추상 클래스
abstract class Beverage {

  // 템플릿 함수 정의 (알고리즘 순서 고정)
  final void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  }

  // 공통 함수 구현 : 물 끓이기
  void boilWater() {
    System.out.println("Boiling water");
  }

  // 공통 함수 구현 : 음료를 컵에 따르기
  void pourInCup() {
    System.out.println("Pouring into cup");
  }

  // 음료 제조 함수 선언
  abstract void brew();

  // 첨가물 넣기 함수 선언
  abstract void addCondiments();
}

// 하위 클래스 1 : 티 클래스
class Tea extends Beverage {
  // 음료 제조 함수 구현
  void brew() {
    System.out.println("Steeping the tea");
  }

  // 첨가물 넣기 함수 구현
  void addCondiments() {
    System.out.println("Adding lemon");
  }
}

// 하위 클래스 2 : 커피 클래스
class Coffee extends Beverage {
  // 음료 제조 함수 구현
  void brew() {
    System.out.println("Dripping coffee through filter");
  }

  // 첨가물 넣기 함수 구현
  void addCondiments() {
    System.out.println("Adding sugar and milk");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 템플릿 추상 클래스에 하위클래스 객체 저장
    Beverage tea = new Tea();
    Beverage coffee = new Coffee();

    // 티 객체에서 템플릿 함수 호출
    tea.prepareRecipe();

    System.out.println();

    // 커피 객체에서 템플릿 함수 호출
    coffee.prepareRecipe();
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Boiling water
Steeping the tea
Pouring into cup
Adding lemon

Boiling water
Dripping coffee through filter
Pouring into cup
Adding sugar and milk

템플릿 메서드 패턴 예시 2

// 상위 추상 클래스 : 데이터 처리 클래스
abstract class DataProcessor {

  // 템플릿 함수 정의 (알고리즘 순서 고정)
  public final void process(String data) {
    // 파일 로드
    loadData(data);

    // 파일 유효성 확인
    if (isValidData(data)) {
      // 정상 파일 : 데이터 처리 후 저장
      processData(data);
      saveData(data);
    } else {
      // 비정상 파일 : 오류메시지 출력
      System.out.println("Data is invalid, processing aborted.");
    }
  }

  // 파일 데이터 로드 함수 선언
  protected abstract void loadData(String data);

  // 파일 유효성 확인 함수 선언
  protected abstract boolean isValidData(String data);

  // 데이터 처리 함수 선언
  protected abstract void processData(String data);

  // 데이터 저장 함수 선언
  protected abstract void saveData(String data);
}

// 하위 클래스 1 : CSV 데이터 처리 클래스
class CSVDataProcessor extends DataProcessor {
  // 파일 데이터 로드 함수 구현
  @Override
  protected void loadData(String data) {
    System.out.println("Loading data from CSV file: " + data);
  }

  // 파일 유효성 확인 함수 구현
  @Override
  protected boolean isValidData(String data) {
    return data != null && data.contains("CSV");
  }

  // 데이터 처리 함수 구현
  @Override
  protected void processData(String data) {
    System.out.println("Processing CSV data");
  }

  // 데이터 저장 함수 구현
  @Override
  protected void saveData(String data) {
    System.out.println("Saving CSV data to database");
  }
}

// 하위 클래스 2 : JSON 데이터 처리 클래스
class JSONDataProcessor extends DataProcessor {
  // 파일 데이터 로드 함수 구현
  @Override
  protected void loadData(String data) {
    System.out.println("Loading data from JSON file: " + data);
  }

  // 파일 유효성 확인 함수 구현
  @Override
  protected boolean isValidData(String data) {
    return data != null&& data.contains("JSON");
  }

  // 데이터 처리 함수 구현
  @Override
  protected void processData(String data) {
    System.out.println("Processing JSON data");
  }

  // 데이터 저장 함수 구현
  @Override
  protected void saveData(String data) {
    System.out.println("Saving JSON data to database");
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // 템플릿 추상 클래스에 하위클래스 객체 저장
    DataProcessor csvProcessor = new CSVDataProcessor();
    DataProcessor jsonProcessor = new JSONDataProcessor();

    // CSV 데이터 처리 객체에서 템플릿 함수 호출
    csvProcessor.process("CSV data");

    System.out.println();

    // JSON 데이터 처리 객체에서 템플릿 함수 호출
    jsonProcessor.process("XML data");
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Loading data from CSV file: CSV data
Processing CSV data
Saving CSV data to database

Loading data from JSON file: XML data
Data is invalid, processing aborted.

방문자 패턴 (Visitor)

방문자 패턴 예시 1

// Element interface
interface Shape {
  void accept(Visitor visitor);
}

// Concrete elements
class Circle implements Shape {
  double radius;

  Circle(double radius) {
    this.radius = radius;
  }

  public double getRadius() {
    return radius;
  }

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

class Rectangle implements Shape {
  double width, height;

  Rectangle(double width, double height) {
    this.width = width;
    this.height = height;
  }

  public double getWidth() {
    return width;
  }

  public double getHeight() {
    return height;
  }

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

// Visitor interface
interface Visitor {
  void visit(Circle circle);
  void visit(Rectangle rectangle);
}

// Concrete Visitor
class AreaVisitor implements Visitor {
  @Override
  public void visit(Circle circle) {
    double area = Math.PI * circle.getRadius() * circle.getRadius();
    System.out.println("Circle Area: " + area);
  }

  @Override
  public void visit(Rectangle rectangle) {
    double area = rectangle.getWidth() * rectangle.getHeight();
    System.out.println("Rectangle Area: " + area);
  }
}

class PerimeterVisitor implements Visitor {
  @Override
  public void visit(Circle circle) {
    double perimeter = 2 * Math.PI * circle.getRadius();
    System.out.println("Circle Perimeter: " + perimeter);
  }

  @Override
  public void visit(Rectangle rectangle) {
    double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight());
    System.out.println("Rectangle Perimeter: " + perimeter);
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Shape circle = new Circle(5);
    Shape rectangle = new Rectangle(4, 6);

    Visitor areaVisitor = new AreaVisitor();
    Visitor perimeterVisitor = new PerimeterVisitor();

    System.out.println("Calculating Area:");
    circle.accept(areaVisitor);
    rectangle.accept(areaVisitor);

    System.out.println("\nCalculating Perimeter:");
    circle.accept(perimeterVisitor);
    rectangle.accept(perimeterVisitor);
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Calculating Area:
Circle Area: 78.53981633974483
Rectangle Area: 24.0

Calculating Perimeter:
Circle Perimeter: 31.41592653589793
Rectangle Perimeter: 20.0

방문자 패턴 예시 2

// FileSystemElement interface
interface FileSystemElement {
  void accept(Visitor visitor);
}

// File class
class File implements FileSystemElement {
  private String name;
  private long size;

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

  public String getName() { return name; }
  public long getSize() { return size; }

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

// Directory class
class Directory implements FileSystemElement {
  private String name;
  private List<FileSystemElement> elements;

  public Directory(String name) {
    this.name = name;
    this.elements = new ArrayList<>();
  }

  public String getName() { return name; }

  public void addElement(FileSystemElement element) {
    elements.add(element);
  }
  public List<FileSystemElement> getElements() {
    return elements;
  }

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

// Visitor interface
interface Visitor {
  void visit(File file);
  void visit(Directory directory);
}

// Concrete visitors
class SizeCalculatorVisitor implements Visitor {
  private long totalSize = 0;

  @Override
  public void visit(File file) {
    totalSize += file.getSize();
  }

  @Override
  public void visit(Directory directory) {
    for (FileSystemElement element : directory.getElements()) {
      element.accept(this);
    }
  }

  public long getTotalSize() {
    return totalSize;
  }
}

class FileSearchVisitor implements Visitor {
  private String searchFileName;
  private File foundFile;

  public FileSearchVisitor(String searchFileName) {
    this.searchFileName = searchFileName;
  }

  @Override
  public void visit(File file) {
    if (file.getName().equals(searchFileName)) {
      foundFile = file;
    }
  }

  @Override
  public void visit(Directory directory) {
    for (FileSystemElement element : directory.getElements()) {
      element.accept(this);
    }
  }

  public File getFoundFile() {
    return foundFile;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    // Create files
    File file1 = new File("file1.txt", 100);
    File file2 = new File("file2.txt", 200);
    File file3 = new File("file3.txt", 300);

    // Create directories and add files to them
    Directory dir1 = new Directory("Folder1");
    dir1.addElement(file1);
    dir1.addElement(file2);

    Directory dir2 = new Directory("Folder2");
    dir2.addElement(file3);

    Directory rootDir = new Directory("Root");
    rootDir.addElement(dir1);
    rootDir.addElement(dir2);

    SizeCalculatorVisitor sizeVisitor = new SizeCalculatorVisitor();
    rootDir.accept(sizeVisitor);
    System.out.println("Total size of file system: " + sizeVisitor.getTotalSize() + " bytes");

    FileSearchVisitor searchVisitor = new FileSearchVisitor("file3.txt");
    rootDir.accept(searchVisitor);
    File foundFile = searchVisitor.getFoundFile();
    if (foundFile != null) {
      System.out.println("File found: " + foundFile.getName() + ", Size: " + foundFile.getSize() + " bytes");
    } else {
      System.out.println("File not found.");
    }
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Total size of file system: 600 bytes
File found: file3.txt, Size: 300 bytes

명세 패턴 (Specification)

명세 패턴 예시 1

public interface Specification {
  boolean isSatisfiedBy(int number);

  default Specification and(Specification other) {
    return number -> this.isSatisfiedBy(number) && other.isSatisfiedBy(number);
  }
}

class EvenSpecification implements Specification {
  @Override
  public boolean isSatisfiedBy(int number) {
    return number % 2 == 0;
  }
}

class RangeSpecification implements Specification {
  private int min;
  private int max;

  public RangeSpecification(int min, int max) {
    this.min = min;
    this.max = max;
  }

  @Override
  public boolean isSatisfiedBy(int number) {
    return number >= min && number <= max;
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    Specification evenSpec = new EvenSpecification();
    Specification rangeSpec = new RangeSpecification(10, 20);

    Specification evenAndInRangeSpec = evenSpec.and(rangeSpec);

    int number = 24;

    System.out.println("Even: " + evenSpec.isSatisfiedBy(number));
        
    System.out.println("In range 10-20: " + rangeSpec.isSatisfiedBy(number));
        
    System.out.println("Even and in range 10-20: " + evenAndInRangeSpec.isSatisfiedBy(number));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Even: true
In range 10-20: false
Even and in range 10-20: false

명세 패턴 예시 2

public class Product {
  private String name;
  private String category;
  private int price;
  private int stock;

  public Product(String name, String category, int price, int stock) {
    this.name = name;
    this.category = category;
    this.price = price;
    this.stock = stock;
  }

  public String getName() { return name; }
  public String getCategory() { return category; }
  public double getPrice() { return price; }
  public int getStock() { return stock; }
}

public class PriceSpec implements Specification {
  private int maxPrice;

  public PriceSpec(int maxPrice) {
    this.maxPrice = maxPrice;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return item.getPrice() <= maxPrice;
  }
}

public class InStockSpec implements Specification {
  @Override
  public boolean isSatisfiedBy(Product item) {
    return item.getStock() > 0;
  }
}

public class AndSpec implements Specification {
  private Specification spec1;
  private Specification spec2;

  public AndSpec(Specification spec1, Specification spec2) {
    this.spec1 = spec1; this.spec2 = spec2;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return spec1.isSatisfiedBy(item) && spec2.isSatisfiedBy(item);
  }
}

public class OrSpec implements Specification {
  private Specification spec1;
  private Specification spec2;

  public OrSpec(Specification spec1, Specification spec2) {
    this.spec1 = spec1; this.spec2 = spec2;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return spec1.isSatisfiedBy(item) || spec2.isSatisfiedBy(item);
  }
}

public class NotSpec implements Specification {
  private Specification spec;

  public NotSpec(Specification spec) {
    this.spec = spec;
  }

  @Override
  public boolean isSatisfiedBy(Product item) {
    return !spec.isSatisfiedBy(item);
  }
}

public class ProductFilter {
  public static List<Product> filter(List<Product> items, Specification spec) {
    return items.stream()
                .filter(spec::isSatisfiedBy)
                .collect(Collectors.toList());
  }

  public static void printProducts(List<Product> products) {
    products.forEach(
      p -> System.out.println(p.getName() + " - " + p.getCategory() + " - $" + p.getPrice() + " - Stock: " + p.getStock())
    );
  }
}

// 클라이언트 (호출부)
public class Main {
  public static void main(String[] args) {
    List<Product> products = Arrays.asList(
      new Product("Laptop", "Electronics", 1200, 5),
      new Product("Smartphone", "Electronics", 800, 0),
      new Product("Headphones", "Electronics", 200, 10),
      new Product("Book", "Literature", 20, 50)
    );

    Specification electronicsSpec = new CategorySpec("Electronics");
    Specification inStockSpec = new InStockSpec();
    Specification expensiveSpec = new PriceSpec(500);

    Specification electronicInStock = new AndSpec(electronicsSpec, inStockSpec);
    Specification electronicOrInStock = new OrSpec(electronicsSpec, inStockSpec);
    Specification notExpensive = new NotSpec(expensiveSpec);

    System.out.println("Electronics in stock:");
    ProductFilter.printProducts(ProductFilter.filter(products, electronicInStock));

    System.out.println("\nElectronics or items in stock:");
    ProductFilter.printProducts(ProductFilter.filter(products, electronicOrInStock));

    System.out.println("\nNot expensive items:");
    ProductFilter.printProducts(ProductFilter.filter(products, notExpensive));
  }
}

위 코드의 실행 결과는 아래와 같습니다.

Electronics in stock:
Laptop - Electronics - $1200.0 - Stock: 5
Headphones - Electronics - $200.0 - Stock: 10

Electronics or items in stock:
Laptop - Electronics - $1200.0 - Stock: 5
Smartphone - Electronics - $800.0 - Stock: 0
Headphones - Electronics - $200.0 - Stock: 10
Book - Literature - $20.0 - Stock: 50

Not expensive items:
Laptop - Electronics - $1200.0 - Stock: 5
Smartphone - Electronics - $800.0 - Stock: 0