실무에서 활용되는 디자인 패턴 소개

싱글톤 패턴

싱글톤 패턴은 자바에서 가장 많이 사용되는 디자인 패턴 중 하나입니다. 이 패턴은 특정 클래스의 인스턴스가 단 하나만 생성되도록 보장하고, 모든 클라이언트에서 동일한 인스턴스를 사용할 수 있도록 합니다.

예시 코드


public class Singleton {
    private static Singleton instance;
    
    private Singleton() {
        // 생성자는 private로 선언하여 외부에서 직접 인스턴스 생성을 막습니다.
    }
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    // 싱글톤 클래스의 나머지 로직과 메서드들을 작성합니다.
}

싱글톤 패턴은 getInstance() 메서드를 통해 단일 인스턴스를 반환하고, 인스턴스가 없으면 새로 생성하는 방식으로 동작합니다. 이를 통해 어디서든 동일한 인스턴스에 접근할 수 있게 됩니다. 이때, private 생성자로 외부에서 인스턴스 생성을 막아줍니다.

위의 예시 코드에서는 Singleton 클래스를 싱글톤으로 구현하였습니다. 이제 어디서든 Singleton.getInstance()를 호출하여 단일 인스턴스에 접근할 수 있게 됩니다.


팩토리 메서드 패턴

팩토리 메서드 패턴은 객체를 생성하는 인터페이스를 정의하고, 객체의 인스턴스화를 서브 클래스에게 위임하는 패턴입니다. 이를 통해 객체 생성의 변화에 유연하게 대처할 수 있으며, 클라이언트 코드의 수정 없이도 새로운 객체를 생성할 수 있습니다.

예시 코드


public abstract class Animal {
    public abstract void makeSound();
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

public interface AnimalFactory {
    Animal createAnimal();
}

public class DogFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}

public class CatFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Cat();
    }
}

위의 예시 코드에서 Animal은 추상 클래스로, makeSound()라는 추상 메서드를 정의하고 있습니다. Dog와 Cat 클래스는 Animal을 상속받아 각각 자신의 소리를 출력하는 makeSound() 메서드를 구현하고 있습니다.

AnimalFactory는 Animal 객체를 생성하는 인터페이스로, createAnimal() 메서드를 정의하고 있습니다. 이를 구현한 DogFactory와 CatFactory는 각각 Dog와 Cat 객체를 생성하여 반환하는 로직을 구현하고 있습니다.

이제 클라이언트는 AnimalFactory를 통해 동물 객체를 생성할 수 있습니다. 클라이언트가 고양이를 생성하려면 CatFactory의 createAnimal() 메서드를 호출하면 됩니다.


추상 팩토리 패턴

추상 팩토리 패턴은 서로 관련된 객체들을 생성하기 위한 인터페이스를 제공하는 패턴입니다. 이 패턴은 클라이언트가 구체적인 클래스를 명시하지 않고도 서로 연관된 객체들을 생성할 수 있게 해줍니다. 또한, 새로운 종류의 관련 객체들을 추가하기 쉽게 만들어줍니다.

예시 코드


// Abstract Factory
public interface AbstractPhoneFactory {
    SmartPhone createSmartPhone();
    FeaturePhone createFeaturePhone();
}

// Concrete Factory 1
public class SamsungPhoneFactory implements AbstractPhoneFactory {
    @Override
    public SmartPhone createSmartPhone() {
        return new SamsungSmartPhone();
    }
    
    @Override
    public FeaturePhone createFeaturePhone() {
        return new SamsungFeaturePhone();
    }
}

// Concrete Factory 2
public class ApplePhoneFactory implements AbstractPhoneFactory {
    @Override
    public SmartPhone createSmartPhone() {
        return new AppleSmartPhone();
    }
    
    @Override
    public FeaturePhone createFeaturePhone() {
        return new AppleFeaturePhone();
    }
}

// Abstract Product
public interface SmartPhone {
    void makeCall();
    void sendText();
}

// Concrete Product 1
public class SamsungSmartPhone implements SmartPhone {
    @Override
    public void makeCall() {
        System.out.println("Samsung 스마트폰으로 전화를 건다.");
    }
    
