Notice
Recent Posts
Recent Comments
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Factory Method Pattern (기본) 본문

Design Pattern/Creational Pattern

Factory Method Pattern (기본)

defacto standard 2017. 9. 26. 06:54

1. 가정


 - 게임에서 마시는 체력포션을 구현한다.

 - 포션을 사용하면 각 포션의 색깔을 출력한다.

 - 유저는 포션을 마실 수 있다.

 - 포션은 포션 상점으로부터 제공받는다.


2. Naive Code


 - RedPotion

public class RedPotion {
public void use() {
System.out.println("using RedPotion");
}
}

체력포션. 자신의 역할을 수행한다.



 - NaiveUser

public class NaiveUser {
private RedPotion redPotion;

public void setRedPotion(RedPotion redPotion) {
this.redPotion = redPotion;
}

public void drink() {

if(redPotion==null)
System.out.println("포션이 부족합니다");
else
redPotion.use();

redPotion = null;
}
}

외부로부터 포션을 주입받아서 지역객체로 세팅한 후, drink라는 메서드를 호출되면 포션을 사용하는 NaiveUser 클래스이다.

포션을 마신 후에는 소비했다는 의미로 null을 세팅한다.


 - NaivePotionShop

public class NaivePotionShop {

public RedPotion getRedPotion(){
return new RedPotion();
}
}

포션을 만들어서 리턴하는 상점 클래스.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {

NaiveUser naiveUser = new NaiveUser();
NaivePotionShop naivePotionShop = new NaivePotionShop();

naiveUser.setRedPotion(naivePotionShop.getRedPotion());
naiveUser.drink();
naiveUser.drink();

}
}

유저와 포션샵을 만들고, 상점으로부터 포션을 얻어서 유저에게 세팅한 후 현재 세팅된 포션의 기능을 사용하는 Client이다.


실행 결과는 다음과 같다.



3. 문제점


위 소스코드는 심각한 유지보수의 결함을 야기한다.


만약 다음과 같이 요구사항이 수정된다고 가정해보자.

 - 유저는 파랑포션, 보라포션도 사용할 수 있어야 한다.

 - 유저에게 할당되는 포션은 동적으로 변경될 수 있다.


현재는 RedPotion과 NaiveUser, 그리고 RedPostion와 PotionShop이 아주 강한 결합도를 보이기 때문에, 파랑포션, 보라포션도 사용이 가능하려면 NaiveUser와 NaivePotionShop의 소스가 수정되어야 한다.


또한, 추가된 포션을 동적으로 세팅하여 사용하기 위해 RedPotion, BluePotion, PurplePotion 의 레퍼런스를 모두 가지고 있다고 하여도 새로운 포션이 추가된다면 또다시 소스는 수정되어야 한다.




4. 해결방안

현재 이러한 문제가 일어나는 이유는 클래스간의 강력한 결합때문이다.

RedPotion, BluePotion, PurplePotion의 공통적인 로직은 '마시는 것' 이다.

따라서 추상화된 개념을 사용하여 컨트롤 하여야 한다. 이렇게 구체적인 객체에 대한 추상적인 개념을 'Product'라고 하며, 구체적인 개념을 'ConcreteProduct'라고 한다.


또한, PotionShop에서는 이러한 객체의 생성을 현재 new RedPotion()을 직접 호출하여 수행하고있다.

이에 따라 각 포션을 만드는 메서드가 따로 존재하여, Potion의 종류가 많아지거나 변경되면 PotionShop이 수정되어야 한다.

또한 PotionShop의 변경에 따라 PotionShop을 이용하는 Client단의 소스 역시도 수정되어야 한다.


이를 해결하려면, 객체를 생성하는 클래스와 메서드를 서브 클래스로 따로 만들고, PotionShop은 이 클래스로부터 포션을 제공받아 넘기는 역할로만 구현하게 끔 해야한다.


하위 클래스로부터 구체적인 포션을 추상적인 개념으로 받아들여서 이를 사용하는 클래스에게 넘기는 클래스를 'Factory' 라고 한다.

구체적인 객체 생성을 담당하는 클래스를 'ConcreteFactory'라고 한다.


이렇게 한다면 Potion의 종류가 추가되더라도 PotionShop(Factory)의 소스는 변경되지 않아 OCP를 만족한다.

PotionShop이 변경되지 않으므로, PotionShop을 이용하는 Client의 소스코드 역시도 변경되지 않는다.



5. Solution Code

 - Product

