본문 바로가기

기술 블로그 (Tech Blog)/Dev Notes

4대천왕👑이 정리한 객체 정의서(Gof 디자인패턴)

 

 


 

java의 Iterator 를 공부하는데 무릎을 탁치게 되었다.

그런데... Iterator 패턴이 있다고 한다..

있었던가.. 그럼 간단하게 패턴을 정리해볼까?

 

 

사담

더보기

 

확실히 책으로 읽을때보다 gpt를 사용해서 디자인패턴으로 정리해서 코드로 이해하는게

와 닿는다.... 개발자에게는 gpt는 선생님...

 

책으로 읽다보면 깊게 깊게 파다보니

요약이 잘 되지 않는다.

 

그렇게 저는 '또 길을 잃었다.(딴딴딴)👠👠👠'

 

 

 

 


이미지 출처: https://www.youtube.com/watch?v=An7kqZ5D7j8&list=PLe6NQuuFBu7FhPfxkjDd2cWnTy2y_w_jZ

 

Gof 뭐여?

-> 4대천왕👑(GoF)이 정리한 패턴!

 

Gang of Four(4명의 갱)

어머 멋져☺️ 오빠만 믿고 있었다구!

객체지향 설계 문제를 23개 디자인 패턴을 정의

크게 3개로 나눈다.

 

1. 생성 패턴 : 객체 생성

2. 구조 패턴: 객체 간 관계

3. 행위 패턴: 객체 간 데이터 주고 받는 방법, 책임 분배

 

 

 

 

 

 

 


객체 생성할 때 어떻게 만들까?

1. 생성 패턴

 

싱글톤(Singleton)

원리

  • 특정 클래스의 인스턴스를 하나만 생성하고, 어디서든 이를 공유하도록 보장.
  • 전역 상태를 관리하거나 공통 리소스를 사용할 때 유용.

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return instance; }
}

 

sigleton example code

더보기
class Singleton {
    private static Singleton instance; // 단일 인스턴스 저장
    private Singleton() {}            // 생성자를 private으로 막음

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("Singleton Instance");
    }
}

public class SingletonExample {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        s1.showMessage();
        System.out.println("s1과 s2가 같은 인스턴스인가? " + (s1 == s2));
    }
}

//결과: 
//Singleton Instance
//s1과 s2가 같은 인스턴스인가? true

 

 

 

팩토리 메서드 (Factory Method)

원리

  • 객체 생성을 클라이언트 코드에서 분리하여 객체 생성 책임을 팩토리 클래스에 위임.
  • 객체 생성을 캡슐화하여 클라이언트는 구체적인 클래스를 몰라도 객체를 생성할 수 있음.

-> 같은 interface를 가진 객체 생성을 담당하는 클래스를 따로 만듦

ShapeFactory class를 따로 만들어서

main에서는 원을 만들지, 사각을 만들지만 정해주면

ShapeFactory 내부에서 잘 정리한 규칙에 따라 객체를 생성함.

 

 

interface Product { void use(); }
class ProductA implements Product { public void use() { System.out.println("ProductA"); } }
class Factory {
    public Product createProduct() { return new ProductA(); }
}

 

factory method example code : Shape, Circle, Square

더보기
// 제품 인터페이스
interface Shape {
    void draw();
}

// 구체적인 제품 클래스들
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}

class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Square");
    }
}

// 팩토리 클래스
class ShapeFactory {
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }
}

public class FactoryExample {
    public static void main(String[] args) {
        ShapeFactory shapeFactory = new ShapeFactory();

        Shape shape1 = shapeFactory.getShape("CIRCLE");
        shape1.draw();

        Shape shape2 = shapeFactory.getShape("SQUARE");
        shape2.draw();
    }
}



 

추상 팩토리 (Abstract Factory)

객체를 그룹으로 생성

 

 

abstract factory example code:

Button, CheckBox, WindowButton, MacOSButton, GUIFactory, MacFactory, WindowFactory, Application

