디자인패턴 Iteratoer Pattern

이터레이터 패턴은 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공해 줍니다.

 

Iteratoer Pattern 어떻게 구현할까요?


이터레이터 패턴은 Iterator라는 인터페이스에 의존합니다.
이터레이터 패턴은 이용하면 내부적인 구현 방법을 외부로 노출시키지 않으면서도 집합체에 있는 모든 항목에 일일이 접근할 수 있습니다.
또한 각 항목에 일일이 접근할 수 있게 해주는 기능을 집합체가 아닌 반복자 객체에서 책임지게 된다는 것도 장점으로 작용합니다.
그러면 집합체 인터페이스 및 구현이 간단해지고, 각자 중요한 일만 처리하면 됩니다.

hasNext와 next라는 함수만 노출시킵니다.
이 함수를 사용하는 사용자는 hasNext가 boolean으로 되어있으니 다음 데이터가 있는지 여부를 반환하는 함수라고 예측 가능하고, Next는 MenuItem이라는 객체를 반환하니 객체에 담겨있는 다음 데이터를 보여주는구나라고 예측할 수 있습니다.
하지만 내부적인 구현 방법은 모릅니다.
이렇게 구현 방법을 외부로 노출시키지 않고 기능을 제공할 수 있습니다.

 

구현

Iterator

package cg.park.designpattern.iterator;

public interface Iterator {
   boolean hasNext();
   MenuItem next();
}

 

WashMenuIterator

package cg.park.designpattern.iterator;

public class WashMenuIterator implements Iterator {
    MenuItem[] items;
    int position = 0;

    public WashMenuIterator(MenuItem[] items) {this.items = items;}

    public MenuItem next() {return items[position++];}
    public boolean hasNext() {return items.length > position;}

}

 

Menu

package cg.park.designpattern.iterator;

public interface Menu {
   public Iterator createIterator();
}

 

WashMenu

package cg.park.designpattern.iterator;

public class WashMenu implements Menu {
    static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;

    public WashMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("카 샴푸");
        addItem("세차용 스폰지");
        addItem("휠 브러시 사용");
        addItem("물통에 스폰지 세척");
        addItem("고압수 뿌리기");
        addItem("타월을 이용하여 물기 제거");
    }

    public void addItem(String description) {
        MenuItem washItem = new MenuItem(description);
        if (numberOfItems >= MAX_ITEMS) {
            System.out.println("최대 개수를 초과했습니다.");
            return;
        }
        menuItems[numberOfItems] = washItem;
        numberOfItems++;
    }

    public MenuItem[] getMenuItems() {
        return menuItems;
    }

    public Iterator createIterator() {
        return new WashMenuIterator(menuItems);
    }

}

 

MenuItem

package cg.park.designpattern.iterator;

public class MenuItem {
    String description;
    public MenuItem(String description) {
        this.description = description;
    }
    public String getDescription() {
        return description;
    }
    public String toString() { return description; }
}

 

Cleaner

package cg.park.designpattern.iterator;

public class Cleaner {
    Menu washMenu;

    public Cleaner(Menu washMenu) {
        this.washMenu = washMenu;
    }

    public void cleaning() {
        System.out.println("==========청소 시작==========");
        printCleaning(washMenu.createIterator());
        System.out.println("==========청소 끝==========");
    }

    private void printCleaning(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = iterator.next();
            System.out.println(menuItem.description);
        }
    }
}

 

IteratorTest

package cg.park.designpattern.iterator;

public class IteratorTest {
    public static void main(String[] args) {
        Cleaner cleaner = new Cleaner(new WashMenu());
        cleaner.cleaning();
    }
}

 

후기

내부적인 구현 방법을 외부로 노출시키지 않으면서도 집합체에 있는 모든 항목에 일일이 접근할 수 있는 방법은 유용한 기술입니다.

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

디자인패턴 TemplateMethod Pattern

템플릿 메서도 패턴은 메서드에서 알고리즘의 골격을 정의합니다.
알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있습니다.
템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의 할 수 있습니다.

 

TemplateMethod Pattern 실제로 어떻게 적용할까요?

버거 레시피를 만들 예정입니다.
버거에 들어가는 재료들을 확인하는 기능을 만들겠습니다.

각각의 레시피에 조금 다른 부분이 있긴 하지만, 만드는 방법은 똑같다고 볼 수 있기 때문에, 다른 부분을 추상화 시켜 공유하기로 했습니다.

 

다른 부분을 찾아서 추상화를 했습니다.
이 클래스는 버거 레시피의 알고리즘 틀입니다.

 

알고리즘 틀

