디자인 패턴

2025. 7. 18. 15:21·cs/객체지향

생성 패턴

싱글톤 패턴

클래스의 인스턴스가 하나만 생성하도록 보장하고, 그 인스턴스에 대한 전역적인 접근법을 제공한다.

 
코드
public class SettingsManager {

    // 1. 클래스 로딩 시점에 유일한 인스턴스를 미리 생성
    private static final SettingsManager instance = new SettingsManager();

    // 2. 외부에서 생성자를 호출할 수 없도록 private으로 선언
    private SettingsManager() {
        // 설정 로딩 등 초기화 코드
    }

    // 3. 외부에서 유일한 인스턴스에 접근할 수 있는 통로 제공
    public static SettingsManager getInstance() {
        return instance;
    }

    // 기타 메소드
    public void printSettings() {
        System.out.println("Singleton instance settings.");
    }
}

프로토타입 패턴

새로운 객체를 new 키워드를 사용해 생성하는 대신, 기존에 있는 객체를 복사(clone)하여 생성하는 디자인 패턴이다.

목적: 복사할 객체를 파라미터로 전달했을 때 복사할 객체의 상태 정보를 알고 있어 캡슐화 원칙을 어기는 문제가 발생할 수 있다. 그리고 객체를 생성하는 과정에서 DB 조회 혹은 네트워크 통신으로 비용이 발생한다면 미리 만들어진 객체를 통하여 새로운 객체를 생성할 때 사용한다.

코드
public abstract class Shape implements Cloneable {

    private String id;
    protected String type;

    abstract void draw();

