[F-Lab 모각코 챌린지 17일차] 디자인 패턴

부추·2023년 6월 17일
0

F-Lab 모각코 챌린지

목록 보기
17/66

TIL

  1. 데코레이터 패턴
  2. 팩토리 패턴
  3. 싱글톤 패턴



1. 데코레이터 패턴

객체의 메소드의 원본 기능을 유지하면서, 특정 값이나 행위를 추가하여 반환하고 싶을 때 사용하는 디자인 패턴이다.

카페에서 음료를 판다고 생각해보자. cost() 메소드는 음료의 가격을 반환한다. 음료를 나타내는 Beverage를 상위 추상 클래스로 두었다. 그를 상속하는 Americano, Latte 클래스가 각각 존재한다.

public abstract class Beverage {
	String name;
    public abstract int cost();
    public String getDescription() {
    	return this.name;
    }
}

public class Americano extends Beverage {
	public Latte() {
    	this.name = "아메리카노";
    }
    public int cost() {
    	return 2000;
    }
}

public class Latte extends Beverage {
	public Latte() {
    	this.name = "라떼";
    }
    public int cost() {
    	return 3000;
    }
}

카페 음료는 여러가지 토핑이 추가될 수도 있다. 샷을 추가한 아메리카노, 바닐라 시럽을 추가한 바닐라 라떼, 휘핑을 올린 라떼 ... 등등. 토핑이 추가될 때마다 cost()의 반환 값은 바뀔 것이다.

각각의 경우에 대해서 DoubleShotAmericano, VanillaLatte, WhippedLatte 등의 클래스를 새로 정의할 것인가? 만약 시럽을 두 번 추가했다면? 트리플 샷이라면?
'너희 지금 전혀 객체지향을 하고있지 않아'. 887462487624개의 클래스 파일을 만들 것인가?!

토핑을 구성요소로 사용하는 등의 옵션이 있겠지만.. 음료의 옵션이 추가될 때마다 cost() 메소드에 옵션에 맞는 가격을 더해주는 데코레이터 패턴을 사용해보자.

BeverageTopping이다. Beverage 클래스의 cost() 메소드에 특정 값이나 행위를 추가하기 위해 작성한다. 구성으로 Beverage 타입의 객체를 갖는다.

public abstract class BeverageTopping extends Beverage {
	private Beverage beverage;
}

왜 데코하려는 클래스에 대해서 상위 클래스를 상속하냐? 이는 '형식을 맞추기 위함'이다. 같은 Beverage 타입으로 cost()를 호출할 수 있음으로써 토핑이 추가된 음료들 역시 일관적으로 처리가 가능하다.

실제로 토핑 클래스를 구현하자. 간단하게 샷이랑 휘핑만 작성했다.

public class Shot extends BeverageTopping {
	public Shot(Beverage beverage) {
    	this.beverage = beverage;
    }
    
    @Override
    public int cost() {
    	return this.beverage.cost() + 500;
    }
    
    @Override
    public String getDescription() {
    	return this.getDescription() + ", 샷추가";
    }
}

public class Whipping extends BeverageTopping {
	public Whipping(Beverage beverage) {
    	this.beverage = beverage;
    }
    
    @Override
    public int cost() {
    	return this.beverage.cost() + 800;
    }
    
    @Override
    public String getDescription() {
    	return this.getDescription() + ", 휘핑추가";
    }
}

이제 사용할 Beverage 객체, 추가할 BeverageTopping 객체를 만들어놓고 원하는 만큼 BeverageTopping 객체를 사용하면 토핑 추가가 가능하다.

public class CafeDriver {
	public static void main(String [] args) {
    	Beverage latte = new Latte();
        
        latte = new Whipping(latte);
        latte = new Whipping(latte);
        latte = new Whipping(latte);
        
        System.out.println(latte.getDescription());
        System.out.println("총 가격 : " + latte.cost());
    }
}

휘핑 추가한 라떼를 좋아해서 3번이나 휘핑을 추가해보았다. 3번 추가된 라떼와 기존 라떼보다 비싸진 가격을 확인할 수 있다.




2. 팩토리 패턴

"구현보단 인터페이스에 맞춰 설계하자" 라는 객체지향 설계원칙이 있다. 그에 따르면 프로그램 로직 부분에서 new를 통해 직접 구상 클래스를 초기화하는 것은 선호되지 않는다. 그 때 팩토리 패턴을 사용한다.

