본문 바로가기

디자인 패턴/디자인 패턴

[디자인 패턴] 싱글턴 패턴 문제 - 파스타 (데코레이터, 옵저버, 팩토리)

책을 공부하며 새로운 문제를 만들고 구현한것을 기록한 게시글입니다.

(이해한 내용을 바탕으로 작성했기 때문에, 내용이 정확하지않고 틀린부분이 있을수있으며, 예시가 패턴을 사용하기에 알맞지 않을수도있습니다)


싱글턴 패턴

클래스 인스턴스가 하나만 만들어지도록하며, 해당 인스턴스를 전역적으로 접근하도록함.

 

다중스레드를 이용할때, 싱글턴 패턴을 사용하면, 오류가 발생할수있다. (스레드가 동시에 클래스를 호출해, 인스턴스가 한개 초과로 생성된다거나..)

이를 방지하기위해, syncronized와 volatile을 사용한다.

 

syncronized 

 

syncronized는 스레드간에 동기화를 시켜준다. A스레드와 B스레드가있다고하자. A스레드가 a메소드를 사용중일때, syncronized는 a메소드에 lock을건다. 즉, A스레드의 a메소드 사용이 끝날때까지 B스레드는 a메소드를 사용하지못한다. (A스레드의 사용이 끝난후 사용함)

 

volatile

 

같은 프로세스에있는 스레드는 메인 메모리를 공유한다. 하나의 스레드A에서 static변수 num의 값을 10으로 변경했다고하자.

이때, 스레드B에서 num의 값을 가져올때, 10이 아닐수도 있다. (스레드는 작업을할때 메인 메모리를 cache에 복사해 작업을 하기때문이다.)

volatile은 변수를 캐시가 아닌 메인메모리에서 가져와 작업을하도록 한다. 스레드 A에서의 변경값이 스레드 B까지 이어진다.


문제

명수는 파스타를 먹으러 파스타 거리에 왔다.

 

파스타 거리를 걷던 명수는 두 파스타집을 발견했고, 미국식 파스타 전문점 과 이탈리아식 파스타 전문점 으로 분류되어있는것을 발견했다.

각각 가게의 세부 메뉴는 다음과 같다.

 

- 이탈리아식 치즈오븐 / 미국식 치즈오븐

- 이탈리아식 로제 / 미국식 로제

- 이탈리아식  알리오올리오 / 미국식 알리오올리오

 

(미국식 파스타의 모든메뉴에는 엄청난 양의 치즈가 들어간다하자) 

 

두 가게는 모든 원재료를 한 업체에서 수입한다. (따라서, 업체의 사정에따라 파스타의 가격 편차가 클수있다.) 

(명수가 메뉴를 선택하는 중에도 업체가 제공하는 원재료가격은 달라질수있다.)

 

돈이 얼마없는 명수는 자신이 선택한 메뉴의 가격을 미리 알고싶다.

 

명수가 어떤 파스타집에 들어가서 파스타를 정했을때, 가격을 계산하는 프로그램을 만들자.

 

각 메뉴의 기본가격과 들어가는 재료는 다음과 같다. (미국식 파스타라면 모든메뉴에 치즈가 한번 더 들어간다.)

- 치즈오븐 : $10

(치즈, 파스타 면, 토마토 소스)

- 로제 : $13

(파스타 면, 토마토 소스)

- 알리오올리오 : $15

(파스타 면, 허브, 올리브 유)

 

입력은 다음과 같다.

 

명수가 알아볼 메뉴의 갯수 N (명수가 메뉴를 알아볼때마다 공급업체의 가격또한 달라진다)

명수가 들어간 파스타 가게 

공급 업체의 달라지는 원재료가격 (토마토소스, 파스타면발, 올리브오일, 허브, 치즈 순으로 주어진다.)

명수가 고른 파스타

 

ex)

4

AmericanPasta 

3 2 3 1 2 

CheeseOvenPasta

 

출력

메뉴이름 + 치즈오븐의 기본가격 + 원재료들의 가격

CheeseOvenPasta $17


구현

옵저버패턴, 팩토리패턴, 싱글턴패턴, 데코레이터 패턴을 사용해 구현했다.

디자인...