    @Override
    public void sendText() {
        System.out.println("Samsung 스마트폰으로 문자를 보낸다.");
    }
}

// Concrete Product 2
public class AppleSmartPhone implements SmartPhone {
    @Override
    public void makeCall() {
        System.out.println("Apple 스마트폰으로 전화를 건다.");
    }
    
    @Override
    public void sendText() {
        System.out.println("Apple 스마트폰으로 문자를 보낸다.");
    }
}

// Abstract Product
public interface FeaturePhone {
    void makeCall();
    void sendText();
}

// Concrete Product 1
public class SamsungFeaturePhone implements FeaturePhone {
    @Override
    public void makeCall() {
        System.out.println("Samsung 피처폰으로 전화를 건다.");
    }
    
    @Override
    public void sendText() {
        System.out.println("Samsung 피처폰으로 문자를 보낸다.");
    }
}

// Concrete Product 2
public class AppleFeaturePhone implements FeaturePhone {
    @Override
    public void makeCall() {
        System.out.println("Apple 피처폰으로 전화를 건다.");
    }
    
    @Override
    public void sendText() {
        System.out.println("Apple 피처폰으로 문자를 보낸다.");
    }
}

위의 예시 코드에서 AbstractPhoneFactory는 추상 팩토리로, createSmartPhone()과 createFeaturePhone() 메서드를 정의하고 있습니다. 이를 구현한 SamsungPhoneFactory와 ApplePhoneFactory는 각각 Samsung 스마트폰 및 피처폰, Apple 스마트폰 및 피처폰을 생성하는 로직을 구현하고 있습니다.

SmartPhone과 FeaturePhone은 각각 추상 제품으로, makeCall()과 sendText() 메서드를 정의하고 있습니다. 이를 구현한 SamsungSmartPhone, AppleSmartPhone, SamsungFeaturePhone, AppleFeaturePhone은 각각 해당 제품들의 기능들을 구현하고 있습니다.

이제 클라이언트는 AbstractPhoneFactory를 통해 스마트폰과 피처폰을 생성할 수 있습니다. 클라이언트가 삼성 스마트폰을 생성하려면 SamsungPhoneFactory의 createSmartPhone() 메서드를 호출하면 됩니다.

추상 팩토리 패턴은 다양한 종류의 제품을 생성하거나 다양한 제품군을 구축하기에 유용합니다.


빌더 패턴

빌더 패턴은 복잡한 객체의 생성 과정을 추상화하여 객체를 단계별로 생성할 수 있는 패턴입니다. 이 패턴은 객체의 생성 과정을 개별적인 메서드로 나누어 표현하고, 필요한 메서드들을 호출하여 객체를 구성합니다.

빌더 패턴은 가독성이 좋고, 코드를 이해하기 쉽게 만들어줍니다. 또한, 객체 생성의 유연성과 확장성을 제공하여 동일한 빌더를 사용하여 다양한 종류의 객체를 생성할 수 있게 합니다.

예시 코드


// Product
public class Pizza {
    private String dough;
    private String sauce;
    private String topping;
    
    public void setDough(String dough) {
        this.dough = dough;
    }
    
    public void setSauce(String sauce) {
        this.sauce = sauce;
    }
    
    public void setTopping(String topping) {
        this.topping = topping;
    }
    // ...
}

// Abstract Builder
public abstract class PizzaBuilder {
    protected Pizza pizza;
    
    public Pizza getPizza() {
        return pizza;
    }
    
    public void createNewPizza() {
        pizza = new Pizza();
    }
    
    public abstract void buildDough();
    public abstract void buildSauce();
    public abstract void buildTopping();
}

// Concrete Builder
public class HawaiianPizzaBuilder extends PizzaBuilder {
    @Override
    public void buildDough() {
        pizza.setDough("cross");
    }
    
    @Override
    public void buildSauce() {
        pizza.setSauce("mild");
    }
    
    @Override
    public void buildTopping() {
        pizza.setTopping("ham+pineapple");
    }
}