더보기
// 1. 공통 인터페이스
// Abstract Product: Button
interface Button {
    void render();
}

// Abstract Product: Checkbox
interface Checkbox {
    void render();
}

// 2. 운영체제에 따라 Button과 Checkbox를 구현합니다.
// Concrete Product: Windows Button
class WindowsButton implements Button {
    public void render() {
        System.out.println("Rendering Windows Button");
    }
}

// Concrete Product: MacOS Button
class MacOSButton implements Button {
    public void render() {
        System.out.println("Rendering MacOS Button");
    }
}

// Concrete Product: Windows Checkbox
class WindowsCheckbox implements Checkbox {
    public void render() {
        System.out.println("Rendering Windows Checkbox");
    }
}

// Concrete Product: MacOS Checkbox
class MacOSCheckbox implements Checkbox {
    public void render() {
        System.out.println("Rendering MacOS Checkbox");
    }
}

// 3. 팩토리 인터페이스
// Abstract Factory
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 4. 운영체제에 다라 팩토리 구현
// Concrete Factory: Windows Factory
class WindowsFactory implements GUIFactory {
    public Button createButton() {
        return new WindowsButton();
    }

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

// Concrete Factory: MacOS Factory
class MacOSFactory implements GUIFactory {
    public Button createButton() {
        return new MacOSButton();
    }

    public Checkbox createCheckbox() {
        return new MacOSCheckbox();
    }
}

//5. 고객은 구체에 의존하지 않는다
// Client
public class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        // 팩토리를 통해 구체적인 제품 생성
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

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

// 6. 팩토리 선택
public class Main {
    public static void main(String[] args) {
        GUIFactory factory;

        // 환경 변수나 사용자 입력으로 팩토리를 결정
        String os = System.getProperty("os.name").toLowerCase();

        if (os.contains("win")) {
            factory = new WindowsFactory();
        } else if (os.contains("mac")) {
            factory = new MacOSFactory();
        } else {
            throw new UnsupportedOperationException("Unsupported OS: " + os);
        }

        // Application 초기화 및 실행
        Application app = new Application(factory);
        app.render();
    }
}

//7. 결과
Windows 환경:
Rendering Windows Button
Rendering Windows Checkbox

MacOS 환경:
Rendering MacOS Button
Rendering MacOS Checkbox

 

 

빌더 (Builder)

@Builder 를 적용하면 생기는 코드와 같다.

 

장점: 

생성자처럼 매개변수의 순서를 신경쓸 필요가 없다.

매개변수를 다르게하여, 생성자 오버로딩이 필요하지 않다.

builder instance의 값을 변경하지 않도록 만들 수 있다.

build()하면 new 로 새로운 instance로 전달할 수 있다.

class Product {
    String partA, partB;
    static class Builder {
        Product product = new Product();
        Builder setPartA(String a) { product.partA = a; return this; }
        Builder setPartB(String b) { product.partB = b; return this; }
        Product build() { return product; }
    }
}

 

 

프로토타입 (Prototype)

객체를 복제하여 생성

class Prototype implements Cloneable {
    public Prototype clone() throws CloneNotSupportedException {
        return (Prototype) super.clone();
    }
}

 

 


객체 간 관계를 어떻게 중재하지? 변경하지?

2. 구조 패턴

어댑터(Adapter)

원리

  • 기존 클래스의 인터페이스를 클라이언트에서 원하는 다른 인터페이스로 변환.
  • 호환되지 않는 인터페이스를 연결하여 협업을 가능하게 만듦.
class OldSystem { void oldMethod() { System.out.println("Old Method"); } }
interface NewSystem { void newMethod(); }
class Adapter implements NewSystem {
    OldSystem oldSystem = new OldSystem();
    public void newMethod() { oldSystem.oldMethod(); }
}

 

 

adapter example code: OldPrinter, Printer, PrinterAdapter,

더보기
// 기존 클래스
class OldPrinter {
    public void printOldMessage() {
        System.out.println("Printing using the old printer.");
    }
}

// 새로운 인터페이스
interface Printer {
    void print();
}

// 어댑터 클래스
class PrinterAdapter implements Printer {
    private OldPrinter oldPrinter;