템플릿 메소드 패턴은 간단하게 생각하면 알고리즘의 틀을 만들기 위한 패턴입니다.
틀이란 일련의 단계들로 알고리즘을 정의한 메소드입니다.
일련의 단계들 중 하나 이상이 추상 메소드로 정의되며, 그 추상 메소드는 서브클래스에서 구현됩니다.
이렇게 하면 서브클래스에서 달라지는 부분을 구현할 수 있도록 하면서도 알고리즘의 구조는 바꾸지 않아도 됩니다.

 

달라지는 부분은 어떻게 구현할까요?

템플릿 메소드에서는 알고리즘의 각 단계들을 정의하며, 그 중 한 개 이상의 단계가 서브클래스에 의해  제공될 수 있습니다.

버거 레시피 추상클래스가 있고, 그 레시피를 상속받은 치킨버거와 불고기버거 클래스가 있습니다. 
서브클래스를 구현하여 다른 부분에 대해 정의하면 하겠습니다.

 

구현

BurgerBeverage

package cg.park.designpattern.templateMethod;

public abstract class BurgerBeverage {

    final void makeBurger() {
        menu();
        bread();
        patty();
        onion();
        sauce();
        cheese();
    }

    abstract void menu();//메뉴

    abstract void patty();//패티(돼지,닭,소 등...)

    abstract void sauce();//버거 소스

    void bread() { System.out.println("모닝빵");}

    void onion() { System.out.println("양파");}

    void cheese() { System.out.println("치즈");}

}

 

ChickenBurger

package cg.park.designpattern.templateMethod;

public class ChickenBurger extends BurgerBeverage {
    public void menu() { System.out.println("*치킨 버거");}
    public void patty() { System.out.println("치킨 패티");}
    public void sauce() { System.out.println("양념치킨소스");}
}

 

BulgogiBurger

package cg.park.designpattern.templateMethod;

public class BulgogiBurger extends BurgerBeverage {
    public void menu() { System.out.println("*불고기 버거");}
    public void patty() { System.out.println("불고기 패티");}
    public void sauce() { System.out.println("불고기 소스");}
}

 

TemplateMethodTest

package cg.park.designpattern.templateMethod;

public class TemplateMethodTest {
    public static void main(String[] args) {
        BulgogiBurger bulgogiBurger = new BulgogiBurger();
        ChickenBurger chickenBurger = new ChickenBurger();

        bulgogiBurger.makeBurger();
        System.out.println("==========================");
        chickenBurger.makeBurger();

    }
}

 

후기

템플릿 메서드 패턴은 실무에서도 많이 사용되는 패턴입니다.
디자인 패턴에 대해 학습하고, 신규로 접하는 프로젝트가 디자인 패턴이 적용되어 있다면 코드 확장에 필요한 지식이 될 것입니다.

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

디자인패턴 Facade Pattern

퍼사드 클래스에서는 서브시스템 클래스들을 캡슐화하지 않습니다.
그냥 서브시스템의 기능을 사용할 수 있는 간단한 인터페이스를 제공할 뿐입니다.

 

Facade Pattern은 실제로 어떻게 적용할까요?

어떤 일을 하기 위해서는 작업 순서가 있습니다.
하지만 그 작업 순서를 하나씩 동작하기엔 너무 복잡하지 않을까요?
복잡한 작업 순서들을 하나로 묶어서 사용할 수 있게 된다면 편리하지 않을까요?
이런 경우에 퍼사드 패턴을 사용하면 됩니다.
퍼사드 클래스를 구현함으로써 복잡한 시스템을 훨씬 쉽게 사용할 수 있습니다.
퍼사드는 인터페이스를 단순화시킬 뿐 아니라 클라이언트와 구성요소들로 이루어진 서브시스템을 분리시키는 역할도 합니다.

 

세차하는 시스템을 퍼사드로 구현

세차를 할 때 작업 순서가 있습니다.
 -카 샴푸 사용
 -세차용 스펀지로 닦기
 -물통에 세차용 스펀지 세척하기
 -휠 브러시 사용하기
 -고압수 뿌리기
 -타월을 사용하여 물기 제거하기

 

 

이 작업 순서를 매번 하나씩 동작한다면 번거롭기도 하고, 복잡하기도 합니다.
더 간편한 방법이 있을지 찾아봅니다.
그래서 위의 작업 순서들을 하나로 묶기로 했습니다.

 

세차를 할 때 복잡하던 작업 순서들을 하나로 묶었습니다.
기존에는 각 클래스를 하나씩 동작해야 했다면, 이제는 CarWash라는 클래스로 기능들을 묶어서 한 번만 사용하면 됩니다.

 

구현

BubbleBomb

package cg.park.designpattern.facade;

public class BubbleBomb {
    String description;
    public BubbleBomb(String description) {
        this.description = description;
    }

