nowwater 2023. 4. 5. 20:07
728x90

new 연산자의 문제점

  • new 연산자 자체의 문제는 없다
  • 그러나 구상 클래스를 만들게 되고, 변화에 취약하다는 것이 진짜 문제

이러한 변화에 유연하게 대응할 수 있게 해주는 것이 인터페이스를 바탕으로 만든 코드

  • 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문

 

예시: 피자 만들기

1. 인스턴스를 만드는 구상 클래스 선택

  • 피자 종류가 바뀔 때마다 코드를 계속 고쳐야 함
public class PizzaStore {
	Pizza orderPizza(String type) {
		Pizza pizza;

		// 바뀌는 코드
		if (type.equals("cheese")) {
			pizza = new CheesePizza();
		} else if (type.equals("pepperoni")) {
			pizza = new Pepperoni();
		}
		// ...

		// 바뀌지 않는 코드
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		return pizza;
	}
}

2. Simple 팩토리를 사용하여 객체 생성 부분을 캡슐화

  • 팩토리: 객체 생성을 처리하는 클래스
  • 구현을 변경할 때 팩토리 클래스 하나만 고치면 됨
  • 디자인 패턴이라기 보다는 자주 쓰이는 관용구
  • 정적 메소드로 만들어서 팩토리 객체 인스턴스 생성하지 않고도 객체 생성 메서드 호출할 수 있게 가능
    • 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경할 수 없다는 단점 존재
public class PizzaStore {
 	SimplePizzaFactory factory;

	public PizzaStore(SimplePizzaFactory factory) {
		this.factory = factory;
	}

	public Pizza orderPizza(String type) {
		Pizza pizza = factory.createPizza(type);

		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();

		return pizza;
	}
}

public class SimplePizzaFactory {

	public Pizza createPizza(String type) {
	  	Pizza pizza = null;

		if (type.equals("cheese")) {
			pizza = new CheesePizza();
		} else if (type.equals("pepperoni")) {
			pizza = new Pepperoni();
		}
		// ...

		return pizza;
	}
}

SimplePizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

3. PizzaStore 를 추상 클래스로 변경 - 팩토리 메소드 패턴 적용

 

팩토리 메소드: 서브 클래스에서 어떤 클래스를 만들지 결정하여 객체를 생성하는 메서드

  • 피자를 주문하는 일 자체 로직은 구현 (final 로 오버라이드 불가하게 선언 가능)
  • 피자를 생성하는 로직(달라지는 부분)은 각 서브 클래스마다 오버라이드하여 작성
  • orderPizza 메서드는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 알 수 없음
    • PizzaStore 와 Pizza 는 서로 완전히 분리 !
    • 피자는 어떤 서브클래스를 선택했느냐에 따라 결정됨
public abstract class PizzaStore {

	public final Pizza orderPizza(String type) {
 		Pizza pizza = createPizza(type);

		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();

		return pizza;
	}

	abstract Pizza createPizza(String type);
}

public class NYStylePizzaStore extends PizzaStore {

	@Override
  	public Pizza createPizza(String type) {
	  	Pizza pizza = null;

		if (type.equals("cheese")) {
			pizza = new NYStyleCheesePizza();
		} else if (type.equals("pepperoni")) {
			pizza = new NYStylePepperoni();
		}
		// ...

		return pizza;
	}
}

PizzaStore nyPizzaStore = new NYPizzaStore();
nyPizzaStore.orderPizza("cheese");

4. 원재료군으로 묶기 - 추상 팩토리 패턴 적용

  • 추상 팩토리로 제품군을 생성하는 인터페이스를 제공
  • 코드가 실제 제품과 분리되어 있으므로, 다른 결과가 필요하면 다른 팩토리 사용
public interface PizzaIngredientFactory {

	public Dough createDough();
	public Sauce createSauce();
	public Cheese createCheese();
	public Veggies[] createVeggies();
	public Pepperoni createPepperoni();
	public Clams createClam();

}

public abstract class Pizza {

	Dough dough;
	Sauce sauce;
	Veggies veggies[];
	Cheese cheese;
	Pepperoni pepperoni;
	Clams clams;