2-1) 팩토리 메소드 패턴

클래스 타입이 만들 인스턴스를 결정하는 팩토리 메소드 패턴을 살펴보자.

사과를 파는 사과가게, 바나나를 파는 바나나가게가 있다. 상속 구조를 사용해서 코드를 쫙 작성해보겠다.

@Getter
public class Fruit {
	String name;
}

public class Apple {
	public Apple() {
    	this.name = "사과";
    }
}

public class Banana {
	public Banana() {
    	this.name = "바나나";
    }
}
public abstract class FruitMarket {
	public abstract void sellFruit();
}
public class AppleMarket extends {
	@Override
	public void sellFruit() {
    	Fruit apple = new Apple();
    	System.out.println(apple.getName() + "팔기");
    }
}
public class BananaMarket extends {
	@Override
	public void sellFruit() {
    	Fruit banana = new Banana();
    	System.out.println(banana.getName() + "팔기");
    }
}

FruitMarket 로직 코드에 구상 클래스의 생성자 직접 호출 부분이 있다. 만약 Fruit과 관련한 객체들에 옵션이 추가된다거나 하는 변화가 일어나면 곤란해질 가능성이 있다.
'너희 지금 전혀 객체지향을 하고있지 않아'.

이름값 하자. "팩토리 메소드", 즉 객체를 만드는 공장 메소드 makeFruit()를 추가한다.

public abstract class FruitMarket {
	public abstract void sellFruit();
    public abstract Fruit makeFruit();
}

public class AppleMarket extends {
	@Override
	public void sellFruit() {
    	Fruit apple = makeFruit();
    	System.out.println(apple.getName() + "팔기");
    }
    
    @Override
    public Fruit makeFruit() {
    	return new Apple();
    }
}

public class BananaMarket extends {
	@Override
	public void sellFruit() {
    	Fruit banana = makeFruit();
    	System.out.println(banana.getName() + "팔기");
    }
    
    @Override
    public Fruit makeFruit() {
    	return new Banana();
    }
}

차이가 보이나? sellFruit()new를 하는 대신 makeFruit() 라는 메소드에 new 역할만을 수행하는 코드를 따로 뺐다. makeFruit()는 구현한 하위 메소드에 따라 생성하는 Fruit 타입이 달라진다. 이걸 보고 각 서브클래스에서 어떤 클래스의 인스턴스를 만들지 결정한다라고 말한다.


2-2) 추상 팩토리 패턴

과일가게가 잘돼서 과일 뿐만아니라 과일 주스, 과일 사탕, 과일 아이스크림도 팔고자 하는 모양이다. 으음.. Juice, Candy, IceCream 추상 클래스와 그 클래스들을 상속받는 AppleJuice, BananaIceCream ... 등등의 하위 클래스를 만들었다. 이걸 어떻게 팩토리 패턴으로 넘기지?

Market 클래스의 코드에 makeFruitJuice(), makeFruitCandy, makeFruitIceCream() 팩토리 메소드를 추가할 수 있을 것 같다.

물론 가능하지만 다른 방법도 있다. 아예 제품군 자체를 만드는 역할을 하는 Factory 클래스 하나를 만드는거다. Juice, Candy, IceCream 등 제품군과 그 하위 클래스의 코드는 생략하겠다. 그냥 이런 느낌이다~ 정도로만 추상 메소드와 추상 팩토리 패턴의 차이를 느껴보자.

public abstract class FruitFactory {
	public Fruit makeFruit();
	public Juice makeJuice();
    public Candy makeCandy();
    public IceCream makeIceCream();
}

public class AppleFactory extends FruitFactory {
	public Fruit makeFruit() {
    	return new Apple();
    }
	public Juice makeJuice() {
    	return new AppleJuice();
    }
    public Candy makeCandy() {
    	return new AppleCandy();
    }
    public IceCream makeIceCream() {
    	return new AppleIceCream();
    }
}

public class BananaFactory extends FruitFactory {
	public Fruit makeFruit() {
    	return new Banana();
    }
	public Juice makeJuice() {
    	return new BananaJuice();
    }
    public Candy makeCandy() {
    	return new BananaCandy();
    }
    public IceCream makeIceCream() {
    	return new BananaIceCream();
    }
}