    public void on() {System.out.println(description + " 샴푸 on");}

    public void off() {
        System.out.println(description + " 샴푸 off");
    }

}

 

WashMitt

package cg.park.designpattern.facade;

public class WashMitt {
    String description;

    public WashMitt(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 세차용 스폰지 on");
    }

    public void off() {
        System.out.println(description + " 세차용 스폰지 off");
    }

}

 

WheelBrush

package cg.park.designpattern.facade;

public class WheelBrush {
    String description;

    public WheelBrush(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 휠 브러시 on");
    }

    public void off() {
        System.out.println(description + " 휠 브러시 off");
    }

}

 

Bucket

package cg.park.designpattern.facade;

public class Bucket {
    String description;

    public Bucket(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 물통에 스폰지 세척 on");
    }

    public void off() {
        System.out.println(description + " 물통에 스폰지 세척 off");
    }
}


PressureWasher

package cg.park.designpattern.facade;

public class PressureWasher {
    String description;

    public PressureWasher(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 고압수 뿌리기 on");
    }

    public void off() {
        System.out.println(description + " 고압수 뿌리기 off");
    }

}

 

Towel

package cg.park.designpattern.facade;

public class Towel {
    String description;

    public Towel(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " 타월로 물기 제거 on");
    }

    public void off() {
        System.out.println(description + " 타월로 물기 제거 off");
    }

}

 

CarWash

package cg.park.designpattern.facade;

public class CarWash {
    BubbleBomb bubbleBomb;//카 샴푸
    Bucket bucket;//물통
    PressureWasher pressureWasher;//고압 세척기
    Towel towel;//타월
    WashMitt washMitt;//세차용 스폰지
    WheelBrush wheelBrush;//휠 브러시

    public CarWash(
           BubbleBomb bubbleBomb
         , Bucket bucket
         , PressureWasher pressureWasher
         , Towel towel
         , WashMitt washMitt
         , WheelBrush wheelBrush) {

        this.bucket = bucket;
        this.washMitt = washMitt;
        this.wheelBrush = wheelBrush;
        this.bubbleBomb = bubbleBomb;
        this.towel = towel;
        this.pressureWasher = pressureWasher;
    }

    public void start() {
        bubbleBomb.on();//카 샴푸
        washMitt.on();//세차용 스폰지
        wheelBrush.on();//휠 브러시
        bucket.on();//물통
        pressureWasher.on();//고압수 뿌리기
        towel.on();//타월을 이용하여 물기 제거
    }

    public void end() {
        bubbleBomb.off();
        wheelBrush.off();
        washMitt.off();
        bucket.off();
        pressureWasher.off();
        towel.off();
    }

}

 

FacadeTest

package cg.park.designpattern.facade;

public class FacadeTest {

    public static void main(String[] args) {
        BubbleBomb bubbleBomb = new BubbleBomb("A사 제품");
        Bucket bucket = new Bucket("A사 제품");
        PressureWasher pressureWasher = new PressureWasher("A사 제품");
        Towel towel = new Towel("A사 제품");
        WashMitt washMitt = new WashMitt("A사 제품");
        WheelBrush wheelBrush = new WheelBrush("A사 제품");

        CarWash carWash = new CarWash(
                bubbleBomb
                , bucket
                , pressureWasher
                , towel
                , washMitt
                , wheelBrush);

        carWash.start();//세차 시작
        System.out.println("=================================");
        carWash.end();//세차 종료

    }

}

 

후기

처음에는 퍼사드패턴과 커맨드 패턴이 헷갈렸습니다.
퍼사드 패턴과 커맨드 패턴 둘 다 공부하니 개념적인 차이를 이해했습니다.
퍼사드 패턴: 서브클래스들을 캡슐화하지 않고 구현,  훨씬 쉽게 사용할 수 있는 인터페이스를 제공함으로써 복잡한 시스템을 쉽게 사용.
커맨드 패턴: 요구사항을 객체로 캡슐화, 매개변수로 다른 여러 가지 요구사항 추가 가능.

커맨드 패턴 보러가기

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

디자인패턴 Adapter Pattern

클래스의 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환합니다.
인터페이스가 호환되지 않아 쓸 수 없었던 클래스들을 같이 사용할 수 있게 해줍니다.

 

어댑터란?

어댑터는 그리 어렵지 않게 이해할 수 있습니다.
우리 주변에서 어댑터를 쉽게 볼 수 있습니다.
예를 들어, 한국에서 쓰던 220볼트 어댑터를 해외에 가서 사용하려면 어떻게 해야할까요?
같은 220볼트를 사용하는 국가라면 괜찮지만, 그렇지 않다면?