public abstract class Product {
public abstract void use();
}

추상화된 인스턴스의 클래스 Product이다. 예제에서는 Red/Blue/Purple Potion의 추상적인 개념에 해당된다.

어떤 물약이든 상관없이, 어쨋든 물약은 '사용'할 수 있으므로 use()라는 메서드를 구현해야한다.


 - ConcreteProduct 1, 2

public class ConcreteProductOne extends Product {
@Override
public void use() {
System.out.println("ConcreteProduct1");
}

}

public class ConcreteProductTwo extends Product {
@Override
public void use() {
System.out.println("ConcreteProduct2");
}
}

추상적인 개념인 Product를 상속받아 구체적인 개념을 나타내는 ConcreteProduct이다. 예제 상에서는

Red/Blue/Purple Potion에 해당한다. 편의 상 2개의 클래스만 정의하였다.


 - Factory

public abstract class Factory {

// Template method
public Product create(Type type){
Product product = createConcreteProduct(type);
return product;
}

// Primitive Method, Factory Method
public abstract Product createConcreteProduct(Type type);
}

추상적인 객체 Product를 만들어서 리턴하는 Factory 클래스이다. 예제 상에서는 PotionShop에 해당한다.


이와 같이 상속 관계를 이용해 팩토리 메서드 패턴을 설계하는 경우, 팩토리 메서드를 이용해 구체적인 클래스의 객체를 생성하는 기능은 일반적으로 하위 클래스에서 오버라이드 되게 한다.


그러므로 팩토리 메서드를 호출하는 상위 클래스의 메서드는 템플릿 메서드이면서 팩토리 메서드가 된다.


Factory의 create()에서는 인자를 받아, 해당 인자 타입에 맞는 실제 객체를 반환하도록 구현한다.

 

메서드명에서도 알 수 있듯이, Factory의 역할 자체는 우선 '객체를 만들어서 넘기는 것' 이다. 

다만, 만들어 주는 척만 하고 실제로는 넘기는 역할만 하며 실제 만드는(new) 작업은 하위 클래스(ConcreteFactory)가 모두 한다.

 

Factory클래스의 create()는, 하위 클래스(ConcreteFactory)에서 실제로 생성하는 객체를 받아서 넘기는 메서드를 정한 틀로, Template Method이다.


따라서, Factory Method 패턴은 인스턴스를 생성하는 공장을 Template Method 패턴으로 구성한 것이라 보면 된다.


Template Method Pattern이 아닌, static 메서드를 통해 Factory Method Pattern을 구성하는 방법 역시 많이 쓰이는 기법이다.


 - ConcreteFactory

public class ConcreteFactory extends Factory {

@Override
public Product createConcreteProduct(Type type) {
Product product = null;

switch (type) {
case ONE:
product = new ConcreteProductOne();
break;
case TWO:
product = new ConcreteProductTwo();
break;
}

return product;
}
}

실질적으로 객체를 만들어서 추상적인 객체를 리턴하는 ConcreteFactory이다. 객체의 생성은 이 클래스에서 전부 담당한다. 예제에서 

new RedPotion()을 수행하는 부분을 이 클래스에서 수행하게 된다.


ConcreteFactory는 Factory의 Primitive Method (createConcreteProduct())를 구현하여 실질적인 ConcreteProduct객체를 만들어 반환하는 역할을 한다.

 

결국 Factory는 ConcreteFactory에게, Client로 부터 넘어온 인자를 통해 어떤 ConcreteProduct객체를 생성할 것인지를 알려주고, 생성된 객체를 넘기는 역할만 한다.

 

실제 객체의 생성은 Template method를 구현하는, 지정된 ConcreteFactory에서 만들어져 넘겨지는 것이다.


위 소스코드에서 알 수 있듯이, 객체의 생성은 new 연산자를 사용하여야 한다.

따라서 PotionShop에 해당하는 Factory클래스를 상속받는, 서브 클래스 ConcreteFactory의 경우에는 구체적인 Potion 클래스들과 강한 결합을 맺는다.


따라서, Potion의 종류가 추가됐을 때 이를 지원하려면 이 서브클래스의 변경은 불가피하다.

단, 이를 사용하는 Factory클래스에서는 소스코드의 변화가 없고, 더불어 이를 사용하는 Client의 소스코드 역시 변화가 없다는 것이 중요하다.


Factory 자체에서 어떠한 객체를 생성할지 모르고, 인자를 통해 이것을 결정한다. 다만, Product의 범주에 안에 들어간다는 정보만 알고 있다.