    public PrinterAdapter(OldPrinter oldPrinter) {
        this.oldPrinter = oldPrinter;
    }

    @Override
    public void print() {
        oldPrinter.printOldMessage(); // 기존 클래스 호출
    }
}

public class AdapterExample {
    public static void main(String[] args) {
        OldPrinter oldPrinter = new OldPrinter();
        Printer adapter = new PrinterAdapter(oldPrinter);

        adapter.print(); // 새 인터페이스를 사용해 호출
    }
}

결과:
Printing using the old printer.



 

브리지 (Bridge)

구현부와 추상부 분리

interface Device { void turnOn(); }
class TV implements Device { public void turnOn() { System.out.println("TV On"); } }
class Remote {
    Device device;
    Remote(Device device) { this.device = device; }
    void toggle() { device.turnOn(); }
}

 

 

 

컴포지트 (Composite)

트리구조 표현

 

Interface

Leaf : 가장 마지막 단위

Composite: 부모

 

interface Component { void show(); }
class Leaf implements Component { public void show() { System.out.println("Leaf"); } }
class Composite implements Component {
    List<Component> children = new ArrayList<>();
    public void show() { children.forEach(Component::show); }
}

 

composite example code:

FileSystemComponent, File, Directory

더보기
// 1. 인터페이스
import java.util.List;

interface FileSystemComponent {
    void display(String indent); // 계층 구조를 표시하는 메서드
}

// 2. Leaf
class File implements FileSystemComponent {
    private String name;

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

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

// 3. Parent
import java.util.ArrayList;
import java.util.List;

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

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

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

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

    @Override
    public void display(String indent) {
        System.out.println(indent + "Directory: " + name);
        for (FileSystemComponent child : children) {
            child.display(indent + "  "); // 들여쓰기 추가
        }
    }
}

// 4. 사용
public class Main {
    public static void main(String[] args) {
        // Leaf 객체 생성 (파일)
        File file1 = new File("file1.txt");
        File file2 = new File("file2.txt");
        File file3 = new File("file3.txt");

        // Composite 객체 생성 (디렉토리)
        Directory rootDirectory = new Directory("root");
        Directory subDirectory1 = new Directory("subdir1");
        Directory subDirectory2 = new Directory("subdir2");

        // 트리 구조 구성
        rootDirectory.add(file1);
        rootDirectory.add(subDirectory1);
        subDirectory1.add(file2);
        subDirectory1.add(subDirectory2);
        subDirectory2.add(file3);

        // 트리 구조 출력
        rootDirectory.display("");
    }
}
// 5. 결과
Directory: root
  File: file1.txt
  Directory: subdir1
    File: file2.txt
    Directory: subdir2
      File: file3.txt

 

 

 

 

퍼사드 (Facade)

복잡한 시스템에 단순한 인터페이스 제공

데코레이터보다 더 간단하게 기능 추가 가능?

class Subsystem { void operation() { System.out.println("Subsystem Operation"); } }
class Facade {
    Subsystem subsystem = new Subsystem();
    void simpleOperation() { subsystem.operation(); }
}

 

 

플라이웨이트 (Flyweight)

공유 가능한 객체를 재사용

 

map을 사용해서 객체를 생성하지 못하게 하네

class FlyweightFactory {
    private static final Map<String, Flyweight> cache = new HashMap<>();
    public static Flyweight get(String key) {
        return cache.computeIfAbsent(key, k -> new Flyweight(key));
    }
}
class Flyweight { String data; Flyweight(String data) { this.data = data; } }

 

 

프록시 (Proxy)

template method, strategy pattern은 결국 코드를 많이 수정해야한다.

원본을 수정하지 않고 어떻게 개선할 수 있을까?

interface Subject { void request(); }
class RealSubject implements Service { public void request() { System.out.println("Real Service"); } }
class Proxy implements Service {
    Subject subject = new RealSubject();
    public void request() { System.out.println("Proxy Access"); subject.request(); }
}

 

RealSubject 를 직접 호출하지 않고,

Proxy 클래스의 request()를 호출하면 RealSubject의 request()를 호출한다.

 

 

접근제어를 위한 대리자

대리자는 요청온 것을 감시, 수정해서 목적지에 전달 가능하다.

프록시의 주요 기능

프록시를 통해서 있는 일은 크게 2가지로 구분할 있다.

 