	abstract void prepare(); // 피자 생성에 필요한 재료를 가져옴

	// ...

}

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

	public Dough createDough() {
		return new ThinCrustDough();
	}

	public Sauce createSauce() {
		return new MarinaraSauce();
	}

	// ...
}

public class NYPizzaStore extends PizzaStore {

	protected Pizza createPizza(String name) {
		Pizza pizza = null;
		PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

		if (item.equals("cheese")) {
			pizza = new CheesePizza(ingredientFactory);
		} else if (item.equals("veggie")) {
			pizza = new VeggiePizza(ingredientFactory);
		}
		// ...

		return pizza;
	}
}

팩토리 메소드 패턴

  • 객체를 생성할 때 필요한 인터페이스를 만든다.
  • 서브클래스에서 팩토리 메소드를 구현해서 객체 생성 → 사용하는 서브 클래스에 따라 생산되는 객체 인스턴스가 달라짐
  • 클래스 인스턴스를 만드는 일을 서브클래스에 맡김
  • 상속을 사용 → 객체 생성을 위한 팩토리 메서드를 서브 클래스에서 오버라이드하여 구현
  • 단점: 하나의 서브클래스를 통해 한 가지 제품만 생산 가능 (재사용 불가)

 

추상 팩토리 패턴

  • 구상 클래스에 의존하지 않고도 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공
  • 팩토리 인터페이스에서 선언한 메소드에서 객체 생성이 구현
  • 클라이언트는 추상화된 객체와 객체 생성을 위한 추상화된 팩토리를 가짐
  • 구성을 사용 → 객체 생성을 위한 추상화된 팩토리(인터페이스)를 갖고 있고, 그 팩토리의 구현체로 객체를 생성
  • 장점: 일련의 연관된 제품을 하나로 묶을 수 있음 (재사용 가능) → 추상 팩터리 인터페이스(추상 클래스)의 구현체 단위로 객체를 생성하기 때문
  • 단점: 새로운 제품을 추가하려면 인터페이스를 바꿔야 함

→ 두 패턴 모두 객체 생성을 캡슐화하여 애플리케이션의 느슨한 결합을 도와주고, 특정 구현에 덜 의존하도록 만들 수 있게 해줌 (클라이언트 코드와 구상 클래스 생성을 분리)

추상 팩토리 패턴에서 팩토리 메소드 패턴을 사용할 수 있음

객체 생성을 위한 추상 팩토리의 구현체를 만드는 부분을 팩토리 메서드로 구현해서, 서브 클래스에서 오버라이드하여 제공!


디자인 원칙

추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

의존성 뒤집기 원칙(Dependency Inversion Principle)

 

고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 함

고수준 구성 요소

  • 다른 '저수준' 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소
  • e.g. PizzaStore (피자에 따라 행동이 결정됨)

저수준 구성 요소

  • e.g. Pizza

의존성 뒤집기를 통해 고수준 모듈과 저수준 모듈이 둘 다 하나의 추상 클래스에 의존하게 됨 ( PizzaStore → Pizza ← Pizza 구현체 )

 

의존성 뒤집기 원칙 가이드라인

  • 변수에 구상 클래스의 레퍼런스 저장 X
    • 팩토리를 써서 구상 클래스 레퍼런스 저장 방지
  • 구상 클래스에서 유도된 클래스 생성 X
    • 특정 구상 클래스 의존을 막기 위해 인터페이스나 추상 클래스로부터 클래스 생성
  • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 X
    • 이미 구현되어 있는 메소드를 오버라이드하면 베이스 클래스가 제대로 추상화되지 않음

 

팩토리의 장점

  • 객체 생성 코드를 한 군데서 관리 → 중복 제거 및 관리 용이
  • 객체 인스턴스 생성 시 인터페이스만 있으면 됨 → 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞춰 코딩할 수 있게 해주는 강력한 기법, 구상 클래스 의존성을 줄여줌
  • 객체 생성을 캡슐화하여 클라이언트 코드와 실제 클래스 구현을 분리