// Concrete Builder
public class SpicyPizzaBuilder extends PizzaBuilder {
    @Override
    public void buildDough() {
        pizza.setDough("pan baked");
    }
    
    @Override
    public void buildSauce() {
        pizza.setSauce("hot");
    }
    
    @Override
    public void buildTopping() {
        pizza.setTopping("pepperoni+salami");
    }
}

// Director
public class PizzaDirector {
    private PizzaBuilder pizzaBuilder;
    
    public void setPizzaBuilder(PizzaBuilder builder) {
        pizzaBuilder = builder;
    }
    
    public Pizza getPizza() {
        return pizzaBuilder.getPizza();
    }
    
    public void constructPizza() {
        pizzaBuilder.createNewPizza();
        pizzaBuilder.buildDough();
        pizzaBuilder.buildSauce();
        pizzaBuilder.buildTopping();
    }
}

위의 예시 코드에서 Pizza는 생성할 객체입니다. 이 객체는 속성들을 가지고 있고, 해당 속성들을 설정하기 위한 set 메서드들을 제공합니다.

PizzaBuilder는 추상 빌더로, getPizza 메서드, createNewPizza 메서드, buildDough 메서드, buildSauce 메서드, buildTopping 메서드를 정의하고 있습니다.

HawaiianPizzaBuilder와 SpicyPizzaBuilder는 구체적인 빌더로, 각각의 메서드들을 구현하여 Pizza 객체의 속성들을 설정합니다.

PizzaDirector는 빌더를 사용하여 Pizza 객체를 생성하는 역할을 합니다. setPizzaBuilder 메서드로 사용할 빌더를 설정하고, constructPizza 메서드를 호출하여 Pizza 객체를 생성합니다.

클라이언트는 PizzaDirector를 통해 원하는 종류의 빌더를 설정한 후, constructPizza 메서드를 호출하여 해당 종류의 Pizza를 생성할 수 있습니다. getPizza 메서드를 호출하여 생성된 Pizza 객체를 가져올 수도 있습니다.

빌더 패턴은 객체의 생성 과정을 유연하게 관리하고, 클라이언트 코드에서 복잡한 객체 생성 로직을 캡슐화할 수 있어 유지보수성과 가독성을 개선합니다.


프로토타입 패턴

프로토타입 패턴은 기존의 객체를 복제하여 새로운 객체를 생성하는 패턴입니다. 이 패턴은 객체의 생성 비용이 높거나 복잡한 경우에 유용하며, 복잡한 초기화 과정을 거친 객체를 단순히 복제하여 새로운 객체를 생성할 수 있습니다.

프로토타입 패턴은 새로운 객체를 생성하는 과정을 추상화하고, 객체를 복제하는 메서드를 제공하여 객체 복제를 쉽게할 수 있도록 합니다.

예시 코드