  • 접근 제어
    • 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    • 예) 요청 값이나, 응답 값을 중간에 변형한다.
    • 예) 실행 시간을 측정해서 추가 로그를 남긴다.

장점

  • client 코드의 변경 없이 자유롭게 프록시를 넣고   있다. 실제 client 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
  • 객체 생성과 초기화 과정을 효율적으로 관리.
  • 접근 제어 및 추가 기능(로깅, 캐싱 등) 구현 가능.
  • 클라이언트 코드와 실제 객체의 구체적인 구현을 분리.

단점

  • 설계가 복잡해질 수 있음.
  • 프록시를 추가하면 객체 호출의 간접적인 오버헤드가 발생.

 

프록시 패턴의 활용 사례

  • 가상 프록시 (Virtual Proxy):
    • 리소스가 큰 객체를 지연 초기화하거나, 필요할 때만 로드하는 데 사용.
    • 예: 대형 이미지, 동영상, 데이터베이스 연결.
  • 보호 프록시 (Protection Proxy):
    • 접근 제어를 위해 사용.
    • 예: 권한에 따라 객체 접근을 제한.
  • 원격 프록시 (Remote Proxy):
    • 원격 객체(네트워크 상의 객체)를 로컬에서 대리하여 호출.
    • 예: RMI(Remote Method Invocation).
  • 스마트 프록시 (Smart Proxy):
    • 추가 작업(로깅, 캐싱 등)을 수행.
    • 예: 메서드 호출 로깅, 트랜잭션 관리.

 

 

 

proxy example code: cache 적용

더보기

proxy 도입 전 - cache 적용 안됨

package hello.proxy.pureproxy.proxy.code;

public interface Subject {
    String operation();
}


package hello.proxy.pureproxy.proxy.code;

import lombok.extern.slf4j.Slf4j;
import javax.security.auth.Subject;

@Slf4j
public class RealSubject implements Subject {
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

package hello.proxy.pureproxy.proxy.code;

import javax.security.auth.Subject;

public class ProxyPatternClient {
    private Subject subject;
    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }
    public void execute() {
        subject.operation();
    }
}

package hello.proxy.pureproxy.proxy;

import hello.proxy.pureproxy.proxy.code.ProxyPatternClient;
import hello.proxy.pureproxy.proxy.code.RealSubject;
import org.junit.jupiter.api.Test;

public class ProxyPatternTest {
    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);
        client.execute();
        client.execute();
        client.execute();
    }
}
// 결과
RealSubject - 실제 객체 호출
RealSubject - 실제 객체 호출
RealSubject - 실제 객체 호출

 실제 객체를 3번 호출한다.

 

 

proxy 도입 후 - cache 적용

package hello.proxy.pureproxy.proxy.code;

import lombok.extern.slf4j.Slf4j;
import javax.security.auth.Subject;

@Slf4j
public class CacheProxy implements Subject { // 캐시 구체 class

    private Subject target;// 기존에는 애만 있었음
    private String cacheValue;// 캐시 값

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("프록시 호출");
        if (cacheValue == null) {// 없으면 진짜 객체를 호출해, 이미 있으면 캐시값 쓰렴
            cacheValue = target.operation();
        }
        return cacheValue;
    }
}

package hello.proxy.pureproxy.proxy;

import hello.proxy.pureproxy.proxy.code.CacheProxy;
import hello.proxy.pureproxy.proxy.code.ProxyPatternClient;
import hello.proxy.pureproxy.proxy.code.RealSubject;
import hello.proxy.pureproxy.proxy.code.Subject;
import org.junit.jupiter.api.Test;