    public String getType(){
        return type;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    // clone 메소드를 오버라이드하여 객체 복사를 구현
    @Override
    public Object clone() {
        Object clone = null;
        try {
            clone = super.clone(); // 필드 복사, 얕은 복사
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

public class Circle extends Shape {

    public Circle(){
        type = "Circle";
    }

    @Override
    public void draw() {
        System.out.println("Inside Circle::draw() method.");
    }
}

팩토리 메서드 패턴

객체 생성을 서브클래스에 위임하는 디자인 패턴이다. 부모 클래스에서는 객체 생성에 필요한 인터페이스(메서드)만 정의하고, 어떤 클래스의 인스턴스를 만들지에 대한 결정은 서브클래스가 하도록 만든다.

목적: 구체적인 객체를 결정할 수 없을 때

 
코드
// 커피숍의 기본 구조
abstract class CoffeeShop {
    // 팩토리 메서드: 어떤 커피를 만들지는 서브클래스가 결정
    public abstract Coffee createCoffee();
    
}

class LatteShop extends CoffeeShop {
    @Override
    public Coffee createCoffee() {
        return new Latte(); // 라떼를 생성
    }
}

class AmericanoShop extends CoffeeShop {
    @Override
    public Coffee createCoffee() {
        return new Americano(); // 아메리카노를 생성
    }
}

 

추상 팩토리 패턴

서로 관련이 있거나 의존적인 객체들의 묶음(제품군)을 생성하기 위한 인터페이스를 제공하는 디자인 패턴이다.

“테마에 맞는 제품군을 한번에”

 
코드
interface FurnitureFactory {
    Chair createChair();
    Sofa createSofa();
}

class ModernFurnitureFactory implements FurnitureFactory {
    public Chair createChair() { return new ModernChair(); }
    public Sofa createSofa() { return new ModernSofa(); }
}

class VintageFurnitureFactory implements FurnitureFactory {
    public Chair createChair() { return new VintageChair(); }
    public Sofa createSofa() { return new VintageSofa(); }
}
💡

팩토리 메서드 패턴과 추상 팩토리 패턴의 차이

  1. 객체 개수의 차이
  2. 구조의 차이

빌더 패턴

복잡한 객체의 생성과정과 표현 방법을 분리하여, 동일한 생성 절차로도 서로 다른 결과를 만들 수 있게 하는 생성 디자인 패턴이다.

목적

  • 생성해야 할 객체의 매개변수가 많고, 대부분이 선택사항일 때
  • 객체 생성 시 특정 순서에 따라 단계별로 만들어져야 할 때
  • 생성 후에는 변경되지 않는 불변 객체를 만들 때
‣
// Product: 최종적으로 만들어질 객체
class User {
    private final String userId;   // 필수
    private final String password; // 필수
    private final String name;     // 선택
    private final int age;         // 선택
    private final String address;  // 선택

    // Builder를 통해서만 객체를 생성할 수 있도록 생성자는 private
    private User(UserBuilder builder) {
        this.userId = builder.userId;
        this.password = builder.password;
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
    }

    // Static inner Builder class
    public static class UserBuilder {
        private final String userId;   // 필수
        private final String password; // 필수
        private String name;     // 선택
        private int age;         // 선택
        private String address;  // 선택

        // 필수 매개변수는 Builder의 생성자에서 받음
        public UserBuilder(String userId, String password) {
            this.userId = userId;
            this.password = password;
        }

        // 선택 매개변수는 각각의 메서드를 통해 설정하고, Builder 자신을 반환 (Method Chaining)
        public UserBuilder name(String name) {
            this.name = name;
            return this;
        }

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

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

        // build() 메서드가 최종적으로 User 객체를 생성하여 반환
        public User build() {
            return new User(this);
        }
    }

    @Override
    public String toString() {
        return "User{" +
                "userId='" + userId + '\'' +
                ", password='" + password + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

// 실행 코드
public class Main {
    public static void main(String[] args) {
        User user = new User.UserBuilder("testuser", "1234")
                .name("김철수")
                .age(30)
                // .address("서울") // 주소는 설정하지 않음
                .build();

        System.out.println(user);
    }
}

구조 패턴

구조 패턴은 클래스와 객체를 조합하여 더 큰 구조를 만드는 방법을 다루며, 시스템의 유연성과 효율성을 높이는 데 중점을 둔다.

어댑터 패턴

호환되지 않은 인터페이스를 가진 클래스들이 함께 작동할 수 있도록, 하나의 인터페이스를 다른 인터페이스로 변환해주는 역할을 한다.

목적: 기존 코드를 변경하지 않고 새로운 클래스를 사용하고 싶을 때, 서로 다른 API를 가진 라이브러리를 통합해야 할 때 사용한다.

 
코드
// 사용할 수 없는 구형 시스템
interface OldSystem {
    void legacyRequest();
}
class OldSystemImpl implements OldSystem {
    public void legacyRequest() { System.out.println("구형 시스템 호출"); }
}

// 우리가 사용하려는 새로운 시스템 인터페이스
interface NewSystem {
    void newRequest();
}

// 어댑터: OldSystem 인터페이스를 NewSystem 인터페이스처럼 사용하게 해줌
class Adapter implements NewSystem {
    private OldSystem oldSystem;

    public Adapter(OldSystem oldSystem) {
        this.oldSystem = oldSystem;
    }

    @Override
    public void newRequest() {
        System.out.print("어댑터 경유 -> ");
        oldSystem.legacyRequest();
    }
}

// 실행
public class Main {
    public static void main(String[] args) {
        OldSystem oldSystem = new OldSystemImpl();
        NewSystem client = new Adapter(oldSystem);
        client.newRequest(); // 클라이언트는 NewSystem 인터페이스만 사용
    }
}

브릿지 패턴

추상화 부분과 구현 부분을 분리하여 각각 독립적으로 확장할 수 있게 한다. 상속 대신 합성을 이용하여 기능과 구현을 연결한다.

추상화: 무엇을 하는지(코드 예시에서는 draw)

구현: 어떻게 하는지(코드 예시에서는 applyColor)

목적: 기능과 구현을 분리하여 독립적으로 변경해야 할 때

 
코드
// 구현 인터페이스 (Implementation)
interface Color {
    String applyColor();
}
class Red implements Color {
    public String applyColor() { return "빨간색"; }
}
class Blue implements Color {
    public String applyColor() { return "파란색"; }
}

// 추상화 (Abstraction)
abstract class Shape {
    protected Color color; // 구현을 참조 (브리지)
    public Shape(Color color) { this.color = color; }
    abstract public void draw();
}
class Circle extends Shape {
    public Circle(Color color) { super(color); }
    public void draw() { System.out.println(color.applyColor() + " 원"); }
}

// 실행
public class Main {
    public static void main(String[] args) {
        Shape redCircle = new Circle(new Red());
        redCircle.draw();
    }
}

복합체 패턴

개별 객체와 객체의 집합을 동일한 방식으로 다룰 수 있게 한다. 객체들을 트리 구조로 구성하여 전체-부분 관계를표현한다.

 
코드
// 공통 인터페이스 (Component)
interface Graphic {
    void draw();
}
// 개별 객체 (Leaf)
class Dot implements Graphic {
    public void draw() { System.out.println("점 그리기"); }
}
// 복합 객체 (Composite)
class CompoundGraphic implements Graphic {
    private List<Graphic> children = new ArrayList<>();
    public void add(Graphic child) { children.add(child); }
    public void draw() {
        System.out.println("복합 그래픽 시작");
        for (Graphic child : children) {
            child.draw(); // 자식들에게 재귀적으로 draw 호출
        }
    }
}

// 실행
public class Main {
    public static void main(String[] args) {
        CompoundGraphic all = new CompoundGraphic();
        all.add(new Dot());
        all.add(new Dot());
        all.draw();
    }
}

데코레이터 패턴

객체에 동적으로 새로운 책임(기능)을 추가할 수 있게 한다. 상속을 사용하지 않고도 객체의 기능을 확장할 수 있다.

목적: 객체의 일부 기능만 확장하고 싶을 때, 런타임에 기능을 추가하거나 제거해야 할 때 사용한다.

코드
// 공통 인터페이스 (Component)
interface Coffee {
    String getDescription();
    double getCost();
}
// 기본 객체 (Concrete Component)
class SimpleCoffee implements Coffee {
    public String getDescription() { return "커피"; }
    public double getCost() { return 1.0; }
}
// 데코레이터 추상 클래스
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
    public String getDescription() { return decoratedCoffee.getDescription(); }
    public double getCost() { return decoratedCoffee.getCost(); }
}
// 구체적인 데코레이터
class WithMilk extends CoffeeDecorator {
    public WithMilk(Coffee coffee) { super(coffee); }
    public String getDescription() { return super.getDescription() + ", 우유 추가"; }
    public double getCost() { return super.getCost() + 0.5; }
}

// 실행
public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new WithMilk(coffee); // 우유 기능 추가
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());
    }
}

퍼사드 패턴

복잡한 서브시스템에 대해 단순화된 인터페이스를 제공한다. 서브시스템의 복잡한 내부 구조를 숨기고, 클라이언트가 쉽게 사용할 수 있는 창구 역할을 한다.

코드
// 복잡한 서브시스템
class CPU { void process() { System.out.println("CPU 처리"); } }
class Memory { void load() { System.out.println("메모리 로드"); } }
class HardDrive { void read() { System.out.println("하드 드라이브 읽기"); } }

// 퍼사드
class ComputerFacade {
    private CPU cpu = new CPU();
    private Memory memory = new Memory();
    private HardDrive hardDrive = new HardDrive();
    // 단순화된 인터페이스 제공
    public void start() {
        hardDrive.read();
        memory.load();
        cpu.process();
        System.out.println("컴퓨터 부팅 완료");
    }
}

// 실행
public class Main {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();
        computer.start(); // 복잡한 과정 대신 start()만 호출
    }
}

플라이웨이트 패턴

다수의 유사한 객체를 생성할 때, 공유를 통해 메모리 사용량을 최소화한다. 객체의 상태를 공유 가능한 내재적 상태와 공유 불가능한 외재적 상태로 분리한다.

 
코드
// 플라이웨이트 객체 (공유 대상)
class TreeType {
    private String name; // 내재적 상태 (공유 가능)
    public TreeType(String name) { this.name = name; }
    public void draw(int x, int y) { // 외재적 상태 (공유 불가)
        System.out.println(name + " 나무를 (" + x + ", " + y + ")에 그리기");
    }
}
// 플라이웨이트 팩토리
class TreeFactory {
    private static Map<String, TreeType> treeTypes = new HashMap<>();
    public static TreeType getTreeType(String name) {
        if (!treeTypes.containsKey(name)) {
            treeTypes.put(name, new TreeType(name));
        }
        return treeTypes.get(name);
    }
}

// 실행
public class Main {
    public static void main(String[] args) {
        TreeType type1 = TreeFactory.getTreeType("소나무");
        type1.draw(10, 20);
        TreeType type2 = TreeFactory.getTreeType("소나무"); // 기존 객체 재사용
        type2.draw(30, 50);
    }
}

프록시 패턴

어떤 객체에 대한 접근을 제어하기 위해 대리인(프록시) 객체를 제공한다. 프록시는 실제 객체에 대한 접근을 관리하며, 접근 직후에 추가적인 로직을 수행할 수 있다.

💡

프록시 패턴 vs 데코레이터 패턴

두 패턴 사이의 구조적인 유사성 때문에 같은 패턴이라고 생각할 지 모르지만 둘의 차이는 존재한다.

두 패턴의 차이는 목적에 존재한다.

