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

 

Strategy Pattern

알고리즘 군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.

스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 


 

구현

 

의자를 만들 때 컴퓨터로 미리 디자인을 하려고 합니다.

의자는 휠이 있는 의자도 있고 휠이 없는 의자도 있습니다.

그리고 의자의 종류는 다양합니다.

사무용 의자, 흔들의자, 무중력 의자 등...

이런 의자를 객체지향 기법을 사용하여 Chair라는 부모 클래스를 만든 다음, 그 클래스를 확장하여 다른 종류의 의자들도 만듭니다.

public abstract class Chair {
    Moves moves;
    Sounds sounds;
}

 

의자는 휠이 있어서 움직이는 의자인지 아니면 휠이 없어서 움직이지 못하는 의자인지 정의하고,

의자의 종류마다 어떤 소리가 나는지 만들 예정입니다.

 

Moves

package cg.park.designpatten.designpatten.strategy;

public interface Moves {
    public void move();
}

 

Sounds

package cg.park.designpatten.designpatten.strategy;

public interface Sounds {
    public void sound();
}

 

의자가 움직이는지와 어떤 소리가 나는지에 대해서 정의합니다.

하지만 움직이지 않은 의자의 경우 소리가 나지 않습니다.

그러면 어떤 방법으로 클래스를 설계하고 구현해야 할까요?

 

 

이렇게 N:N으로 클래스마다 이것저것 만들어 놓는다면 같은 의자라도 어떤 방식으로 구현되었는지 감이 오지 않을 것입니다.

 

*디자인 원칙

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.

 

코드에 새로운 요구사항이 있을 때마다 바뀌는 부분이 있다면, 그 행동을 바뀌지 않는 다른 부분으로부터 골라내서 분리해야 한다는 것을 알 수 있습니다.

 

 

바뀌는 부분과 그렇지 않은 부분을 분리하여 설계를 합니다.

*Chair

의자를 추상화해놓고 의자의 종류마다 특성에 맞게 구현하였습니다.

*Moves 

움직임에 대해 의자 종류에 상관없이 휠이 있다/없다 로 분리하였습니다.

*Sounds

소리에 대해서는 의자 종류마다 다른 소리가 나는 것을 표현하였습니다.

 

*디자인 원칙

구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.

 

"인터페이스에 맞춰서 프로그래밍한다"는 것은 "상위 형식에 맞춰서 프로그래밍한다"는 것을 뜻합니다.

의자 종류마다 소리를 알고 싶을 때 구성에 맞춰서 프로그래밍을 한다면 다음과 같이 할 수 있습니다.

//무중력의자 소리
ZeroGravityChair zeroGravityChair = new ZeroGravityChair();
zeroGravityChair.ZeroGravityChairSound();

//사무용의자 소리
OfficeChair officeChair = new OfficeChair();
officeChair.OfficeChairSound();

//흔들의자 소리
RockingChair rockingChair = new RockingChair();
rockingChair.RockingChairSound();

 

하지만 인터페이스/상위 형식에 맞춰서 프로그래밍한다면 다음과 같이 할 수 있습니다.

//무중력의자 소리
Chair zeroGravityChair = new ZeroGravityChair();
zeroGravityChair.eventSounds();

//사무용의자 소리
Chair officeChair = new OfficeChair();
officeChair.eventSounds();

//흔들의자 소리
Chair rockingChair = new RockingChair();
rockingChair.eventSounds();

 

객체 생성이 소리 메서드를 호출할 때 룰이 생깁니다.

 

*동적으로 소리를 지정하는 방법

 

package cg.park.designpatten.designpatten.strategy;

public abstract class Chair {
    Moves moves;
    Sounds sounds;

    public Chair (){}

    public void eventMoves() { moves.move(); }

    public void eventSounds() { sounds.sound(); }

    public void setMoves(Moves moves) {
        this.moves = moves;
    }

    public void setSounds(Sounds sounds) {
        this.sounds = sounds;
    }

}

 

Chair 클래스에 Moves와 Sounds를 동적으로 바꿀 수 있게 setMoves와 setSounds를 생성합니다.

 

//무중력의자 인스턴스 생성
Chair chair = new ZeroGravityChair();
chair.eventMoves();//움직임 여부 호출
chair.eventSounds();//의자 소리 호출

chair.setMoves(new dontMovingChair());//못움직이는 의자로 선언
chair.setSounds(new RockingChairSound());//흔들의자 소리로 변경
chair.eventMoves();//움직임 여부 호출
chair.eventSounds();//의자 소리 호출

 

각 단계에 맞게 움직임과 소리가 호출되고 있습니다.

 

"A에는 B가 있다" 관계에 대해 생각해 봅시다.

Moves와 Sounds가 있으며, 각각 움직임과 소리를 위임받습니다.

두 클래스를 이런 방법으로 합치는 것을 구성을 이용하는 것이라고 부릅니다.

위에 나와있는 의자 클래스에서 행동을 상속받는 대신, 올바른 행동 객체로 구성됨으로써 행동을 부여받게 됩니다.

이 기술은 매우 중요한 기술입니다.

사실 우리가 사용한 이 방법은 세 번째 디자인 원칙입니다.

 

*디자인 원칙

상속보다는 구성을 활용한다.

 

이 구성을 이용하여 시스템을 만들면 유연성을 크게 향상할 수 있습니다.

단순히 알고리즘 군을 별도의 클래스의 집합으로 캡슐화할 수 있도록 만들어주는 것뿐 아니라, 구성요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있게 해 줍니다.

 

후기

 

디자인 패턴이라는 말이 처음에는 어렵게 느껴졌다.

하지만 개발 생활을 해보면서 느낀 점은 이러한 방법론은 선배 개발자님들의 시행착오를 바탕으로 더 나은 개발 방법인 것이다.

단어를 암기하듯 디자인 패턴을 암기하는 것이 아니라 직접 구현해보면서 이 패턴이 왜 생겨났고, 그래서 뭐가 더 좋아졌는지를 알게 된다면 더 이상 암기는 하지 않아도 될 것이다.

이번에 구현해본 Strategy Pattern은 실무에서도 많이 볼 수 있는 패턴이다.

의자나 오리가 아닌 각 서비스에 맞는 구현 주제에 관련하여 인터페이스에 맞게 프로그래밍을 하고, 알고리즘들을 캡슐화하여 교환할 수 있게 만드는 방법은 알아둘 필요가 있다. 

감사합니다.

 

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

참조 자료: Head First Design Patterns

 

+ Recent posts