다들 어댑터가 어떻게 사용되고 있는지 알고 있습니다.
다른 부분을 변환하여 연결시키는 중간역할입니다.

 

 

어댑터 패턴은 실제로 어떻게 적용할까요?

기존 시스템과 신규 시스템이 호환되지 않는다면?

 

어댑터의 역할을 여기서 확인할 수 있습니다.
그림만 봐도 느낌이 오지 않나요?

 

어댑터가 있다면 다른 부분을 변환하여 연결시키는 중간역할을 할 수 있습니다.

 

구현

한국에서 사용하는 220볼트 플러그와 일본에서 사용하는 110볼트 플러그가 있습니다.
220볼트를 어댑터를 사용하여 110볼트를 사용할 수 있게 만들고,
110볼트도 어댑터를 사용하여 220볼트를 사용할 수 있게 만들 예정입니다.

 

Volt220

package cg.park.designpattern.adapter;

public interface Volt220 {
    public void plugIn220();
}

 

usedInKorea

package cg.park.designpattern.adapter;

public class usedInKorea implements Volt220 {
    public void plugIn220() { System.out.println("220볼트 사용");}
}

 

Volt110

package cg.park.designpattern.adapter;

public interface Volt110 {
    public void plugIn110();
}

 

usedInJapan

package cg.park.designpattern.adapter;

public class usedInJapan implements Volt110 {
    public void plugIn110() { System.out.println("110볼트 사용");}
}

 

VoltAdapter220

package cg.park.designpattern.adapter;

public class VoltAdapter220 implements Volt110 {
    Volt220 volt220;

    public VoltAdapter220(Volt220 volt220) {
        this.volt220 = volt220;
    }

    public void plugIn110() {
        volt220.plugIn220();
    }
}

 

VoltAdapter110

package cg.park.designpattern.adapter;

public class VoltAdapter110  implements Volt220 {
    Volt110 volt110;

    public VoltAdapter110(Volt110 volt110) {
        this.volt110 = volt110;
    }

    public void plugIn220() {
        volt110.plugIn110();
    }
}

 

AdapterTest

package cg.park.designpattern.adapter;

public class AdapterTest {
    public static void main(String[] args) {
        Volt110 volt110 = new VoltAdapter220(new usedInKorea());
        Volt220 volt220 = new VoltAdapter110(new usedInJapan());

        volt110.plugIn110();//volt110 사용
        volt220.plugIn220();//volt220 사용

    }

}

 

110볼트를 220볼트 어댑터로 감싸고, 

220볼트를 110볼트 어댑터로 감싼 후

실행하게 되면 위와 같이 결과가 나옵니다.

220어댑터(110볼트): 220볼트 사용

110어댑터(220볼트): 110볼트 사용

 

후기

어댑터의 개념은 많이 사용된다.

코드를 고치지 않고 새로운 라이브러리를 적용할 수 있다는 것은 큰 장점이다.

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

디자인패턴 Command Pattern

커맨드 패턴은 요구사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있습니다.

또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업취소 기능도 지원 가능합니다.

 

커맨드 패턴은 실제로 어떻게 적용할까요?

커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구 사항을 캡슐화한 것입니다.

리시버를 한 객체에 넣어주고, 실행시키기 위한 메서드 하나만 외부에 공개하는 방법을 사용합니다.

이 메소드 호출에 의해 리시버에서 일련의 작업이 처리됩니다.

외부에서는 어떤 객체가 리시버 역할을 하는지 모르고 실제로 어떤 일을 하는지 알 수 없습니다.

실행을 위한 메서드만 호출하면 요구 사항이 처리된다는 것만 알 수 있습니다.

 

Command

package cg.park.designpattern.command;

public interface Command {
    public void execute();
}

 

 

Light ON/OFF

불을 켜고 끄는 Light ON과 Light OFF 라는 기능이 있습니다.

사용자 입장에서는 버튼만 누르면 기능은 실행됩니다.

사용자는 버튼을 눌렀을 때 내부적으로 어떤 일이 일어나는지 모릅니다.

이렇게 요청 내역을 객체로 캡슐화하고, 클라이언트는 버튼 하나로 기능을 제공받을 수 있습니다.

 

구현

Command

package cg.park.designpattern.command;

public interface Command {
    public void execute();
}

 

Light

package cg.park.designpattern.command;

public class Light {

    public Light() {}

    public void on() {System.out.println("Light: ON");}

    public void off() {System.out.println("Light: OFF");}

}

 

LightOff

package cg.park.designpattern.command;

public class LightOff implements Command {
    Light light;

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

    public void execute() {
        light.off();
    }
}

 

LightOn

package cg.park.designpattern.command;

public class LightOn implements Command {
    Light light;

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

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

 

RemoteControl

package cg.park.designpattern.command;

public class RemoteControl {