  • 프록시 패턴: 객체에 대한 접근을 제어하고 관리
  • 데코레이터 패턴: 새로운 기능(책임)을 동적으로 추가
 
코드
// 공통 인터페이스 (Subject)
interface Image {
    void display();
}
// 실제 객체 (Real Subject)
class RealImage implements Image {
    private String fileName;
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk();
    }
    private void loadFromDisk() { System.out.println(fileName + " 로딩 중..."); }
    public void display() { System.out.println(fileName + " 표시"); }
}
// 프록시 객체
class ProxyImage implements Image {
    private RealImage realImage; // 실제 객체를 참조
    private String fileName;
    public ProxyImage(String fileName) { this.fileName = fileName; }
    public void display() {
        if (realImage == null) { // 필요할 때만 실제 객체 생성
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}

// 실행
public class Main {
    public static void main(String[] args) {
        Image image = new ProxyImage("photo.jpg");
        image.display(); // 이 시점에 RealImage 로딩 및 표시
        image.display(); // 이미 로딩되었으므로 바로 표시
    }
}

행동 패턴

행동 패턴은 객체들이 상호작용하는 방식과 책임을 분배하는 방법을 다룬다. 객체 간의 결합도를 낮추고 유연한 협력 관계를 만드는 데 중점을 둔다.

책임 연쇄 패턴

요청을 보내는 객체와 요청을 처리하는 개체를 분리하고, 여러 가지 객체를 사슬처럼 연결하여 요청이 해결될 떄까지 사슬을 따라 전달하게 하는 패턴이다.

목적: 요청 처리 객체와 요청 객체 간의 결합을 느슨하게 하고, 요청 처리 순서를 동적으로 변경할 수 있다.

 
패턴 적용 전 코드
// 클라이언트 코드
void requestApproval(int amount) {
    if (amount <= 1000000) {
        teamLeader.process(amount); // 클라이언트가 직접 팀장에게 보냄
    } else if (amount <= 5000000) {
        manager.process(amount); // 클라이언트가 직접 부장에게 보냄
    } else {
        // ...
    }
}
 
코드
// 처리기(결재자)의 공통 인터페이스
interface Approver {
    void setNext(Approver next); // 다음 결재자 설정
    void process(int amount);    // 결재 요청 처리
}

// 팀장
class TeamLeader implements Approver {
    private Approver next;

    @Override
    public void setNext(Approver next) {
        this.next = next;
    }

    @Override
    public void process(int amount) {
        if (amount <= 1000000) { // 100만원 이하는 팀장이 처리
            System.out.println("팀장이 승인했습니다.");
        } else if (next != null) {
            System.out.println("팀장이 처리할 수 없어 부장에게 넘깁니다.");
            next.process(amount); // 처리 못하면 다음 사람에게 넘김
        }
    }
}

// 부장
class DepartmentManager implements Approver {
    private Approver next;

    @Override
    public void setNext(Approver next) {
        this.next = next;
    }

    @Override
    public void process(int amount) {
        if (amount <= 5000000) { // 500만원 이하는 부장이 처리
            System.out.println("부장이 승인했습니다.");
        } else if (next != null) {
            System.out.println("부장이 처리할 수 없어 사장에게 넘깁니다.");
            next.process(amount);
        } else {
             System.out.println("결재할 수 있는 사람이 없습니다.");
        }
    }
}

// 실행 코드
public class Main {
    public static void main(String[] args) {
        // 1. 결재 라인(사슬) 생성
        Approver teamLeader = new TeamLeader();
        Approver manager = new DepartmentManager();
        teamLeader.setNext(manager);

        // 2. 50만원 요청 (팀장이 처리)
        System.out.println("50만원 경비 요청:");
        teamLeader.process(500000);

        System.out.println("\n---------------------------\n");

        // 3. 300만원 요청 (팀장 -> 부장)
        System.out.println("300만원 경비 요청:");
        teamLeader.process(3000000);
    }
}

커맨드 패턴

요청 자체를 객체로 캡슐화하여, 요청이 필요한 시점에 실행하거나, 취소(Undo), 로깅, 큐에 저장하는 등의 작업을 가능하게 한다.

목적: 요청을 보내는 객체와 요청을 수행하는 객체를 분리하여 서로 의존하지 않게 한다.

 
패턴 적용 전 코드
// 작업을 수행할 객체들
class Light {
    public void on() { System.out.println("불이 켜졌습니다."); }
    public void off() { System.out.println("불이 꺼졌습니다."); }
}

class Fan {
    public void start() { System.out.println("선풍기가 켜졌습니다."); }
    public void stop() { System.out.println("선풍기가 꺼졌습니다."); }
}

// 요청을 보내는 리모컨
class SimpleRemoteControl {
    private Light light = new Light();
    private Fan fan = new Fan();

    // 각 버튼이 특정 기기와 그 기기의 메서드에 직접 의존
    public void lightOnButtonClick() {
        light.on();
    }

    public void fanStartButtonClick() {
        fan.start();
    }
}

public class Main {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl();
        remote.lightOnButtonClick();
        remote.fanStartButtonClick();
    }
}
 
코드
// --- 1. 작업을 수행할 객체들 (Receiver) ---
class Light {
    public void on() { System.out.println("불이 켜졌습니다."); }
}
class Fan {
    public void start() { System.out.println("선풍기가 켜졌습니다."); }
}


// --- 2. 모든 커맨드의 공통 인터페이스 (Command) ---
interface Command {
    void execute();
}


// --- 3. 구체적인 커맨드 객체들 (Concrete Command) ---
class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) { this.light = light; }

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