// Prototype
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;
    }
    
    @Override
    protected Object clone() {
        Object clone = null;
        try {
            clone = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

// ConcretePrototype
public class Rectangle extends Shape {
    public Rectangle() {
        type = "Rectangle";
    }
    
    @Override
    public void draw() {
        System.out.println("Inside Rectangle::draw() method.");
    }
}

// ConcretePrototype
public class Circle extends Shape {
    public Circle() {
        type = "Circle";
    }
    
    @Override
    public void draw() {
        System.out.println("Inside Circle::draw() method.");
    }
}

// Prototype Manager
public class ShapeCache {
    private static Map shapeMap = new HashMap<>();
    
    public static Shape getShape(String shapeId) {
        Shape cachedShape = shapeMap.get(shapeId);
        return (Shape) cachedShape.clone();
    }
    
    public static void loadCache() {
        Circle circle = new Circle();
        circle.setId("1");
        shapeMap.put(circle.getId(), circle);
        
        Rectangle rectangle = new Rectangle();
        rectangle.setId("2");
        shapeMap.put(rectangle.getId(), rectangle);
    }
}

위의 예시 코드에서 Shape은 프로토타입으로 추상 클래스로 정의되고, Cloneable 인터페이스를 구현합니다. 이 클래스는 draw 메서드와 getType 메서드, setId 메서드를 제공합니다. 또한, clone 메서드를 정의하여 객체를 복제할 수 있게 합니다.

Rectangle과 Circle은 구체적인 프로토타입으로, Shape 클래스를 상속받고 type 값을 설정합니다. draw 메서드를 구현하여 해당 도형의 그리기 동작을 수행합니다.

ShapeCache는 프로토타입 객체를 관리하는 매니저 역할을 합니다. HashMap을 사용하여 프로토타입 객체들을 저장하고, getShape 메서드를 통해 해당 id에 맞는 복제된 프로토타입 객체를 반환합니다. loadCache 메서드를 통해 초기에 사용할 프로토타입 객체들을 미리 생성하여 저장합니다.

클라이언트는 ShapeCache를 통해 프로토타입 객체를 얻어올 수 있고, 이를 이용하여 새로운 객체를 생성할 수 있습니다.

프로토타입 패턴은 객체 생성 비용이 높거나 복잡한 경우에 유용하며, 객체를 복제하여 다양한 종류의 객체를 생성할 수 있도록 합니다.


어댑터 패턴

어댑터 패턴은 한 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하는 패턴입니다. 즉, 호환되지 않는 인터페이스를 가진 두 개의 클래스를 함께 동작할 수 있도록 중간에 어댑터 클래스를 두는 것입니다.

어댑터 패턴은 기존의 코드를 수정하지 않고도 인터페이스를 적용하고 재사용할 수 있는 장점이 있습니다. 또한, 호환성이 없는 클래스들을 함께 사용할 수 있으므로 유연성과 확장성을 제공합니다.

예시 코드


// Target Interface
public interface MediaPlayer {
    public void play(String audioType, String fileName);
}

// Adaptee Interface
public interface AdvancedMediaPlayer {
    public void playVlc(String fileName);
    public void playMp4(String fileName);
}

// Adaptee Class
public class VlcPlayer implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }
    
    @Override
    public void playMp4(String fileName) {
        // nothing
    }
}

// Adaptee Class
public class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // nothing
    }
    
    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
}

// Adapter Class
public class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedMediaPlayer;
    
    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMediaPlayer = new Mp4Player();
        }
    }
    
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMediaPlayer.playMp4(fileName);
        }
    }
}

// Concrete Class
public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;
    
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

위의 예시 코드에서 MediaPlayer는 Target 인터페이스로, play 메서드를 정의합니다.

AdvancedMediaPlayer는 Adaptee 인터페이스로, playVlc 메서드와 playMp4 메서드를 정의합니다.

VlcPlayer와 Mp4Player는 Adaptee 클래스로, AdvancedMediaPlayer 인터페이스를 구현합니다.

MediaAdapter는 어댑터 클래스로, MediaPlayer 인터페이스를 구현합니다. 어댑터 클래스는 생성자에서 주어진 audioType에 따라 VlcPlayer 또는 Mp4Player 객체를 생성하고, MediaPlayer의 play 메서드를 호출하여 해당 객체의 playVlc 또는 playMp4 메서드를 호출합니다.

AudioPlayer는 Concrete Class로, MediaPlayer 인터페이스를 구현합니다. play 메서드에서 mp3 파일은 바로 재생하고, vlc 파일과 mp4 파일은 MediaAdapter를 사용하여 처리합니다.

클라이언트는 AudioPlayer를 사용하여 다양한 유형의 오디오 파일을 재생할 수 있습니다. AudioPlayer는 내부적으로 MediaAdapter를 사용하여 vlc 파일과 mp4 파일을 재생합니다.

어댑터 패턴은 호환성 없는 클래스들을 함께 사용할 수 있도록 중간에 어댑터를 두는 방식으로 인터페이스의 호환성을 제공합니다.


브리지 패턴

브리지 패턴은 추상화와 구현부를 분리하여 각각 독립적으로 확장할 수 있도록 하는 패턴입니다. 브리지 패턴은 상속을 통한 확장보다는 구성을 통한 확장을 선호하며, 객체의 구현부를 변경해도 추상화된 인터페이스에는 영향을 주지 않습니다.