public class ProxyPatternTest {
    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);
        client.execute();
        client.execute();
        client.execute();
    }

    @Test
    void cacheProxyTest() {
        Subject realSubject = new RealSubject();
        Subject cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
        client.execute();
        client.execute();
        client.execute();
    }
}
// 결과
CacheProxy - 프록시 호출
RealSubject - 실제 객체 호출
CacheProxy - 프록시 호출
CacheProxy - 프록시 호출

처음 null일때만 실제 객체를 호출하고 나머지는 캐시에 있는 값을 가져다가 쓴다.

 

 

proxy example code: Image, RealImage, ProxyImage

더보기
// 1. 인터페이스
interface Image {
    void display(); // 이미지를 출력하는 메서드
}

// 2. 작업할 객체
// RealSubject: 실제 객체
class RealImage implements Image {
    private String fileName;//공통

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(); // 이미지 로드
    }

    private void loadFromDisk() {
        System.out.println("Loading image from disk: " + fileName);
    }

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

// 3. 대리자
// Proxy: 대리 객체
class ProxyImage implements Image {
    private String fileName;//공통
    private RealImage realImage;//프록시에서 추가

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

    @Override
    public void display() {
        // 실제 객체를 필요한 시점에 생성
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}

// 4. 사용
public class Main {
    public static void main(String[] args) {
        // ProxyImage 객체 생성
        Image image1 = new ProxyImage("photo1.jpg");
        Image image2 = new ProxyImage("photo2.jpg");

        // 이미지 로드 없이 호출
        System.out.println("Before displaying images...");

        // 첫 번째 이미지를 표시 (이 시점에 실제 객체 생성)
        image1.display();
        System.out.println();

        // 두 번째 이미지를 표시 (이 시점에 실제 객체 생성)
        image2.display();
        System.out.println();

        // 다시 첫 번째 이미지를 표시 (이미 생성된 객체 사용)
        image1.display();
    }
}

// 5. 결과
Before displaying images...
Loading image from disk: photo1.jpg
Displaying image: photo1.jpg

Loading image from disk: photo2.jpg
Displaying image: photo2.jpg

Displaying image: photo1.jpg

 

설명

  1. 지연 초기화:
    • ProxyImage는 실제 객체(RealImage)를 즉시 생성하지 않고, display() 메서드가 호출될 때 생성합니다.
    • 불필요한 리소스 사용을 방지합니다.
  2. 프록시와 실제 객체의 구분:
    • 클라이언트는 ProxyImage와 RealImage를 구분하지 않고 동일한 인터페이스(Image)를 사용합니다.
    • 이는 클라이언트 코드와 실제 구현의 분리를 가능하게 합니다.
  3. 캐싱:
    • 이미 생성된 객체는 재사용됩니다.

 

 

데코레이터 (Decorator)

객체에 동적으로 기능을 추가, 추가, 추가..

 

의도

  • 프록시 패턴의 의도: 다른 개체에 대한 **접근을 제어**하기 위해 대리자를 제공
  • 데코레이터 패턴의 의도: **객체에 추가 책임(기능)을 동적으로 추가**하고, 기능 확장을 위한 유연한 대안 제공

예시

  • 요청 값이나, 응답 값을 중간에 변형
  • 실행 시간을 측정해서 추가 로그를 남긴다.

 

 

decorator example code: Component, ConcreateComponent, Decorator

더보기
// Component 인터페이스
interface Component {
    String operation();
}

// Concrete Component
class ConcreteComponent implements Component {
    public String operation() {
        return "Base Component";
    }
}

// Decorator
abstract class Decorator implements Component {
    protected Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    public String operation() {
        return component.operation();
    }
}

// Concrete Decorators
class DecoratorA extends Decorator {
    public DecoratorA(Component component) {
        super(component);
    }