class FanStartCommand implements Command {
    private Fan fan;
    public FanStartCommand(Fan fan) { this.fan = fan; }

    @Override
    public void execute() {
        fan.start();
    }
}


// --- 4. 요청을 보내는 리모컨 (Invoker) ---
class SimpleRemoteControl {
    private Command slot; // 어떤 커맨드든 담을 수 있는 슬롯

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

    public void buttonWasPressed() {
        slot.execute(); // 커맨드 실행
    }
}


// --- 5. 실행 ---
public class Main {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl();
        
        // 1. 불 켜기 설정
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);
        remote.setCommand(lightOn);
        remote.buttonWasPressed();

        // 2. 선풍기 켜기 설정
        Fan fan = new Fan();
        Command fanStart = new FanStartCommand(fan);
        remote.setCommand(fanStart);
        remote.buttonWasPressed();
    }
}

개선점

  • 낮은 결합도
  • 높은 확장성: 다른 기능이 추가되면 객체와 커맨드만 추가하면 된다.
  • 유연성: 리모컨의 버튼 기능을 런타임에 바꿀 수 있다.

반복자 패턴

컬렉션의 내부 구조를 노출하지 않고 순차적으로 접근할 수 있는 방법을 제공한다.

목적: 컬렉션의 구현 방식과 순회 방식을 분리하여, 어떤 종류의 컬렉션이든 동일한 방식으로 순회할 수 있도록 한다.

 
패턴 적용 전 코드
class Book {
    private String name;
    public Book(String name) { this.name = name; }
    public String getName() { return name; }
}

// 책을 보관하는 책꽂이 클래스
class BookShelf {
    private Book[] books;
    private int last = 0;

    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }

    public Book getBookAt(int index) {
        return books[index];
    }

    public void addBook(Book book) {
        this.books[last] = book;
        last++;
    }

    public int getLength() {
        return last;
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.addBook(new Book("디자인 패턴"));
        bookShelf.addBook(new Book("자바 기초"));
        
        // 클라이언트가 책꽂이의 내부 구현 방식(배열, getLength)을 직접 알아야 함
        for (int i = 0; i < bookShelf.getLength(); i++) {
            System.out.println(bookShelf.getBookAt(i).getName());
        }
    }
}
 
코드
import java.util.Iterator;
import java.util.ArrayList;

class Book { /* 이전과 동일 */ 
    private String name;
    public Book(String name) { this.name = name; }
    public String getName() { return name; }
}

// Iterable 인터페이스를 구현하여 "순회 가능한 객체"임을 명시
class BookShelf implements Iterable<Book> {
    // 내부 구현을 ArrayList로 변경해도 클라이언트 코드는 영향을 받지 않음
    private ArrayList<Book> books;

    public BookShelf() {
        this.books = new ArrayList<>();
    }
    
    public void addBook(Book book) {
        this.books.add(book);
    }
    
    // Iterator를 반환하는 메서드를 구현
    @Override
    public Iterator<Book> iterator() {
        return books.iterator(); // ArrayList가 기본으로 제공하는 iterator를 반환
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf();
        bookShelf.addBook(new Book("디자인 패턴"));
        bookShelf.addBook(new Book("자바 기초"));

        // 1. Iterator를 이용한 순회
        Iterator<Book> it = bookShelf.iterator();
        while (it.hasNext()) {
            System.out.println(it.next().getName());
        }

        System.out.println("--------------------");

        // 2. 향상된 for문 사용 (Iterable을 구현했기 때문에 가능)
        // 클라이언트는 내부 구조를 전혀 몰라도 순회 가능
        for (Book book : bookShelf) {
            System.out.println(book.getName());
        }
    }
}

개선점

  • 낮은 결합도: 클라이언트는 BookShelf의 내부 구현이 무엇인지 전혀 모른다.
  • 높은 유연성 및 유지보수성: 컬렉션을 변경하더라도 iterator 만 변경하면 된다.
  • 코드 간결성: Iterable 인터페이스를 구현하면 향상된 for 문을 사용할 수 있어 클라이언트 코드가 깔끔해진다.

중재자 패턴

여러 객체 간의 복잡한 상호작용(M:N)을 하나의 중재자 객체에 캡슐화하여 관리한다.

목적: 객체간의 직접적인 통신을 막아 결합도를 낮추고, 상호작용 로직을 한 곳에서 중앙 관리하여 유지보수를 용이하게 한다.

 
패턴 적용 전 코드
// 각 컴포넌트가 다른 모든 컴포넌트를 알아야 하는 상황
class Button {
    private TextField textField;
    private Checkbox checkbox;

    public void setTextField(TextField textField) { this.textField = textField; }
    public void setCheckbox(Checkbox checkbox) { this.checkbox = checkbox; }