브리지 패턴은 다음의 경우에 사용됩니다:
– 추상화와 구현부를 분리하여 독립적으로 확장하고자 할 때
– 추상화된 부분과 구현된 부분을 별도로 다른 방식으로 확장하고자 할 때
– 상속을 통한 확장이 복잡하고 유연하지 않을 때

예시 코드


// Implementor Interface
public interface DrawAPI {
    public void drawCircle(int radius, int x, int y);
}

// Concrete Implementor 1
public class RedCircle implements DrawAPI {
    @Override
    public void drawCircle(int radius, int x, int y) {
        System.out.println("Drawing Circle[ color: red, radius: " + radius + ", x: " + x + ", y: " + y + "]");
    }
}

// Concrete Implementor 2
public class GreenCircle implements DrawAPI {
    @Override
    public void drawCircle(int radius, int x, int y) {
        System.out.println("Drawing Circle[ color: green, radius: " + radius + ", x: " + x + ", y: " + y + "]");
    }
}

// Abstraction
public abstract class Shape {
    protected DrawAPI drawAPI;
    
    protected Shape(DrawAPI drawAPI) {
        this.drawAPI = drawAPI;
    }
    
    public abstract void draw();
}

// Refined Abstraction 1
public class Circle extends Shape {
    private int x, y, radius;
    
    public Circle(int x, int y, int radius, DrawAPI drawAPI) {
        super(drawAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        drawAPI.drawCircle(radius, x, y);
    }
}

// Refined Abstraction 2
public class Square extends Shape {
    private int x, y, side;
    
    public Square(int x, int y, int side, DrawAPI drawAPI) {
        super(drawAPI);
        this.x = x;
        this.y = y;
        this.side = side;
    }
    
    @Override
    public void draw() {
        drawAPI.drawSquare(side, x, y);
    }
}

위의 예시 코드에서 DrawAPI는 Implementor 인터페이스로, drawCircle 메서드를 정의합니다.

RedCircle과 GreenCircle은 Concrete Implementor 클래스로, DrawAPI 인터페이스를 구현하여 drawCircle 메서드를 구현합니다.

Shape은 Abstraction 추상 클래스로, DrawAPI를 멤버 변수로 가지며 생성자에서 초기화합니다. draw 메서드를 추상 메서드로 가지고 있습니다.

Circle과 Square는 Refined Abstraction 클래스로, Shape을 상속받고 draw 메서드를 구현합니다. draw 메서드에서 drawAPI를 호출하여 실제 도형을 그립니다.

클라이언트는 다양한 타입의 도형을 생성할 수 있으며, 여러 색상으로 그릴 수 있습니다. DrawAPI는 구현된 부분으로, 여러 색상의 원을 그릴 수 있고, Shape은 추상화된 부분으로, 여러 도형 타입에 대해 추상 인터페이스를 제공합니다.

브리지 패턴을 사용하여 추상화와 구현부를 분리함으로써 독립적으로 확장 가능한 구조를 구축할 수 있습니다.


컴포지트 패턴

컴포지트 패턴은 객체들을 트리 구조로 구성하여 하나의 객체와 복합 객체를 동일한 방식으로 다룰 수 있는 패턴입니다. 즉, 개별 객체와 복합 객체를 동일한 인터페이스로 다룰 수 있습니다.

컴포지트 패턴은 다음의 경우에 사용됩니다:
– 객체들을 트리 구조로 구성하고자 할 때
– 개별 객체와 복합 객체를 동일한 방식으로 다루고자 할 때
– 클라이언트가 개별 객체와 복합 객체를 동일하게 처리하고자 할 때
– 객체들의 계층 구조를 표현하고 접근하는 방식을 일관성 있게 유지하고자 할 때

예시 코드


// Component
public interface Employee {
    public void showDetails();
}

// Leaf
public class Developer implements Employee {
    private String name;
    private String position;
    private double salary;
    