    public static void main(String[] args) {
        Light light = new Light();
        LightOn lightOn = new LightOn(light);
        LightOff lightOff = new LightOff(light);

        lightOn.execute();//ON
        lightOff.execute();//OFF
    }

}

위의 main 클래스에서 execute를 사용하면 LightON, LightOFF가 실행됩니다.

 

후기

처음에는 Facade Pattern과 뭐가 다른지 궁금했었는데, 직접 자료를 찾아보고 구현해보면서 알게되었습니다.
감사합니다.

퍼사드 패턴 보러가기

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

디자인 패턴 Singleton Pattern 

 

싱글턴 패턴은 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공합니다.

 

인스턴스 생성을 위해서 getInstance()를 호출해야 함.

 

싱글턴 패턴은 실제로 어떻게 적용할까요?

클래스에서 단 하나만 생성할 수 있게 관리합니다.

다른 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 합니다.

인스턴스를 생성하길 원하면 반드시 Singleton 클래스의 getInstance()를 호출해야 합니다.

 

인스턴스 접근을 어디에서도 가능하게 만들어야 합니다.

다른 객체에서 인스턴스가 필요할 때 언제든지 Singleton 클래스에 요청을 할 수 있게 만들고, 요청이 들어오면 getInstance()에서 만들어진 유일한 인스턴스를 반환해줘야 합니다.

 

Singleton Pattern 인스턴스를 이해하려면?

Singleton 인스턴스 사용

Singleton 인스턴스 사용을 유료 서비스 구독이라고 가정해봅니다.

유료 서비스를 이용할 때 구독이 안되어 있다면 최초 이용 시 구독 신청을 하게 됩니다.

구독 신청이 된 후 서비스를 제공받을 수 있습니다.

그리고 다시 서비스를 이용할 경우, 구독이 되어있다면 구독 신청 없이 바로 서비스를 제공받게 됩니다.

이미 구독 중이라면 다시 구독 신청을 할 필요는 없습니다.

 

 

Instance로 바꿔서 생각해보면?

위의 내용들을 Singleton Pattern의 인스턴스로 바꿔서 생각해보면 Singleton은 쉽게 느껴질 것입니다.

유료 서비스: Instance 호출

구독 중: Instance 존재 여부

구독 신청: Instance 생성

서비스 제공: Instance 제공

 

 

Singleton Class

package cg.park.designpattern.singleton;

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

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

}

 

Singleton 인스턴스를 호출 시 getInstance()를 호출해야 합니다.

객체가 Null 인지(생성된 적이 없는지) 확인을 합니다.

그리고 동시에 이 영역에 접근하지 못하게 synchronized를 선언합니다.

그리고 이 찰나에 null이 되었는지 체크를 다시 한 후에

Singleton 인스턴스를 생성 후 반환해줍니다.

Singleton 인스턴스가 이미 생성되었다면 바로 반환해줍니다.

 

synchronized를 제일 앞에 선언하지 않은 이유

synchronized가 있다면 이미 생성된 Singleton 인스턴스라도 모든 접근이 동기화되기 때문입니다.

동기화가 된다는 것은 인스턴스를 사용하기 위해 접근하려면 줄을 서야 하는 것입니다.

점심시간에 인기가 많은 식당을 가게 되면 줄을 서는 것처럼 Singleton 인스턴스를 사용하기 위해서는 줄을 서야 합니다.

하지만 이미 인스턴스가 있고 반환만 해주면 되는 상태라면 굳이 줄을 설 필요가 없습니다.

그래서 제일 위해 null 체크를 한 후, 인스턴스가 없는 경우에만 synchronized를 선언합니다.

synchronized가 선언되었기 때문에 1개의 인스턴스만 만들어지고, 그 후에는 이미 생성된 인스턴스를 반환할 것입니다.

 

구현

Paid라는 클래스를 만들어서 Singleton 인스턴스를 호출합니다.

Paid

package cg.park.designpattern.singleton;

public class Paid {

    public void perpare(String name) {
        System.out.println("유료 서비스 ========== START ==========");
        System.out.println(name +" 접속");
        Singleton.getInstance();
        System.out.println("유료 서비스 ========== E N D ==========");
    }
}

 

Singleton에 접근하면 로그를 남기도록 합니다.

Singleton

package cg.park.designpattern.singleton;

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    System.out.println("결제 후 서비스 제공");
                    singleton = new Singleton();
                    return singleton;
                }
            }
        }
        System.out.println("서비스 제공");
        return singleton;
    }

}

 

Paid를 호출할 Main 클래스를 만듭니다.

Main

package cg.park.designpattern.singleton;

public class Main {