파스타는 각각 재료를 선택적으로 가져와야한다. (예를들어, 알리오올리오에는 토마토소스가 들어가지않는다.) 따라서, 파스타를 만드는과정을 데코레이터 패턴을 사용해만들자. 

(또한, 데코레이션 패턴을 이용해 파스타의 재료들을 넣을경우, AmericanStyleCheesePasta, AmericanStyleOvenPasta...등과같이 객체를 여러번 만들지 않아도 되서 편할것같다.)

- 파스타에 들어가는 재료들 (토마토소스, 치즈, 허브 ...)가 데코레이션이 될것이다. 이후에 createPasta 메소드에서 선택적으로 데코레이팅하면된다.

 

공급업체(Supplier)는 유일하다. (새로운 공급업체가 생겨서, 각 가게들이 서로 다른 원재료 가격을 가져오면안된다.) 따라서, 싱글턴패턴을 사용해 만든다.

또한, Supplier에서 가격변동이 일어나면, Supplier에 등록된 가게들이 공급받는 재료들의 가격또한 달라져야한다. 이는 옵저버패턴을 사용해 구현하자.

- sujbect는 supplier가 될것이고, observer는 파스타에 들어가는 재료들이 될것이다.

 

명수가 주문할 파스타에 대해서는 팩토리 메소드 패턴을 사용해서 만들자. 추상클래스 pastaStore을 만들고, pastaOrder()를 구현, pastaCreate()를 정의하자.

- pastaOrder() 피자를 만드는 과정은 동일하므로, 서브 클래스에서 직접 구현하지않는다.

- PastaCreate() 는 PastaStreet의 서브클래스 AmericanStylePastaStore, ItalianStylePastaStore에서 각각 구현한다. 또한, 위에서 만든 재료들을 올려 pastaCreate 반환할것이다.(재료가 올라간 파스타가 반환되면 pastaCreate에서는 굽기만하면된다.)

 

디자인 대로 구현하고 실행해보자.

(디자인을 해보니 지금까지 배운 디자인패턴이 전부 섞여서 구현이 상당히 복잡할것같다..문제를 어렵게 만들었나..?)

 

 

재료들이 있어야 파스타를 만들수있다. 또한, 재료들의 가격을 측정하는 Supplier(Subject)가 있어야 재료들을 observer로 등록할수있으므로 구현순서는 대충 

Supplier -> Ingredient(재료) -> 파스타관련 메소드들 이 될것이다.

 

(이후 구현은 코드를 확인하자 주석을 달아놨다.)

 

구현한 클래스의 특징을 체크해보자.

 

1. Supplier 클래스

- 옵저버 패턴의 Subject를 구현한다 자신에 등록된 Observer들이 Observer interface를 구현한다는것을 제외하면 어떠한 정보도 모른다. 즉, Observer들에게 변경사항이 생겨도 Observer패턴만 구현한다면, 오류가 생기지않는다. -> OCP

- 싱글턴 패턴으로, 실행중 하나만 생성된다.

Supplier 클래스의 코드는 다음과 같다.

package prob4_pasta.Supplier;

import java.util.ArrayList;
import prob4_pasta.Pasta.Ingredients.*;
import java.util.Scanner;

public class Supplier implements Subject{
    private int costTomatoSauce;
    private int costPastaNoodles;
    private int costOliveOil;
    private int costHurb;
    private int costCheese;
    private Scanner sc;
    // 재료들의 가격을 외부에서 접근하면 안되므로 private로 선언한다.
    private static Supplier supplier; // 싱글톤 객체인 Supplier
    private ArrayList<Observer> ingredients = new ArrayList<Observer>(); // Observer을 등록하고 관리하는 ArrayList
    // 재료들의 가격을 외부에서 접근하면 안되므로 private로 선언한다.
    private Supplier(){
        this.sc = new Scanner(System.in);
    }
    
    public static Supplier getInstance(){
        if(supplier == null) supplier = new Supplier();
        return supplier;
        // static으로 만듦으로써 구상 인스턴스 없이도 getInstance는 선언가능하다. (싱글톤 패턴)
    }
    