    public void click() {
        System.out.println("버튼 클릭됨");
        // 버튼이 클릭되면 다른 컴포넌트들의 상태를 직접 변경
        textField.setText("");
        checkbox.setChecked(false);
    }
}

class TextField {
    private String text = "";
    public void setText(String text) { this.text = text; }
    public String getText() { return text; }
}

class Checkbox {
    private boolean checked = false;
    public void setChecked(boolean checked) { this.checked = checked; }
    public boolean isChecked() { return checked; }
}


// 클라이언트가 모든 객체 간의 의존성을 직접 설정해야 함
public class Main {
    public static void main(String[] args) {
        Button button = new Button();
        TextField textField = new TextField();
        Checkbox checkbox = new Checkbox();

        // 모든 객체들이 서로를 직접 참조하도록 설정 (스파게티 코드)
        button.setTextField(textField);
        button.setCheckbox(checkbox);

        button.click();
    }
}
 
코드
// --- 1. 중재자 인터페이스 ---
interface Mediator {
    void notify(Component sender, String event);
}

// --- 2. 동료(Colleague) 클래스들 ---
// 모든 컴포넌트의 부모 클래스
abstract class Component {
    protected Mediator mediator;
    public Component(Mediator mediator) { this.mediator = mediator; }
}

class Button extends Component {
    public Button(Mediator mediator) { super(mediator); }
    public void click() {
        System.out.println("버튼 클릭됨");
        mediator.notify(this, "click"); // 변경 사항을 중재자에게 알림
    }
}

class TextField extends Component {
    private String text = "";
    public TextField(Mediator mediator) { super(mediator); }
    public void setText(String text) { 
        this.text = text;
        System.out.println("텍스트 필드 초기화됨");
    }
}

class Checkbox extends Component {
    private boolean checked = false;
    public Checkbox(Mediator mediator) { super(mediator); }
    public void setChecked(boolean checked) {
        this.checked = checked;
        System.out.println("체크박스 해제됨");
    }
}

// --- 3. 구체적인 중재자 ---
class DialogMediator implements Mediator {
    private Button button;
    private TextField textField;
    private Checkbox checkbox;
    
    // 중재자는 모든 컴포넌트를 알고 있음
    public void setButton(Button button) { this.button = button; }
    public void setTextField(TextField textField) { this.textField = textField; }
    public void setCheckbox(Checkbox checkbox) { this.checkbox = checkbox; }
    
    // 모든 상호작용 로직이 여기에 중앙 집중화됨
    @Override
    public void notify(Component sender, String event) {
        if (sender == button && event.equals("click")) {
            textField.setText("");
            checkbox.setChecked(false);
        }
    }
}


// --- 4. 실행 ---
public class Main {
    public static void main(String[] args) {
        DialogMediator mediator = new DialogMediator();
        
        Button button = new Button(mediator);
        TextField textField = new TextField(mediator);
        Checkbox checkbox = new Checkbox(mediator);
        
        // 중재자에게 컴포넌트들을 등록
        mediator.setButton(button);
        mediator.setTextField(textField);
        mediator.setCheckbox(checkbox);
        
        button.click();
    }
}

개선점

  • 낮은 결합도: Button, TextField, Checkbox는 서로 전혀 모른다.오직 Mediator와만 통신한다.
  • 중앙 집중화된 제어: 모든 상호작용 로직이 DialogMediator 안에 모여있어 코드를 이해하고 수정하기 훨씬 쉬워졌다.
  • 높은 재사용성: 개별 컴포넌트들은 다른 중재자와도 쉽게 조합하여 재사용할 수 있다.

메멘토 패턴

객체의 내부 상태를 외부화하여 저장해두고, 나중에 이 상태로 다시 복원할 수 있다.

목적: 객체의 캡슐화를 깨지 않으면서 되돌리기(Undo) 와 같은 상태 복원 기능을 수행할 수 있다.

 
패턴 적용 전 코드
// 글의 상태를 저장할 객체
class Editor {
    // 내부 상태가 public으로 외부에 그대로 노출됨 (캡슐화 위반)
    public String content;

    public void type(String words) {
        this.content = (this.content == null ? "" : this.content) + words;
    }
}

// Editor의 상태를 저장하고 관리하는 객체
class History {
    // Editor의 상태를 저장하기 위해 Editor의 필드와 동일한 필드를 가져야 함
    private String backupContent;

    public void save(Editor editor) {
        // Editor의 내부 상태에 직접 접근하여 저장
        this.backupContent = editor.content;
    }

    public void undo(Editor editor) {
        // Editor의 내부 상태를 직접 변경하여 복원
        editor.content = this.backupContent;
    }
}

public class Main {
    public static void main(String[] args) {
        Editor editor = new Editor();
        History history = new History();

        editor.type("첫 번째 문장. ");
        history.save(editor); // 현재 상태 저장

        editor.type("두 번째 문장. ");
        System.out.println("현재 내용: " + editor.content);

        history.undo(editor); // 되돌리기
        System.out.println("되돌린 후 내용: " + editor.content);
    }
}
 
코드
// --- 2. 메멘토 (Memento): Originator의 상태를 저장하는 객체 ---
// Editor의 상태를 저장하지만, 외부에서는 상태에 접근할 수 없음 (보통 private inner class로 구현)
class EditorState {
    private final String content; // 불변

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

    public String getContent() {
        return content;
    }
}

// --- 1. 오리지네이터 (Originator): 상태를 저장하고 복원할 주체 ---
class Editor {
    private String content; // 내부 상태는 private으로 캡슐화

    public void type(String words) {
        this.content = (this.content == null ? "" : this.content) + words;
    }

    public String getContent() {
        return content;
    }

    // 자신의 현재 상태를 Memento 객체로 만들어 반환
    public EditorState save() {
        return new EditorState(this.content);
    }