    public static void main(String[] args) {
        Paid paid = new Paid();
        paid.perpare("1년 전");
        paid.perpare("반년 전");
        paid.perpare("현재");
    }

}

 

위의 로그를 보면 첫 번째 접근 시에만 결제 후 서비스를 제공하고 그다음부터는 바로 서비스를 제공합니다.

 

후기

Singleton Pattern은 꼭 필요한 경우가 아니라면 사용하지 않는 것을 추천합니다.

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

Decorator Pattern

 

객체에 추가 요소를 동적으로 더할 수 있습니다.

데코레이터를 사용하면 서브클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.

 


 

설명

 

버거 가게 메뉴를 만드려고 합니다.

버거 단품을 주문할 수 있지만 그 단품에 패티, 치즈 등 재료를 추가할 수도 있습니다.

이렇게 추가 기능을 만들 때 각각의 버거 클래스에 패티 추가, 치즈 추가 등 옵션 기능을 추가해주는 것이 맞을까요?

 

Burger

package cg.park.designpattern.decorator.buger;

public class CheeseBurger extends Burger {
    public CheeseBurger() {
        description = "Basic Burger";
    }//버거 이름
    public double cost() {
        return 6000;
    }//버거 가격
}

 

치즈버거 객체입니다.

현재 구현된 것은 버거 이름과 버거 가격입니다.

옵션 추가 기능을 클래스에 부여한다고 가정해보겠습니다.

 

Burger

package cg.park.designpattern.decorator.buger;

public class CheeseBurger extends Burger {
    public CheeseBurger() {
        description = "Basic Burger";
    }//버거 이름
    public double cost() {
        return 6000;
    }//버거 가격

    //------추가 부분------
    public void addCheese() {};//치즈 추가
    public void addPatty() {};//패티 추가
}

 

치즈 추가와 패티 추가내용을 하단에 만들었습니다.

예를 들어 100개의 버거 클래스가 있다면 모든 클래스에 추가 내용을 만들어줘야 합니다.

그리고 추가하는 옵션이 달라질 때마다 100개의 버거 클래스를 수정해야합니다.

과연 이것이 유지보수에 용이하게 만들어진 것일까요?

이번 Decorator Pattern에서는 옵션의 기능을 따로 분리하여 만들 것입니다.

만들어진 객체 코드를 수정하지 않고 추가 기능을 더할 수 있는 패턴입니다.

 

BurgerOption

package cg.park.designpattern.decorator.bugger;

public abstract class BurgerOption extends Burgger {
    public Burgger burgger;
    public abstract String getDescription();

    public Burgger.Size getSize() {return burgger.getSize();}
}

 

버거의 옵션을 담당할 클래스를 따로 만들어줍니다.

 

 

전체 버거의 구성을 잡아줄 추상 클래스 Burger를 만듭니다.

Burger를 상속받아서 각 버거 클래스를 구현합니다.

BurgerOption 클래스도 만들어줍니다.

BurgerOption 클래스는 각 옵션들의 부모 클래스가 될 것이기 때문에 추상 클래스로 구현합니다.

BurgerOption을 상속받은 각 옵션 클래스들을 구현합니다.

 

구현

 

Burger

package cg.park.designpattern.decorator.bugger;

public abstract class Burger {
    public enum Size { BASIC, LARGE };//버거 사이즈
    Burger.Size size = Size.BASIC;//기본 제공 사이즈
    String description = "Unknown Burger";//버거 설명

    //버거 설명
    public String getDescription() {
        return description;
    }

    //버거 사이즈 변경
    public void setSize(Burger.Size size) {
        this.size = size;
    }

    //버거 사이즈 조회
    public Size getSize() {
        return this.size;
    }

    //버거 가격
    public abstract double cost();

}

 

BasicBurger

package cg.park.designpattern.decorator.bugger;

public class BasicBurger extends Burger {
    
    //버거 설명: 기본 버거
    public BasicBurger() {
        description = "Basic Burger";
    }

    //가격: 5000
    public double cost() {
        return 5000;
    }
}

버거 클래스에 사이즈 변경 및 옵션 기능을 넣지 않았습니다.

버거 외 부가적인 기능은 옵션 클래스로 분리하였습니다.

 

BurgerOption

package cg.park.designpattern.decorator.bugger;

public abstract class BurgerOption extends Burger {
    public Burger burger;//버거 객체 상속
    public abstract String getDescription();//버거 설명

    //버거 사이즈
    public Size getSize() {return burger.getSize();}
}

 

Cheese

package cg.park.designpattern.decorator.bugger;

public class Cheese extends BurgerOption {
    //버거 객체
    public Cheese(Burger burger) {
        this.burger = burger;
    }

    //버거 설명에 Cheese 추가
    public String getDescription() {return burger.getDescription() + ", Cheese";}