    public void updatePrice(){ // 가격 바꾸는 메소드
        this.costTomatoSauce = sc.nextInt();
        this.costPastaNoodles = sc.nextInt();
        this.costOliveOil = sc.nextInt();
        this.costHurb = sc.nextInt();
        this.costCheese = sc.nextInt();
        notice();
    }
    
    public void notice(){ // 등록된 옵저버들 (재료들)에게 가격 변동을 알리는 메소드
        for(int i = 0; i < ingredients.size(); i++){
            Observer ingredient = ingredients.get(i);
            ingredient.get(this.costTomatoSauce, this.costPastaNoodles, this.costOliveOil, this.costHurb, this.costCheese);
        }
    }
    
    public void addObserver(Observer ingredient){ // 재료들을 등록하는 메소드
        this.ingredients.add(ingredient);
    }
}

 

2. Pasta 클래스

- 데코레이터 패턴으로 PastaPrepare인터페이스를 구현한다. 중첩된 클래스의 가장아래에 항상 위치하므로, cost()메소드가 가장 늦게 호출된다.(cost메소드는 PastaPrepare인터페이스에 선언되어있다) 따라서, cost가 호출되었을때, 만든 파스타 이름을 출력한다. 

파스타 클래스와, 파스타에 재료를 넣는 Ingredients 를 구현한 클래스(TomatoSauce)는 다음과 같다.

package prob4_pasta.Pasta;

public class Pasta implements PastaPrepare{
    
    private int cost;
    private String pastaMenu;
    
    public Pasta(int cost, String Menu){
        this.cost = cost;
        this.pastaMenu = Menu;
    }
    
    public int cost(){
        System.out.print(this.pastaMenu + " $"); 
        // 데코레이션 패턴에서 재귀로 cost를 더하며 들어올때, 마지막에 위치하는 Pasta클래스 까지 들어온시점에, pastaMenu를 호출한다. 
        //이렇게 구현한 이유는, PastaPrepare에 메뉴 이름을 리턴하는 메소드가 없기때문이다.(다형성 사용못함)
        return this.cost;
    }
    
}
package prob4_pasta.Pasta.Ingredients;

import prob4_pasta.Pasta.*;

public class TomatoSauce implements PastaPrepare, Observer{
    
    private PastaPrepare pasta;
    
    private static int TomatoSauceCost;
    
    public TomatoSauce(){}
    
    public TomatoSauce(PastaPrepare pasta){
        this.pasta = pasta;
    }
    
    public int cost(){
        return this.pasta.cost() + TomatoSauceCost;
    }
    
    public void get(int tomato, int pasta, int oil, int hurb, int cheese){
        TomatoSauceCost = tomato;
    }
    
}

3. PastaStore 클래스

- 팩토리 메소드 패턴으로 구현되었다. 

 

package prob4_pasta.Store;

import prob4_pasta.Pasta.*;
import prob4_pasta.Pasta.Ingredients.*;

public class ItalianStylePastaStore extends PastaStore{
    
    protected PastaPrepare createPasta(String menu){
        if(menu.equals("CheeseOvenPasta")){
            pasta = new Pasta(10, menu);
            pasta = new Cheese(pasta);
            pasta = new PastaNoodles(pasta);
            pasta = new TomatoSauce(pasta);
        }
        else if(menu.equals("RosePasta")){
            pasta = new Pasta(13, menu);
            pasta = new PastaNoodles(pasta);
            pasta = new TomatoSauce(pasta);
        }
        else if(menu.equals("AglioOlioPasta")){
            pasta = new Pasta(15, menu);
            pasta = new PastaNoodles(pasta);
            pasta = new Hurb(pasta);
            pasta = new OliveOil(pasta);
        }
        return pasta;
    }
    
}

전체 소스코드

커밋전에 폴더를 정리하고 올리기때문에, 커밋하는 과정에서 경로가 변경되어 코드 실행이 안될수도있다. (모두 실행되는걸 확인하고올리지만) 컴파일시 경로를 변경해서 실행하도록하자.

 

https://github.com/devxb/DesignPatterns/tree/main/DesignPatterns/prob4.PastaStore

 

devxb/DesignPatterns

디자인 패턴 🤔. Contribute to devxb/DesignPatterns development by creating an account on GitHub.

github.com