    public String operation() {
        return super.operation() + " + DecoratorA";
    }
}

class DecoratorB extends Decorator {
    public DecoratorB(Component component) {
        super(component);
    }

    public String operation() {
        return super.operation() + " + DecoratorB";
    }
}

public class Main {
    public static void main(String[] args) {
        Component component = new ConcreteComponent();
        Component decorated = new DecoratorA(new DecoratorB(component));

        System.out.println(decorated.operation());
    }
}

 

 


객체가 변경됐네? 여러 객체한테 데이터 변경하라고 알리자!

3. 행위 패턴

 

옵저버(Observer)

원리

  • 객체의 상태가 변경되면 이를 의존하고 있는 다른 객체(옵저버)들에게 자동으로 알림.
  • 이벤트 기반 시스템에서 자주 사용.

 

 

observer example code: Observer, Subject, ConcreateObserver

더보기
import java.util.ArrayList;
import java.util.List;

// 옵저버 인터페이스
interface Observer {
    void update(String message);
}

// 주제 클래스(Subject)
class Subject {
    private List<Observer> observers = new ArrayList<>();

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

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

    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

// 구체적인 옵저버 클래스
class ConcreteObserver implements Observer {
    private String name;

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

    @Override
    public void update(String message) {
        System.out.println("Observer " + name + " received message: " + message);
    }
}

public class ObserverExample {
    public static void main(String[] args) {
        Subject subject = new Subject();

        Observer observer1 = new ConcreteObserver("A");
        Observer observer2 = new ConcreteObserver("B");

        subject.attach(observer1);
        subject.attach(observer2);

        subject.notifyObservers("State has changed!");
    }
}

결과:
Observer A received message: State has changed!
Observer B received message: State has changed!

 

 

 

 

책임 연쇄 (Chain of Responsibility)

요청을 여러 객체가 처리

 

abstract class Handler {
    Handler next;
    void setNext(Handler next) { this.next = next; }
    abstract void handle(String request);
}
class ConcreteHandler extends Handler {
    void handle(String request) { System.out.println("Handled: " + request); }
}

 

 

 

커맨드 (Command)

요청을 캡슐화

 

interface Command { void execute(); }
class LightOnCommand implements Command {
    public void execute() { System.out.println("Light On"); }
}

 

 

 

인터프리터 (Interpreter)

해석자, 변역자

interface Expression { boolean interpret(String context); }
class TerminalExpression implements Expression {
    private String data;
    TerminalExpression(String data) { this.data = data; }
    public boolean interpret(String context) { return context.contains(data); }
}

 

 

 

반복자 (Iterator)

컬렉션 순회

interface Iterator { boolean hasNext(); Object next(); }
class ConcreteIterator implements Iterator {
    int index = 0; String[] data = {"A", "B"};
    public boolean hasNext() { return index < data.length; }
    public Object next() { return data[index++]; }
}

 

Iterator 인터페이스는 hasNext(), Object next() 을 무조건 구현하라고 한다.

ConcreteIterator 구현 클래스는  hasNext(), Object next() 를 구현해놨다.

 

main에서 ConcreteIterator 객체는 hasNext(), next() 함수로 순회가 가능하다.

ConcreteIterator의 자료구조는 문자 배열이다.

 

Iterable 인터페이스로 인해 Collection의 하위인 List, Set, Queue가 같은 함수 hasNext(), next()로 호출이 가능해진다

 

 

 

 

중재자 (Mediator)

객체 간 통신을 중재한다

interface Mediator { void notify(String event); }
class ConcreteMediator implements Mediator {
    public void notify(String event) { System.out.println("Event: " + event); }
}

 

 

 

상태 (State)

상태에 따라 행동을 변경한다

interface State { void handle(); }
class ConcreteState implements State {
    public void handle() { System.out.println("Handling State"); }
}

 

state example code: PaymentState, PendingState, ProcessingState, CompleteState, FailedState, PaymentContext

더보기
// State 인터페이스
public interface PaymentState {
    void processPayment(PaymentContext context);
    void cancelPayment(PaymentContext context);
}

// ConcreteState 1: 대기 상태
public class PendingState implements PaymentState {