    //버거 가격에 Cheese 가격 추가
    public double cost() {return burger.cost() + 500;}
}

 

PcgBurger

package cg.park.designpattern.decorator.bugger;

public class PcgBurger {

    public static void main(String args[]) {
        Burger burger = new BasicBurger();//기본 버거
        System.out.println("[첫 번째 주문] " +
                "\n버거: "+ burger.getDescription()
                +"\n가격: "+ burger.cost()
        );

    }
}

버거 주문 시 결과입니다.

기본 버거뿐 아니라 치즈버거, 치킨버거, 불고기버거와 옵션도 추가해보겠습니다.

 

package cg.park.designpattern.decorator.bugger;

import cg.park.designpattern.decorator.bugger.Burger.Size;

public class PcgBurger {

    public static void main(String args[]) {
        Burger burger = new BasicBurger();//기본 버거
        System.out.println("[첫 번째 주문] " +
                "\n버거: "+ burger.getDescription()
                +"\n가격: "+ burger.cost()
        );

        Burger cheeseBurger = new CheeseBurger();//치즈 버거
        cheeseBurger = new Cheese(cheeseBurger);//치즈 추가
        System.out.println("[두 번째 주문] " +
                "\n버거: "+cheeseBurger.getDescription()
                +"\n가격: "+cheeseBurger.cost()
        );

        Burger bulgogiBurger = new BulgogiBurger();//불고기 버거
        bulgogiBurger.setSize(Size.LARGE);//라지로 변경
        System.out.println("[세 번째 주문] " +
                "\n버거: "+bulgogiBurger.getDescription()
                +"\n가격: "+bulgogiBurger.cost()
        );

        Burger chickenBurger = new ChickenBurger();//치킨 버거
        chickenBurger = new Chicken(chickenBurger);//치킨 추가
        System.out.println("[네 번째 주문] " +
                "\n버거: "+chickenBurger.getDescription()
                +"\n가격: "+chickenBurger.cost()
        );
    }
}

각 주문의 결과를 확인할 수 있습니다.

 


 

후기

 

실무에서 클래스에 기능을 추가하는 일은 많습니다.

기능을 추가할 때 단순하게 생각하면 해당 클래스나 메서드 밑에 기능을 추가하면 됩니다.

하지만 이렇게 만든다면 전체 프로젝트 중 방금 수정한 1개의 클래스를 위해 작업이 진행된 것입니다.

해당 추가 기능에 대해 분석, 설계가 잘 되었다면 다행이지만, 임시 방안으로 만든 내용이라면 추후에 문제가 발생할 수 있습니다.

이런 상황에서 프로젝트에 대한 이해와 기본기가 탄탄하면 보다 좋은 방향으로 기능을 추가할 수 있습니다.

감사합니다.

 

Git: https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

 

Observer Pattern

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.

 


 

설명

 

4남매가 식자재를 판매 창업을 하려고 합니다.

4남매가 각각 창고에 재고관리를 하지 않고, 1개의 창고에 모든 재고를 공유하기로 했습니다.

첫째: 정육점

둘째: 과일가게

셋째: 야채가게

넷째: 종합마트

이렇게 각각 다른 창업을 시작했습니다.

4남매 모두 하나의 창고를 이용해 관리하고, 재고가 변동될 때마다 창고에 의존하고 있는 각 매장에 변동 내용을 알려주고 있습니다.

 

 

정육점에고 고기가 판매된다면 남아있는 재고를 각 매장에 알려주고 고기를 팔아야 할 다른 매장에서도 재고 현황을 확인하고 판매할 수 있습니다.

이렇게 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의하는 것이 "옵저버 패턴"입니다.

 

옵저버 패턴은 모든 옵저버 객체를 관리하고 알림을 대신해줍니다.

N : N 관계가 아닌 1 : N입니다.

각 객체마다 엮여있는 결합도 높은 코드가 아닌 1 : N 관계로 결합도가 낮고 응집성은 높아집니다.

얼마든지 옵저버 객체를 추가할 수 있고, 삭제할 수도 있습니다.

설계된 코드를 변경할 필요도 없습니다.

그리고 각 옵저버 객체의 코드가 바뀌어도 서로에게 영향을 미치지 않습니다.

 


 

구현

 

객체 상태가 바뀌면 동작할 인터페이스를 정의합니다.

바뀐 상태를 갱신시켜주는 update와 그 내역을 보여주는 display를 만듭니다.

 

Observer

package cg.park.designpattern.observer;

public interface Observer {
   public void update(int meat, int fruit, int vegetable);
}

 

DisplayElement

package cg.park.designpattern.observer;

public interface DisplayElement {
   public void display();
}

 