ConcreteFactory에서 구체적인 ConcreteProduct를 만들지만, 리턴은 추상화된 Product를 넘긴다는 것이다.

 


 

이때, Factory클래스 단에서 인자를 받아 어떤 구체적인 클래스의 인스턴스를 생성할지 결정하는 방법은 2가지를 생각할 수 있다.

1. Factory 객체의 생성자로 인자를 넘겨 어떤 인스턴스를 생성할지 정한다.

2. 이미 만들어진 Factory객체의 create()메서드에서 인자를 받아, 이 메서드 안에서 인스턴스를 생성할지 정한다.

 

1번의 문제점은, 다른 인스턴스를 생성할 때 마다, 추상 Factory를 new 연산자를 사용하여야 한다는 것이다.

2번과 같이 Factory 객체만을 만들어 놓고, create()메서드를 통하여 인자를 받게 구현한다면, 다양한 종류에 대한 구상 인스턴스를 한 개의 추상 인스턴스만 사용하여 계속해서 생성해낼 수 있다.

 

추상 클래스는 기본적으로 이 자체만으로는 인스턴스화 할 수 없다.

따라서 ConcreteFactory라는 클래스를 사용하여 바인딩하는 방법으로 사용해야 한다.


 - Type

public enum Type {
ONE, TWO
}

어떤 구체적인 종류의 Product를 만들 수 있는지에 대한 리스트를 나타낸다.


 - Client

public class Client {
public static void main(String args[]){
Factory factory = new ConcreteFactory();
Product product1 = factory.create(Type.ONE);
Product product2 = factory.create(Type.TWO);
product1.use();
product2.use();
}

}

Client는 어떤 클래스(ConcreteFactory)가 추상화 객체(Factory)에 할당이 되었는지 알 수 없다.

 

인자(주로 String 혹은 enum을 사용)로 넘긴 값에 해당하는, ConcreteFactory에서 생성된 구상 인스턴스를 리턴받아 추상 인스턴스에 바인딩하여 그대로 사용하기만 하면 된다.


수행 결과는 다음과 같다.


6. Factory Method Pattern

객체의 생성 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성의 변화에 대비하는데 유용하다. 프로그램이 제공하는 기능은 상황에 따라 변경될 수 있다. 그리고 특정 기능의 구현은 개별 클래스를 통해 제공되는 것이 바람직한 설계다. 그러므로 기능의 변경이나 상황에 따른 기능의 선택은 바로 해당 객체를 생성하는 코드의 변경을 초래한다. 게다가 상황에 따라 적절한 객체를 생성하는 코드는 자주 중복될 수 있다. 이런 경우 객체 생성 방식의 변화는 해당되는 모든 코드 부분을 변경해야 하는 문제를 일으킨다.


이러한 경우 객체 생성 코드를 별도의 클래스/메서드로 분리해 이용한다면 이 클래스/메서드만 변경함으로써 객체 생성 방식의 변화에 효과적으로 대응할 수 있다.

위 그림의 왼쪽처럼 여러 개의 클래스(A ~ Z)에서 필요에 따라 클래스 X1의 객체와 클래스 X2의 객체를 생성해 사용한다. 만약 X1과 X2를 생성하는 방식이 달라지거나 X3과 같이 새로운 클래스의 객체를 생성해야 하는 경우에는 X1과 X2를 생성하는 모든 코드 부분을 변경해야 한다.


하지만 오른쪽과 같이 팩토리 메서드 패턴을 사용하면 객체 생성 기능을 제공하는 Factory 클래스를 정의하고 이를 활용하는 방식으로 설계하면 된다. 이렇게 설계하면 X1과 X2의 생성 방식이 변경되거나 X3를 추가해야할 때 Factory 클래스만 변경하고 클래스 A~Z는 변경할 필요가 없게 된다.


또한 팩토리 메서드 패턴은 객체 생성을 전답하는 별도의 클래스를 두는 대신 하위 클래스에서 적합한 클래스의 객체를 생성하는 방식으로도 적용할 수 있다. 


- UML



7. 장단점


 장점 : 생성할 ConcreteProduct 타입을 예측할 수 없을 때 -> 추상화된 Product 클래스 타입을 이용하여 캡슐화

생성할 객체의 명세를 하위 클래스에서 정의. 객체 생성의 책임을 하위 클래스에 위임시키고, 어느 하위 클래스가 위임했는지에 대한 정보를 은닉

 단점 :

Comments