그럼 이제 각 Market에서는 아래와 같이 Factory를 Composition으로 둔 후, sellXXX() 메소드가 호출될 때 makeXXX() 메소드를 호출할 수 있다.

public abstract class FruitMarket {
	private FruitFactory fruitFactory;
	public void sellFruit() {
    	Fruit fruit = fruitFactory.makeFruit();
    	System.out.println(fruit.getName() + "팔기");
    }
    public void sellCandy() {
    	Candy candy = fruitFactory.makeCandy();
    	System.out.println(candy.getName() + "팔기");
    }
    public void sellJuice() {
    	Juice juice = fruitFactory.makeJuice();
    	System.out.println(juice.getName() + "팔기");
    }
    public void sellIceCream() {
    	IceCream iceCream = fruitFactory.makeIceCream();
    	System.out.println(iceCream.getName() + "팔기");
    }
}

FruitMarket 입장에서, FruitFactory의 인터페이스 형식을 지킨 fruitFactory 객체의 내부 구조는 신경쓰지 않아도 된다. 그저 정해진 makeXXX() 메소드만 잘 수행하면 위 코드는 문제없이 동작한다.

이런 식으로 느슨한 결합을 활용하여, 구성 요소의 변화엔 닫혀있고 확장엔 열려있게 된다. 또한, FruitMarketFruitFactory라는 상위 추상 클래스에 의존하게 되므로 변하지 않는 추상적인 것에 의존하게 된다고도 말할 수 있다. 이는 객체지향의 설계 원칙에 해당하는 것으로, 나중에 SOLID에 대해 정리할 때 한번 더 언급하도록 하겠다.




3. 싱글톤 패턴

프로그램 전체에서 딱 하나만 필요한 객체들이 있다. 커넥션 풀이나 유틸 클래스 같은 애들이다. 얘네를 많이 만들면 메모리 낭비가 있을 수 있다.

효율적인 싱글톤 운영을 위해 다음과 같은 방법을 사용한다.

  1. 생성자는 private으로 만들어 절대! 외부에서 해상 객체를 생성하게 해선 안된다.
  2. 내부에 싱글톤 구성을 두고 getter 비슷한 메소드를 작성한다.
  3. 해당 메소드는 싱글톤 객체 생성 전(=구성요소가 null)이라면 새로운 객체를 만들어서 반환, 객체 생성 뒤라면 생성된 객체를 반환한다.

... 를 코드로 나타냈다.

public class Singleton {
	private static final Singleton instance;
	private Singleton() { }
    
    public static Singleton getInstance() {
    	if (this.instance==null) { 
        	return new Singleton()
        }
        return this.instance;
    }
}

하지만 위 방식의 코드는 멀티스레드에서 참사를 일으킬 수도 있다.

  1. 스레드1이 this.instance==null 확인을 한다. true임을 확인한 뒤, 스레드1의 pc는 return new Singleton()라인을 가리킨다.
  2. 스레드2가 this.instance==null 확인을 한다. 아직 스레드1이 new를 호출하기 전이므로 역시 true. 스레드2의 pc 역시 바로 다음줄을 가리킨다.
  3. 스레드1에 의해 객체가 생성되었다.
  4. 스레드2에 의해 객체가 생성되었다.

'너희 지금 전혀 제대로된 프로그래밍을 하고있지 않아'.

일단 getInstance() 메소드에 synchronized를 붙일 수 있는데, 많은 개발자들이 말하길 해당 키워드가 붙은 메소드는 그냥 100배 느려진다고 생각하라고 한다. 해당 키워드는 언제, 어떤 상황이든 최, 최, 최, 최후의 보루로 남겨두어야 한다.

다음으로, static의 초기화 성질을 이용해서 다음과 같이 JVM에게 초기화 과정을 위임할 수도 있다.

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static Singleton INSTANCE = new Singleton();
    }
    public MultiSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

좋은 방법이다.

스프링에서는 기본적으로 Bean 객체들이 싱글톤으로 동작해서 실제로! 싱글톤을 직접 만들 기회는 많지 않다고 한다(물론 스프링이 없어도 많은 것은 아니다..).. 정말 필요한 레코드클래스 같은 경우는 클래스 자체를 enum으로 선언하는 경우도 많고.. 주절주절..




REFERENCE

헤드퍼스트 디자인패턴 3,4,5장

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글