    // Memento 객체를 받아 자신의 상태를 복원
    public void restore(EditorState state) {
        this.content = state.getContent();
    }
}

// --- 3. 케어테이커 (Caretaker): Memento 객체를 보관하고 관리 ---
class History {
    private EditorState savedState;

    public void save(EditorState state) {
        this.savedState = state;
    }
    
    public EditorState getSavedState() {
        return this.savedState;
    }
}

// --- 4. 실행 ---
public class Main {
    public static void main(String[] args) {
        Editor editor = new Editor();
        History history = new History();

        editor.type("첫 번째 문장. ");
        history.save(editor.save()); // Editor가 직접 생성한 상태(Memento)를 저장

        editor.type("두 번째 문장. ");
        System.out.println("현재 내용: " + editor.getContent());

        editor.restore(history.getSavedState()); // 저장된 상태(Memento)로 복원
        System.out.println("되돌린 후 내용: " + editor.getContent());
    }
}

옵저버 패턴

한 객체의 상태가 변하면, 그 객체의 의존하는 다른 객체들에게 자동으로 알림이 가고 업데이트되도록 하는 일대다(1:N) 의존 관계를 정의한다.

목적: 객체 간의 느슨한 결합을 유지하면서 상태 변화에 따른 자동 업데이트를 구현한다.

 
패턴 적용 전 코드
// 날씨 정보를 표시하는 디스플레이들
class CurrentConditionsDisplay {
    public void update(float temperature, float humidity) {
        System.out.println("현재 날씨: 온도 " + temperature + "도, 습도 " + humidity + "%");
    }
}

class StatisticsDisplay {
    public void showStats(float temperature) {
        System.out.println("통계: 평균 온도 " + temperature + "도");
    }
}


// 날씨 정보를 관리하는 주체
class WeatherStation {
    private float temperature;
    private float humidity;

    // WeatherStation이 모든 디스플레이 종류를 직접 알고 있어야 함
    private CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
    private StatisticsDisplay statsDisplay = new StatisticsDisplay();

    public void setMeasurements(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;

        // 데이터가 변경될 때마다 각 디스플레이의 메서드를 직접 호출
        currentDisplay.update(temperature, humidity);
        statsDisplay.showStats(temperature);
    }
}


public class Main {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();
        weatherStation.setMeasurements(25.0f, 65.0f);
    }
}
 
코드
import java.util.ArrayList;
import java.util.List;

// --- 1. 옵서버 인터페이스 ---
interface Observer {
    void update(float temperature, float humidity);
}


// --- 2. 서브젝트 인터페이스 ---
interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}


// --- 3. 구체적인 서브젝트 ---
class WeatherStation implements Subject {
    private List<Observer> observers; // Observer 인터페이스에만 의존
    private float temperature;
    private float humidity;

    public WeatherStation() {
        observers = new ArrayList<>();
    }

    public void setMeasurements(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        notifyObservers(); // 상태 변경 시 옵서버들에게 알림
    }
    
    @Override
    public void registerObserver(Observer o) { observers.add(o); }
    @Override
    public void removeObserver(Observer o) { observers.remove(o); }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity);
        }
    }
}


// --- 4. 구체적인 옵서버들 ---
class CurrentConditionsDisplay implements Observer {
    @Override
    public void update(float temperature, float humidity) {
        System.out.println("현재 날씨: 온도 " + temperature + "도, 습도 " + humidity + "%");
    }
}
class StatisticsDisplay implements Observer {
    @Override
    public void update(float temperature, float humidity) {
        System.out.println("통계: 평균 온도 " + temperature + "도");
    }
}


// --- 5. 실행 ---
public class Main {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();
        
        // 옵서버(디스플레이)들을 서브젝트(WeatherStation)에 등록
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
        weatherStation.registerObserver(currentDisplay);
        
        StatisticsDisplay statsDisplay = new StatisticsDisplay();
        weatherStation.registerObserver(statsDisplay);
        
        // 날씨 정보 변경 -> 등록된 모든 옵서버에게 자동으로 알림이 감
        weatherStation.setMeasurements(25.0f, 65.0f);
    }
}
  • 낮은 결합도: WeatherStation은 이제 Observer 인터페이스에만 의존하며, CurrentConditionsDisplay와 같은 구체적인 클래스의 존재를 전혀 모른다.
  • 높은 확장성: 새로운 디스플레이를 추가하고 싶다면, Observer 인터페이스를 구현하는 클래스를 만들기만 하면 된다.
  • 유연성: 런타임에 옵저버를 동적으로 추가하거나 제거할 수 있어 유연한 시스템을 만들 수 있다.

상태 패턴

객체의 내부 상태가 변경됨에 따라 객체의 행동이 바뀌도록 한다.객체가 마치 자신의 클래스를 바꾸는 것처럼 보인다.

목적: if/else나 switch 문으로 상태에 따라 분기하는 복잡한 조건문을 상태 객체로 대체하여 코드를 단순화한다.

 
패턴 적용 전 코드
class VendingMachine {
    // 0: 동전 없음, 1: 동전 있음, 2: 매진
    private int state = 0; 

    public VendingMachine() {
        this.state = 0;
    }

    public void insertCoin() {
        if (state == 0) {
            System.out.println("동전이 투입되었습니다.");
            state = 1; // 상태 변경
        } else if (state == 1) {
            System.out.println("이미 동전이 있습니다.");
        } else if (state == 2) {
            System.out.println("매진입니다.");
        }
    }
    