    public Developer(String name, String position, double salary) {
        this.name = name;
        this.position = position;
        this.salary = salary;
    }
    
    @Override
    public void showDetails() {
        System.out.println("Name: " + name + ", Position: " + position + ", Salary: " + salary);
    }
}

// Leaf
public class Manager implements Employee {
    private String name;
    private String position;
    private double salary;
    
    public Manager(String name, String position, double salary) {
        this.name = name;
        this.position = position;
        this.salary = salary;
    }
    
    @Override
    public void showDetails() {
        System.out.println("Name: " + name + ", Position: " + position + ", Salary: " + salary);
    }
}

// Composite
public class Team implements Employee {
    private List members = new ArrayList<>();
    
    public void addMember(Employee member) {
        members.add(member);
    }
    
    public void removeMember(Employee member) {
        members.remove(member);
    }
    
    @Override
    public void showDetails() {
        for (Employee member : members) {
            member.showDetails();
        }
    }
}

위의 예시 코드에서 Employee는 Component 인터페이스로, showDetails 메서드를 정의합니다.

Developer와 Manager는 Leaf 클래스로, Employee 인터페이스를 구현하여 showDetails 메서드를 구현합니다.

Team은 Composite 클래스로, Employee 인터페이스를 구현하며 Employee 객체들을 리스트로 관리합니다. addMember 메서드와 removeMember 메서드로 멤버를 추가하고 제거할 수 있으며, showDetails 메서드에서는 멤버들의 세부정보를 출력합니다.

클라이언트는 개별 개발자 및 매니저 객체뿐만 아니라 팀 객체도 동일한 방식으로 다룰 수 있습니다. 모든 Employee 객체는 동일한 인터페이스를 구현하고 있으므로 개별 객체와 복합 객체를 일관성 있게 처리할 수 있습니다.

컴포지트 패턴은 객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 다룰 수 있는 구조를 제공합니다.


데코레이터 패턴

데코레이터 패턴은 객체의 기능을 동적으로 확장할 수 있는 패턴입니다. 즉, 객체를 래핑함으로써 기능을 추가하거나 수정할 수 있습니다. 데코레이터 패턴은 상속을 사용하지 않고도 객체의 기능을 유연하게 확장할 수 있도록 합니다.

데코레이터 패턴은 다음의 경우에 사용됩니다:
– 객체의 기능을 동적으로 확장하고자 할 때
– 상속을 통한 확장이 복잡하고 유연하지 않을 때
– 객체들을 여러 개 조합하여 새로운 기능을 제공하고자 할 때

예시 코드


// Component
public interface Beverage {
    public String getDescription();
    public double cost();
}

// Concrete Component
public class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "Espresso";
    }
    
    @Override
    public double cost() {
        return 1.99;
    }
}

// Decorator
public abstract class CondimentDecorator implements Beverage {
    protected Beverage decoratedBeverage;
    
    public CondimentDecorator(Beverage decoratedBeverage) {
        this.decoratedBeverage = decoratedBeverage;
    }
    
    @Override
    public String getDescription() {
        return decoratedBeverage.getDescription();
    }
    
    @Override
    public double cost() {
        return decoratedBeverage.cost();
    }
}

// Concrete Decorator 1
public class Milk extends CondimentDecorator {
    public Milk(Beverage decoratedBeverage) {
        super(decoratedBeverage);
    }
    
    @Override
    public String getDescription() {
        return "Milk, " + decoratedBeverage.getDescription();
    }
    
    @Override
    public double cost() {
        return 0.50 + decoratedBeverage.cost();
    }
}

// Concrete Decorator 2
public class Sugar extends CondimentDecorator {
    public Sugar(Beverage decoratedBeverage) {
        super(decoratedBeverage);
    }
    
    @Override
    public String getDescription() {
        return "Sugar, " + decoratedBeverage.getDescription();
    }
    
    @Override
    public double cost() {
        return 0.25 + decoratedBeverage.cost();
    }
}

위의 예시 코드에서 Beverage는 Component 인터페이스로 getDescription과 cost 메서드를 정의합니다.