    @Override
    public void processPayment(PaymentContext context) {
        System.out.println("결제를 진행 중입니다...");
        context.setState(new ProcessingState()); // 상태 전환
    }

    @Override
    public void cancelPayment(PaymentContext context) {
        System.out.println("대기 상태에서 결제가 취소되었습니다.");
        context.setState(new FailedState()); // 상태 전환
    }
}

// ConcreteState 2: 진행 상태
public class ProcessingState implements PaymentState {

    @Override
    public void processPayment(PaymentContext context) {
        System.out.println("결제가 이미 진행 중입니다. 완료를 기다리세요.");
    }

    @Override
    public void cancelPayment(PaymentContext context) {
        System.out.println("진행 상태에서 결제가 취소되었습니다.");
        context.setState(new FailedState()); // 상태 전환
    }
}

// ConcreteState 3: 완료 상태
public class CompletedState implements PaymentState {

    @Override
    public void processPayment(PaymentContext context) {
        System.out.println("결제가 이미 완료되었습니다.");
    }

    @Override
    public void cancelPayment(PaymentContext context) {
        System.out.println("완료된 결제는 취소할 수 없습니다.");
    }
}

// ConcreteState 4: 실패 상태
public class FailedState implements PaymentState {

    @Override
    public void processPayment(PaymentContext context) {
        System.out.println("결제가 실패했습니다. 다시 시도하세요.");
        context.setState(new PendingState()); // 상태 전환
    }

    @Override
    public void cancelPayment(PaymentContext context) {
        System.out.println("결제가 이미 실패한 상태입니다.");
    }
}

// Context 클래스
public class PaymentContext {
    private PaymentState state;

    public PaymentContext() {
        // 초기 상태 설정 (대기 상태)
        this.state = new PendingState();
    }

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

    public void processPayment() {
        state.processPayment(this);
    }

    public void cancelPayment() {
        state.cancelPayment(this);
    }
}

// 테스트 클래스
public class Main {
    public static void main(String[] args) {
        PaymentContext payment = new PaymentContext();

        // 결제 진행
        payment.processPayment(); // "결제를 진행 중입니다..."
        payment.processPayment(); // "결제가 이미 진행 중입니다."

        // 결제 취소
        payment.cancelPayment(); // "진행 상태에서 결제가 취소되었습니다."

        // 결제 재시도
        payment.processPayment(); // "결제를 진행 중입니다..."
        payment.processPayment(); // "결제가 이미 진행 중입니다."
    }
}

 

 

 

 

템플릿 메서드 (Template Method)

 

변하는 것, 변하지 않는 것 분리

  • 변하는 것: 핵심 기능
  • 변하지 않는 것: 템플릿

핵심기능 vs 부가 기능

  • 핵심 기능: 해당 객체가 제공하는 고유 기능.
  • 부가 기능: 핵심 기능을 보조하기 위해 제공하는 기능.
    • ex) log 추적기, transaction

다형성, 상속 사용

  • abstract class에 공통 기능을 넣어둔다.
  • 자식은 override하여 핵심기능을 수정한다.

SRP

단일 책임 원칙을 준수하고 있다.

각 클래스가 명확한 책임을 가짐

 

  • 상위 클래스(Template): 알고리즘의 구조를 정의.
  • 하위 클래스(ConcreteClass): 알고리즘의 구체적인 단계 구현.

이로 인해 한 클래스가 지나치게 많은 책임을 지는 것을 방지합니다.

 

 

 

 

 

 

abstract class == 상속 --> overrride 사용

  • abstract class에서 알고리즘의 골격(템플릿)을 정의
  • 세부적인 구현은 하위 클래스에서 재정의(override)
  • 코드 재사용성을 높이고, 알고리즘의 구조를 유지하면서 세부 구현의 유연성

 

 

단점:

  • 상속
    • 자식은 부모의 기능이 다 필요없는데도, 가지고 있어야한다.
    • 자식은 부모 클래스를 강하게 의존

해결책:

  • template method pattern과 비슷한 역할을 하면서 상속의 단점을 제거한 것이 strategy pattern이다.

 

template method example code  : CookingTemplate, Pasta, BBQ

더보기
// 1. 골격 정의
abstract class CookingTemplate {

    // 템플릿 메서드: 요리 과정의 전체 흐름을 정의
    public final void cook() {
        prepareIngredients();
        cookDish();
        serveDish();
    }

    // 공통 단계 (구현 제공)
    private void prepareIngredients() {
        System.out.println("재료를 준비합니다.");
    }

    // 추상 메서드: 하위 클래스에서 구체적인 구현 제공
    protected abstract void cookDish();

    // 공통 단계 (구현 제공)
    private void serveDish() {
        System.out.println("요리를 접시에 담아 제공합니다.");
    }
}

// 2. 구체 정의
class PastaCooking extends CookingTemplate {
    @Override
    protected void cookDish() {
        System.out.println("파스타를 조리합니다.");
    }
}

class KoreanBBQCooking extends CookingTemplate {
    @Override
    protected void cookDish() {
        System.out.println("고기를 구워 한국식 바비큐를 준비합니다.");
    }
}

// 3. 실행
public class Main {
    public static void main(String[] args) {
        // 파스타 요리
        CookingTemplate pastaCooking = new PastaCooking();
        System.out.println("== 파스타 요리 시작 ==");
        pastaCooking.cook();
        
        System.out.println();

        // 한국식 바비큐 요리
        CookingTemplate koreanBBQCooking = new KoreanBBQCooking();
        System.out.println("== 한국식 바비큐 요리 시작 ==");
        koreanBBQCooking.cook();
    }
}

//4. 결과
== 파스타 요리 시작 ==
재료를 준비합니다.
파스타를 조리합니다.
요리를 접시에 담아 제공합니다.

== 한국식 바비큐 요리 시작 ==
재료를 준비합니다.
고기를 구워 한국식 바비큐를 준비합니다.
요리를 접시에 담아 제공합니다.

 

 

 

전략 (Strategy)

  • template method pattern의 상속의 단점을 제거
  • 위임(=역할, interface)을 정의하고, 대상을 갈아끼움
  • 캡슐화 사용

변하는 것, 변하지 않는 것

  • 변하는 것: 핵심 기능, 회색
  • 변하지 않는 것: 템플릿, 초록색

 

 

 

여기서 한번 더 합칠 수 있다.

Context class에서 setStrategy(Strategy st)와 executeStrategy(int a, int b) 를 하나로 할수 있다.

 

한계

아무리 최적해도, 공통 부분을 적용하기 위해 원본 코드를 수정해야한다.

class가 수백개이면 수백개를 수정해야한다.

그래서 나온 것이 프록시 패턴

 

strategy example code: Strategy, AddStrategy, MultiplyStrategy, Context

더보기
// 전략 인터페이스
interface Strategy {
    int execute(int a, int b);
}

// 구체적인 전략
class AddStrategy implements Strategy {
    public int execute(int a, int b) {
        return a + b;
    }
}

class MultiplyStrategy implements Strategy {
    public int execute(int a, int b) {
        return a * b;
    }
}

// 컨텍스트 클래스
class Context {
    private Strategy strategy;

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int a, int b) {
        return strategy.execute(a, b);
    }
}

public class Main {
    public static void main(String[] args) {
        Context context = new Context();

        context.setStrategy(new AddStrategy());
        System.out.println("Addition: " + context.executeStrategy(5, 3));

        context.setStrategy(new MultiplyStrategy());
        System.out.println("Multiplication: " + context.executeStrategy(5, 3));
    }
}

 

 

방문자 (Visitor)

객체 구조를 순회하며 작업 수행

interface Visitor { void visit(Element element); }
class Element {
    void accept(Visitor visitor) { visitor.visit(this); }
}