    public void selectItem() {
        if (state == 0) {
            System.out.println("동전을 먼저 넣어주세요.");
        } else if (state == 1) {
            System.out.println("상품이 나왔습니다.");
            state = 0; // 상태 변경
        } else if (state == 2) {
            System.out.println("매진입니다.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        VendingMachine vm = new VendingMachine();
        vm.selectItem(); // 동전을 먼저 넣어주세요.
        vm.insertCoin(); // 동전이 투입되었습니다.
        vm.selectItem(); // 상품이 나왔습니다.
    }
}
 
코드
// --- 1. 상태 인터페이스 ---
interface State {
    void insertCoin(VendingMachine vm);
    void selectItem(VendingMachine vm);
}


// --- 2. 구체적인 상태 클래스들 ---
class NoCoinState implements State {
    @Override
    public void insertCoin(VendingMachine vm) {
        System.out.println("동전이 투입되었습니다.");
        vm.setState(new HasCoinState()); // 상태를 '동전 있음'으로 변경
    }
    @Override
    public void selectItem(VendingMachine vm) {
        System.out.println("동전을 먼저 넣어주세요.");
    }
}

class HasCoinState implements State {
    @Override
    public void insertCoin(VendingMachine vm) {
        System.out.println("이미 동전이 있습니다.");
    }
    @Override
    public void selectItem(VendingMachine vm) {
        System.out.println("상품이 나왔습니다.");
        vm.setState(new NoCoinState()); // 상태를 '동전 없음'으로 변경
    }
}

// --- 3. 컨텍스트 (Context) 클래스 ---
class VendingMachine {
    private State currentState; // 현재 상태를 나타내는 객체

    public VendingMachine() {
        // 초기 상태는 '동전 없음'
        this.currentState = new NoCoinState();
    }
    
    // 상태 변경을 위한 메서드
    public void setState(State state) {
        this.currentState = state;
    }
    
    // 요청을 현재 상태 객체에 위임
    public void insertCoin() {
        currentState.insertCoin(this);
    }
    public void selectItem() {
        currentState.selectItem(this);
    }
}


// --- 4. 실행 ---
public class Main {
    public static void main(String[] args) {
        VendingMachine vm = new VendingMachine();
        vm.selectItem();
        vm.insertCoin();
        vm.selectItem();
    }
}

전략 패턴

알고리즘군을 정의하고 각각을 캡슐화하여 서로 교체할 수 있도록 한다.

목적: 클라이언트로부터 알고리즘을 분리하여 독립적으로 변경할 수 있다. if/else 분기 대신 사용할 수 있다.

 
패턴 적용 전 코드
class Robot {
    private String name;
    private String moveMode; // "walking" 또는 "flying"

    public Robot(String name) {
        this.name = name;
        this.moveMode = "walking"; // 기본은 걷기
    }

    public void setMoveMode(String mode) {
        this.moveMode = mode;
    }

    // 이동 방식이 추가될 때마다 이 메서드에 if-else가 계속 늘어남
    public void move() {
        if (moveMode.equals("walking")) {
            System.out.println(name + ": 걷는 중...");
        } else if (moveMode.equals("flying")) {
            System.out.println(name + ": 나는 중...");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Robot robot = new Robot("태권브이");
        robot.move();

        robot.setMoveMode("flying");
        robot.move();
    }
}
 
코드
// --- 1. 전략(Strategy) 인터페이스 ---
interface MovingStrategy {
    void move();
}


// --- 2. 구체적인 전략(Concrete Strategy) 클래스들 ---
class WalkingStrategy implements MovingStrategy {
    @Override
    public void move() {
        System.out.println("걷는 중...");
    }
}

class FlyingStrategy implements MovingStrategy {
    @Override
    public void move() {
        System.out.println("나는 중...");
    }
}


// --- 3. 컨텍스트(Context) 클래스 ---
class Robot {
    private String name;
    private MovingStrategy movingStrategy; // 전략 인터페이스에만 의존

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

    // 외부에서 전략을 설정(주입)
    public void setMovingStrategy(MovingStrategy movingStrategy) {
        this.movingStrategy = movingStrategy;
    }
    
    // 현재 설정된 전략을 실행
    public void move() {
        System.out.print(name + ": ");
        movingStrategy.move();
    }
}


// --- 4. 실행 ---
public class Main {
    public static void main(String[] args) {
        Robot robot = new Robot("태권브이");
        
        robot.setMovingStrategy(new WalkingStrategy());
        robot.move();

        robot.setMovingStrategy(new FlyingStrategy());
        robot.move();
    }
}
💡

상태 패턴과 전략 패턴의 차이는?

둘의 차이는 “누가” 행동을 “왜” 취하는지에 달려있다.

  • 상태 패턴: 객체 내부의 상태가 변경됨에 따라 행동이 달라짐
  • 전략 패턴: 클라이언트가 필요에 따라 행동(전략)을 바꿈

템플릿 메서드 패턴

알고리즘의 골격(뼈대)를 상위에 정의하고, 일부 단계를 서브클래스에서 구현하도록 한다.

목적: 알고리즘의 구조는 그대로 유지하면서 특정 단계만 서브클래스에서 변경할 수 있도록 하여 코드 중복을 줄인다.

 
패턴 적용 전 코드
class Coffee {
    // 커피 만드는 법
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }

    private void boilWater() { System.out.println("물 끓이기"); }
    private void brewCoffeeGrinds() { System.out.println("필터로 커피 내리기"); }
    private void pourInCup() { System.out.println("컵에 따르기"); }
    private void addSugarAndMilk() { System.out.println("설탕과 우유 추가하기"); }
}

class Tea {
    // 차 만드는 법
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }
    
    // Coffee 클래스와 중복되는 메서드들
    private void boilWater() { System.out.println("물 끓이기"); }
    private void pourInCup() { System.out.println("컵에 따르기"); }

    private void steepTeaBag() { System.out.println("티백 우려내기"); }
    private void addLemon() { System.out.println("레몬 추가하기"); }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new Coffee();
        System.out.println("--- 커피 만들기 ---");
        coffee.prepareRecipe();

        Tea tea = new Tea();
        System.out.println("\n--- 차 만들기 ---");
        tea.prepareRecipe();
    }
}
 
코드
// --- 1. 알고리즘의 뼈대를 정의하는 추상 클래스 ---
abstract class CaffeineBeverage {
    