재고의 현재 상태를 알림 받는 기능을 인터페이스로 만듭니다.

생성, 삭제, 알림 


Subject

package cg.park.designpattern.observer;

public interface Subject {
   public void registerObserver(Observer o);
   public void removeObserver(Observer o);
   public void notifyObservers();
}

 

Subject를 상속받아서 재고의 현재 상태를 알림 받는 클래스를 구현합니다.

 

PreorderData

package cg.park.designpattern.observer;

import java.util.*;

public class PreorderData implements Subject {
    private List<Observer> observers;

    private int meat;//고기
    private int fruit;//과일
    private int vegetable;//야채

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

    public void registerObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(meat, fruit, vegetable);
        }
    }

    public void foodMaterialsChange(){
        notifyObservers();
    }

    public void setFoodMaterials(int meat, int fruit, int vegetable){
        this.meat = meat;
        this.fruit = fruit;
        this.vegetable = vegetable;
        foodMaterialsChange();
    }

}

 

그리고 처음에 만든 Observer와 DisplayElement를 상속받아 클래스를 구현합니다.

 

MeatShop

package cg.park.designpattern.observer;

public class MeatShop implements Observer, DisplayElement {

    private int meat;//과일
    private PreorderData preorderData;

    public MeatShop(PreorderData preorderData){
        this.preorderData = preorderData;
        preorderData.registerObserver(this);
    }

    public void update(int meat, int fruit, int vegetable) {
        this.meat = meat;
        display();
    }

    public void display() {
        System.out.println("[정육점] 고기재고: "+meat);
    }
}

 

FruitShop

package cg.park.designpattern.observer;

public class FruitShop implements Observer, DisplayElement {

    private int fruit;//과일
    private PreorderData preorderData;

    public FruitShop(PreorderData preorderData){
        this.preorderData = preorderData;
        preorderData.registerObserver(this);
    }

    public void update(int meat, int fruit, int vegetable) {
        this.fruit = fruit;
        display();
    }

    public void display() {
        System.out.println("[과일가게] 과일재고: "+fruit);
    }

}

 

VegetableShop

package cg.park.designpattern.observer;

public class VegetableShop implements Observer, DisplayElement {

    private int meat;//과일
    private PreorderData preorderData;

    public VegetableShop(PreorderData preorderData){
        this.preorderData = preorderData;
        preorderData.registerObserver(this);
    }

    public void update(int meat, int fruit, int vegetable) {
        this.meat = meat;
        display();
    }

    public void display() {
        System.out.println("[야채가게] 야채재고: "+meat);
    }
}

 

FoodShop

package cg.park.designpattern.observer;

public class FoodShop implements Observer, DisplayElement {

    private int meat;//고기
    private int fruit;//과일
    private int vegetable;//야채
    private PreorderData preorderData;

    public FoodShop(PreorderData preorderData){
        this.preorderData = preorderData;
        preorderData.registerObserver(this);
    }

    public void update(int meat, int fruit, int vegetable) {
        this.meat = meat;
        this.fruit = fruit;
        this.vegetable = vegetable;
        display();
    }

    public void display() {
        System.out.print("[종합마트] 고기재고: "+meat);
        System.out.print(" 과일재고: "+fruit);
        System.out.println(" 야채재고: "+vegetable);
    }
}

 

마지막으로 각 기능을 테스트해봅니다.

 

PreOrder

package cg.park.designpattern.observer;

public class PreOrder {

    public static void main(String[] args) {
        PreorderData preorderData = new PreorderData();

        MeatShop meatShop           = new MeatShop(preorderData);
        FruitShop fruitShop         = new FruitShop(preorderData);
        VegetableShop vegetableShop = new VegetableShop(preorderData);
        FoodShop foodShop           = new FoodShop(preorderData);

        preorderData.setFoodMaterials(10, 20, 30);//오픈 시 재고
        preorderData.setFoodMaterials(5, 20, 30);//고기 5개 판매

        preorderData.removeObserver(vegetableShop);//야채가게는 별도로 창고를 구입하여 이제 연락받지 않음.

        preorderData.setFoodMaterials(5, 10, 30);//과일 10개 판매
    }
}

 

창고의 재고가 달라진 만큼 각 가게에 재고가 갱신되는 것을 확인할 수 있습니다.

 


 

후기

 

옵저버 패턴을 직접 알아보고 구현하는 것을 추천합니다.

모든 디자인 패턴을 모든 코드에 녹일 수는 없겠지만, 기본적으로 알고 있어야 필요할 때 효율적인 디자인 패턴을 사용할 수 있습니다.

감사합니다.

 

Git : https://github.com/qkrcksrbs8/designpattern

참조 자료: Head First Design Patterns

+ Recent posts