Espresso는 Concrete Component 클래스로, Beverage 인터페이스를 구현하여 getDescription 메서드와 cost 메서드를 구현합니다.

CondimentDecorator는 Decorator 추상 클래스로, Beverage 인터페이스를 구현하며 decoratedBeverage라는 멤버 변수를 가지고 있습니다. getDescription 메서드와 cost 메서드를 decoratedBeverage에서 상속받아 구현합니다.

Milk와 Sugar는 Concrete Decorator 클래스로, CondimentDecorator를 상속받고 Beverage 객체를 인자로 받는 생성자를 가지고 있습니다. getDescription 메서드에서는 조합한 재료를 추가하여 반환하고, cost 메서드에서는 해당 재료의 가격을 더하여 반환합니다.

클라이언트는 여러 종류의 음료에 마일크와 설탕 재료를 추가할 수 있으며, 데코레이터 패턴을 사용하여 동적으로 음료의 기능을 확장할 수 있습니다.

데코레이터 패턴은 객체에 기능을 동적으로 추가하거나 수정할 수 있는 유연성을 제공합니다.


퍼사드 패턴

퍼사드 패턴은 복잡한 서브시스템을 단순화된 인터페이스를 제공하는 패턴입니다. 즉, 서브시스템을 래핑하여 클라이언트에게 간단한 인터페이스를 제공하고, 내부의 복잡한 구현은 감추는 역할을 합니다. 퍼사드 패턴은 클라이언트의 코드를 단순화하고, 시스템의 유지보수성을 향상시키는데 도움을 줍니다.

퍼사드 패턴은 다음의 경우에 사용됩니다:
– 복잡한 서브시스템을 단순화하여 외부에 노출되는 인터페이스를 제공하고자 할 때
– 서브시스템의 복잡한 의존 관계를 숨기고자 할 때
– 서브시스템과 클라이언트 간의 의존성을 낮추고자 할 때

예시 코드


// Subsystem 1
public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
    
    public void stop() {
        System.out.println("Engine stopped");
    }
}

// Subsystem 2
public class AirConditioner {
    public void turnOn() {
        System.out.println("Air conditioner turned on");
    }
    
    public void turnOff() {
        System.out.println("Air conditioner turned off");
    }
}

// Subsystem 3
public class Lights {
    public void turnOn() {
        System.out.println("Lights turned on");
    }
    
    public void turnOff() {
        System.out.println("Lights turned off");
    }
}

// Facade
public class Car {
    private Engine engine;
    private AirConditioner airConditioner;
    private Lights lights;
    
    public Car() {
        engine = new Engine();
        airConditioner = new AirConditioner();
        lights = new Lights();
    }
    
    public void startCar() {
        engine.start();
        airConditioner.turnOn();
        lights.turnOn();
        System.out.println("Car started");
    }
    
    public void stopCar() {
        engine.stop();
        airConditioner.turnOff();
        lights.turnOff();
        System.out.println("Car stopped");
    }
}

위의 예시 코드에서 Engine, AirConditioner, Lights는 각각 서브시스템을 나타내는 클래스로 각각의 기능을 제공합니다.

Car는 퍼사드(Facade) 클래스로, Engine, AirConditioner, Lights를 멤버 변수로 가지고 있으며, 클라이언트에게 단순화된 인터페이스를 제공합니다. startCar 메서드에서는 Engine, AirConditioner, Lights의 메서드를 순서대로 호출하여 자동차의 시동을 걸고, stopCar 메서드에서는 역순으로 호출하여 자동차를 멈춥니다.

클라이언트는 Car 클래스의 인스턴스를 생성하여 startCar 메서드와 stopCar 메서드를 호출함으로써 자동차를 간단하게 제어할 수 있습니다. 이를 통해 서브시스템의 복잡성을 숨기고 클라이언트의 코드를 간소화시킬 수 있습니다.

퍼사드 패턴은 복잡한 서브시스템을 단순화된 인터페이스로 제공하여 클라이언트의 코드를 단순화하고, 시스템을 유지보수하기 쉽게 만듭니다.


Leave a Comment