    // 이 메서드가 바로 '템플릿 메서드'
    // 알고리즘의 순서는 바뀌지 않도록 final로 선언
    final void prepareRecipe() {
        boilWater(); // 공통 단계
        brew();      // 자식 클래스가 구현할 단계
        pourInCup(); // 공통 단계
        addCondiments(); // 자식 클래스가 구현할 단계
    }
    
    // 자식 클래스에서 오버라이딩 할 부분 (추상 메서드)
    abstract void brew();
    abstract void addCondiments();

    // 모든 자식 클래스가 공통으로 사용하는 부분
    private void boilWater() {
        System.out.println("물 끓이기");
    }
    private void pourInCup() {
        System.out.println("컵에 따르기");
    }
}


// --- 2. 템플릿의 특정 단계를 구현하는 자식 클래스들 ---
class Coffee extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("필터로 커피 내리기");
    }
    @Override
    void addCondiments() {
        System.out.println("설탕과 우유 추가하기");
    }
}

class Tea extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("티백 우려내기");
    }
    @Override
    void addCondiments() {
        System.out.println("레몬 추가하기");
    }
}


// --- 3. 실행 ---
public class Main {
    public static void main(String[] args) {
        System.out.println("--- 커피 만들기 ---");
        CaffeineBeverage coffee = new Coffee();
        coffee.prepareRecipe();

        System.out.println("\n--- 차 만들기 ---");
        CaffeineBeverage tea = new Tea();
        tea.prepareRecipe();
    }
}

비지터 패턴(Visitor 패턴)

객체 구조의 요소에 대한 처리(연산)를 해당 요소의 클래스에서 분리하여 방문자(Visitor) 클래스에서 정의한다.

목적: 기존 객체 구조를 변경하지 않고 새로운 기능을 추가할 수 있다.

 
패턴 적용 전 코드
// 각 상품 클래스
class Book {
    private int price;
    public Book(int price) { this.price = price; }
    
    // 기능이 추가될 때마다 이 클래스에 메서드를 계속 추가해야 함
    public int getPrice() { return price; }
    public void pack() { System.out.println("책을 포장합니다."); }
}

class Fruit {
    private int pricePerKg;
    private int weight;
    public Fruit(int pricePerKg, int weight) { this.pricePerKg = pricePerKg; this.weight = weight; }
    
    public int getPrice() { return pricePerKg * weight; }
    public void pack() { System.out.println("과일을 포장합니다."); }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        Object[] items = { new Book(10000), new Fruit(2000, 3) };
        
        int total = 0;
        for (Object item : items) {
            if (item instanceof Book) {
                total += ((Book) item).getPrice();
                ((Book) item).pack();
            } else if (item instanceof Fruit) {
                total += ((Fruit) item).getPrice();
                ((Fruit) item).pack();
            }
        }
        System.out.println("총 가격: " + total);
    }
}
 
코드
// --- 1. 방문자(Visitor) 인터페이스 ---
// 각 상품 종류별로 방문 메서드를 정의
interface ItemVisitor {
    void visit(Book book);
    void visit(Fruit fruit);
}

// --- 2. 방문을 받을 요소(Element) 인터페이스 ---
interface ItemElement {
    void accept(ItemVisitor visitor);
}


// --- 3. 구체적인 요소 클래스들 ---
class Book implements ItemElement {
    public int price;
    public Book(int price) { this.price = price; }
    
    @Override
    public void accept(ItemVisitor visitor) {
        visitor.visit(this); // 방문자를 받아들여 자신을 방문하게 함
    }
}
class Fruit implements ItemElement {
    public int pricePerKg;
    public int weight;
    public Fruit(int pricePerKg, int weight) { this.pricePerKg = pricePerKg; this.weight = weight; }
    
    @Override
    public void accept(ItemVisitor visitor) {
        visitor.visit(this);
    }
}


// --- 4. 구체적인 방문자 클래스들 (기능별로 분리) ---
class ShoppingCartVisitor implements ItemVisitor {
    private int total = 0;
    
    public int getTotalPrice() { return total; }
    
    @Override
    public void visit(Book book) {
        total += book.price;
        System.out.println("책을 카트에 담았습니다.");
    }
    @Override
    public void visit(Fruit fruit) {
        total += fruit.pricePerKg * fruit.weight;
        System.out.println("과일을 카트에 담았습니다.");
    }
}


// --- 5. 실행 ---
public class Main {
    public static void main(String[] args) {
        ItemElement[] items = { new Book(10000), new Fruit(2000, 3) };
        
        ShoppingCartVisitor cartVisitor = new ShoppingCartVisitor();
        for (ItemElement item : items) {
            item.accept(cartVisitor); // 각 아이템이 방문자를 받아들임
        }
        
        System.out.println("총 가격: " + cartVisitor.getTotalPrice());
    }
}

 

'cs > 객체지향' 카테고리의 다른 글

객체의 특징  (0) 2025.03.18
객체지향에서의 역할, 책임, 협력  (2) 2025.03.17
'cs/객체지향' 카테고리의 다른 글
  • 객체의 특징
  • 객체지향에서의 역할, 책임, 협력
khw7385
khw7385
khw7385 님의 블로그 입니다.
  • khw7385
    khw7385 님의 블로그
    khw7385
  • 전체
    오늘
    어제
    • 분류 전체보기 (43)
      • 코딩테스트 (7)
      • 자바 (3)
      • 스프링 (3)
      • cs (7)
        • 자료구조 (3)
        • 알고리즘 (1)
        • 객체지향 (3)
      • 개발일지 (6)
        • 트러블슈팅 (1)
      • 데이터베이스 (3)
        • Redis (2)
        • MySQL (1)
      • 기타 (2)
      • devops (6)
      • LG CNS AM INSPIRE (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
khw7385
디자인 패턴
상단으로

티스토리툴바