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

Abstract Factory Pattern 본문

Design Pattern/Creational Pattern

Abstract Factory Pattern

defacto standard 2017. 9. 26. 07:05

1. 가정

 - 휴대폰을 만든다.

 - 휴대폰은 자신을 구성하는 부품들에 대한 정보를 출력한다.

 - 제조사는 삼성, LG 2가지 제조사가 존재한다.

 - 각 제조사의 휴대폰은 자사의 부품들로만 구성된다.

 - 구성 부품은 CPU, 카메라 이다.


 

2. Naive Code

 - Phone

public class Phone {
private CPU cpu;
private Camera camera;

public void setCPU(CPU cpu) {
this.cpu = cpu;
}

public void setCamera(Camera camera) {
this.camera = camera;
}

public void info(){
cpu.info();
camera.info();
}
}

CPU, 카메라로 구성되는 Phone 클래스. 지역 레퍼런스인 CPU, Camera객체에게 기능을 위임(Delegation)하여 정보를 출력한다.


 - CPU

public abstract class CPU {
public abstract void info();
}

여러 제조사이고, 모든 CPU가 정보를 출력할 수 있도록 info()메서드를 제공한다.


 - LG, SamsungCPU

public class LGCPU extends CPU {
@Override
public void info() {
System.out.println("LG CPU");
}
}
public class SamsungCPU extends CPU {
@Override
public void info() {
System.out.println("Samsung CPU");
}
}

 구체적인 CPU 클래스이다.


 - Camera

public abstract class Camera {
public abstract void info();
}

CPU와 동일하게 추상화된 Camera를 다루기 위해 선언한 클래스이다.


 - LG, SamsungCamera

public class LGCamera extends Camera {
@Override
public void info() {
System.out.println("LG Camera");
}
}
public class SamsungCamera extends Camera {
@Override
public void info() {
System.out.println("Samsung Camera");
}
}

 구체화 된 카메라 클래스이다.


 - Vendor

public enum VendorID {
LG, SAMSUNG
}

제조사의 리스트이다.


 - CPUFactory

public class CPUFactory {

public CPU create(VendorID vendorID){

switch (vendorID) {
case LG:
return new LGCPU();

case SAMSUNG:
return new SamsungCPU();

default:
return new UnknownCPU();

}

}
}

Vendor값을 인자로 받아서 특정 제조사의 구체적 CPU를 만들어 내는 Factory 클래스이다.


 - CameraFactory

public class CPUFactory {

public CPU create(VendorID vendorID){

switch (vendorID) {
case LG:
return new LGCPU();

case SAMSUNG:
return new SamsungCPU();

default:
return new UnknownCPU();

}

}
}

Vendor값을 인자로 받아서 특정 제조사의 구체적 Camera를 만들어 내는 Factory 클래스이다.


 - NaiveClient

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

CameraFactory cameraFactory = new CameraFactory();
Camera camera = cameraFactory.create(VendorID.LG);

CPUFactory cpuFactory = new CPUFactory();
CPU cpu = cpuFactory.create(VendorID.LG);

Phone phone = new Phone();
phone.setCamera(camera);
phone.setCPU(cpu);

phone.info();


}
}

 

 - VendorID

public enum VendorID {
A, B
}

실행 결과는 다음과 같다.


VendorID로 'LG'를 주었으므로, CPU / Camera Factory에서는 LG부품을 생산한다.


3. 문제점


현재 소스코드는 Factory Method Pattern을 적용한 코드이다. 


디자인 패턴을 적용했는데 어떠한 문제가 있을까.


다음과 같은 요구사항이 추가되었다고 생각해보자

 - 새로운 부품을 지원해야 하는 경우

 - 새로운 제조 업체의 부품을 지원해야 하는 경우


위 두가지 요구사항을 혼동하면 안된다.

첫 번째 요구사항은, Phone을 구성하는 부품의 종류가 CPU, Camera에서 구성하는 부품이 더 증가한다는 의미이다.

두 번째 요구사항은, LG, Samsung이 아니라 Apple, 샤오미 등 새로운 제조 업체의 부품을 Factory에서 지원할 수 있어야 한다는 의미이다.


첫 번째 요구사항을 반영시킨다고 생각해보자.

스피커, 마이크, 그래픽카드, 액정, 케이스, 홈버튼, 음량버튼, 전원케이블, 이어폰 등을 추가시킨다고 가정하자.

1. 9가지 부품 해당하는 클래스를 만든다. 제조사가 2개이므로 2를 곱하여 총 18개의 클래스가 나온다.

  -> 클래스 18개 추가

2. 위 9가지 부품을 지원하는 Factory를 모두 만든다.

  -> 클래스 9개 추가

3. Phone 클래스의 레퍼런스 변수를 9개 추가로 선언하고, setter를 9개 추가한다.

  -> Phone 클래스 변화

4. Client에 위 9개 팩토리를 생성하여 9개의 setter를 사용하여 객체를 바인딩한다.

  -> Client 클래스 변화


즉, 부품이 하나 추가될 때마다 거의 모든 소스코드가 변경된다.


어쨋거나 첫 번째 요구사항을 만족시켰다고 가정하자. 

이제 두 번째 요구사항을 반영시킨다고 생각해보자.

1. Apple에서 생산하는 11개의 부품 클래스를 선언한다.

 -> 11개 클래스 추가

2. Enum 클래스(VendorID)에 새로운 제조업체 Apple을 추가한다.

 -> Enum 클래스 변경

3. Apple에서 생산하는 11개의 부품을 지원해야하므로, 11개의 Factory 클래스를 수정한다.

 -> 클래스 11개 변경


즉, Client 코드는 수정되지 않으나, Factory 클래스의 변경이 많다.



4. 해결방안


객체 생성에 있어서 대표적인 Factory Method Pattern을 적용하였지만, 만능은 아니라는 것을 알 수 있다.


위 소스코드는 추상적인 객체인 CPU, Camera 등을 선언하여 Client의 소스코드 변경을 최소화 하긴 했지만, 늘어나는 클래스의 수에 대해서는 패턴 적용이라는 말이 무색할 정도로 많은 변화를 주어야 한다.


즉, 기존의 Factory Method Pattern만을 이용한 객체 생성은, 관련있는 여러 개의 객체를 일관성 있는 방식으로 생성하는 경우 많은 코드 변경이 발생한다.


여기서 '관련있는 여러 개의 객체를 만드는 경우'라는 말은, '한 제조사에서 생성하거나, 한 곳에서 다 같이 만들어져도 이상하지 않은 경우'를 말한다.


한 제조사에서 생성하는 것들을 각 부품 팩토리 클래스에서 따로 만들기 때문에, 클래스가 추가될 때 마다 소스의 변경이 일어난다는 말이다.


주의할 점은 '관련있는 여러 개의 객체'이다.


모두가 그런 것은 아니지만, 많은 대기업들의 경우, 자사의 제품을 만들어서 파는 경우에 비용을 최소화하기위해 외주/수주를 맡기지 않고 독자적인 생산 시스템을 구축한다.


한 회사에서 만드는 부품의 집합체를 '관련있는 여러 객체'라고 정의할 수 있다.

즉, A라는 회사에서 a1, a2, a3라는 자사 부품을 만들고, B라는 회사에서 b1, b2, b3라는 자사 부품을 만들어내는 경우이다.

이러한 것을 '객체들 사이의 관련성이 있다'라고 표현하는 것이다.


이러한 경우 소스코드의 변경을 '최소화'하는 것이 Abstract Factory Pattern이다.

먼저 Factory Method와 Abstract Factory의 차이점은 많은데, 대표적인 한 가지를 들자면 다음과 같다.


Factory Method Pattern : 각 종류별로 별도의 Factory 클래스를 사용하여 일관성있게 생산해낸다.

Abstract Factory Pattern : 완제품에 가까운 특정 물건 전용의 팩토리를 사용하여, 관련된 객체들을 일괄적으로 만들어 낸다.


위 2개의 차이점을 조금 더 쉽게 설명해보겠다.

Phone이라는 객체가 있다면, 이는 CPU, Camera등과 같은 부품들로 구성될 것이다.


Factory Method Pattern은 부품 객체(CPU, Camera)를 각각 만드는 Factory를 정의한 후, 상위 제품(Phone)으로 갈수록 조합이 된다.

즉, Bottom - Top 의 관점(작은 것부터 만들어서 큰 것을 만들어 나감)으로 구현한다. 


Abstract Factory Pattern은 처음부터 완제품(Phone)에 가까운, 상위 제품을 만드는 Factory를 정의한 후, 해당 상위 제품(Phone)을 구성하는 부품 객체(CPU, Camera)를 만든다.

즉, Top - Bottom의 관점(큰 것부터 만들어서 작은 것을 만들어 나감)으로 구현한다.


예를 들면, CPUFactory, CameraFactory와 같이 부품별로 Factory 클래스를 만드는 대신,

SamsungPhoneFactory, LGPhoneFactory 등과 같이 완제품에 가까운 것을 찍어내는 Factory클래스를 만드는 것이 좋다는 말이다.


전자는 삼성에서 생산하는 모든 부품 객체를 만드는 역할을 하며, 후자는 LG에서 생산하는 모든 부품 객체를 만드는 역할을 한다.


결론을 내리자면, 객체를 찍어내는 Factory를 만드는 것은 동일하다.

단, '부품 별로' 만드는 것이 아니라, '제조 업체 별로' 만드는 것이다.


여기서, Factory 클래스와의 결합도를 줄이기 위해 추상적인 Factory를 정의한다.

이 추상적인 Factory는 VendorID값을 받아서 특정 제조업체의 Factory를 생성한다.


이제 요구사항을 반영했을 때의 소스코드의 변경은 어떻게 될가.


첫 번째 요구사항을 적용할 때는, 사실 팩토리 메서드 패턴을 적용할 때와 크게 다르지 않다.

1. 9가지 부품에 해당하는 클래스를 만든다. 제조사가 2개이므로 2를 곱하여 총 18개의 클래스가 나온다.

  -> 클래스 18개 추가

2. SamsungPhoneFactory, LGPhoneFactory, PhoneFactory에서 해당 부품을 만드는 것을 지원하도록 9개의 createXXX() 메서드를 추가한다.

 -> 클래스 3개 변화

3. Phone 클래스의 레퍼런스 변수를 9개 추가로 선언하고, setter를 9개 추가한다.

  -> Phone 클래스 변화

4. Client에 factory.createXXX()와 같은 메서드를 9번 수행하여 현재 팩토리로부터 부품을 만들어야 할 것이다. 또한, setter를 사용해야하므로 setter 메서드 또한 9번 사용될 것이다.

 -> Client의 변경


많긴 하지만 얼핏 보기에 Factory Method Pattern보다는 적어보인다.


그렇다면 두 번째 요구사항을 적용한다고 해보자. 팩토리 메서드 패턴을 적용할 때와 다른 점에 주목하기 바란다.

1. Apple에서 만드는 구체 부품 클래스 11개를 추가한다.

 -> 클래스 11개 추가

2. Enum 클래스(VendorID)에 새로운 제조업체 Apple을 추가한다.

 -> Enum 클래스 변경

3. ApplePhoneFactory를 추가한다. 내용으로는 11개의 부품을 만드는 createXXX()메서드를 선언한다.

 -> 클래스 1개 추가


공통적으로 추가/변경되는 부분을 제외하면, Factory Method Pattern와 Abstract Factory Pattern의 차이는 다음과 같다.


Factory Method Pattern은 구성되는 부품이 n개가 추가되었을 때, n개의 Factory 클래스가 추가된다.

Abstract Factory Pattern은 구성되는 부품이 n개 추가되었을 때, 제조사의 갯수 만큼의 Factory 클래스 변경이 일어난다.


Factory Method Pattern은 제조사 1개가 추가되었을 때, 11개의 부품을 만들어야 하는 경우 11개의 클래스를 수정해야한다.

Abstract Factory Pattern은 제조사 1개가 추가되었을 때, 1개의 클래스만 추가하면 된다.


근본적으로, '새로운 클래스의 등장으로 인한 소스코드 변경'은 불가피하다.

그렇다면, 그것을 최소화 하는 방법이 최선이 된다.


물론, 다른 디자인 패턴을 적용한다면 이 역시도 개선될 수 있다.


Abstract Factory Patter에서, 관련있는 객체를 한곳에서 생성하는 Factory의 추상적인 개념을 AbstractFactory라고 한다.

그리고, 관련있는 객체를 실질적으로 만드는 구체적인 개념을 ConcreteFactory라고 한다.


만들어지는 부품의 추상적인 개념을 AbstractProduct라고 하며,

구체적인 개념을 ConcreteProduct라고 한다.



5. Solution Code


 - AbstractProduct1, 2

public abstract class AbstractProduct1 {

// Template Method
public void do1() {
do1Primitive();
}

// Template Method
public void do2() {
do2Primitive();
}

// Primitive Method 또는 Hook Method
protected abstract void do1Primitive();
protected abstract void do2Primitive();
}
public abstract class AbstractProduct2 {

// Template Method
public void do3() {
do3Primitive();
}

// Primitive Method 또는 Hook Method
protected abstract void do3Primitive();
}


팩토리로부터 만들어질 객체를 추상화한 AbstractProduct1, 2이다. 예제에서는 CPU, Camera 등에 해당된다.


 - A/BConcreteProduct1, 2


public class AConcreteProduct1 extends AbstractProduct1 {
@Override
protected void do1Primitive() {
System.out.println("AConcreteProduct1# do1Primitive()");
}

@Override
protected void do2Primitive() {
System.out.println("AConcreteProduct1# do2Primitive()");
}
}
public class AConcreteProduct2 extends AbstractProduct2 {
protected void do3Primitive() {
System.out.println("AConcreteProduct2# do3Primitive()");
}
}
public class BConcreteProduct1 extends AbstractProduct1 {
@Override
protected void do1Primitive() {
System.out.println("BConcreteProduct1# do1Primitive()");
}

@Override
protected void do2Primitive() {
System.out.println("BConcreteProduct1# do2Primitive()");
}
}
public class BConcreteProduct2 extends AbstractProduct2 {
protected void do3Primitive() {
System.out.println("BConcreteProduct2# do3Primitive()");
}
}

AbstractProduct1, 2를 상속받아 실질적으로 구현되는 ConcreteProduct이다.

A와 B는 제조사를 의미하며,

1과 2는 부품의 종류를 의미한다.


예를 들어, 

AConcreteProduct1은 예제에서 삼성에서 생산하는 CPU

AConcreteProduct2는 예제에서 삼성에서 생산하는 Camera

BConcreteProduct1은 예제에서 LG에서 생산하는 CPU

BConcreteProduct2은 예제에서 LG에서 생산하는 Camera

등으로 생각할 수 있다.


 - AbstractFactory

public abstract class AbstractFactory { // 추상 부품을 생성하는 추상 팩토리
public abstract AbstractProduct1 createAbstractProduct1();
public abstract AbstractProduct2 createAbstractProduct2();
}

부품을 생성하는 팩토리들에 대한 추상적인 개념이다.

이 Abstract Factory 클래스는, Product Factory들의 추상적인 개념이 된다.

Product Factory는 결국 구체적인 객체를 만들어낸다. 


다양한 객체를 만들 수 있어야 하기 때문에, 부품마다 메서드를 선언하여 하나의 공장에서 관련있는 객체를 여러 개 찍어낼 수 있도록 한다.

즉, 여기서 만들어지는 Product1과 Product2는 같이 생산될 여지가 굉장히 다분한 (거의 100%에 가까운) 관계이다.


 - A/B ConcreteFactory

// 싱글턴 패턴을 적용한 AConcreteFactory.
// A사에 대한 구체적인 부품들을 생산한다.
public class AConcreteFactory extends AbstractFactory {

private static AbstractFactory factory;

private AConcreteFactory() {
}

public static AbstractFactory getInstance() {
if (factory == null)
factory = new AConcreteFactory();
return factory;
}

// A사의 구체적 부품1 생산
@Override
public AbstractProduct1 createAbstractProduct1() {
return new AConcreteProduct1();
}

// A사의 구체적 부품2 생산
@Override
public AbstractProduct2 createAbstractProduct2() {
return new AConcreteProduct2();
}

}
// 싱글턴 패턴을 적용한 BConcreteFactory.
// B사에 대한 구체적인 부품들을 생산한다.
public class BConcreteFactory extends AbstractFactory {

private static AbstractFactory factory;

private BConcreteFactory() {
}

public static AbstractFactory getInstance() {
if (factory == null)
factory = new BConcreteFactory();
return factory;
}

// B사의 구체적 부품1 생산
@Override
public AbstractProduct1 createAbstractProduct1() {
return new BConcreteProduct1();
}

// B사의 구체적 부품2 생산
@Override
public AbstractProduct2 createAbstractProduct2() {
return new BConcreteProduct2();
}

}

 추상팩토리를 상속받아 실질적으로 객체를 생산해내는 ConcreteFactory들이다.

구체적인 팩토리들은 단순히 객체를 찍어내는 역할만 하기 때문에, 싱글톤 패턴으로 구현하여 소스의 낭비를 최소화 하였다.


create메서드가 2개가 있는데, 각각 A사에서 만드는 부품1과 A사에서 만드는 부품2이다.

같은 회사에서 만드므로, 관련있는 객체라고 볼 수 있다.


따라서 한 팩토리에서 이를 일괄적으로 제공하는 것이다.

 

 - AbstractFactoryFactory

// Factory Method Pattern 사용
// ConcreteProductFactory를 생성해내는 Factory
public class AbstractFactoryFactory {

// VendorID에 따라 ConcreteFactory를 생성하여 추상적 개념인 AbstractFactory 타입으로 반환
public static AbstractFactory getFactory(VendorID vendorID) {
AbstractFactory factory = null;

switch (vendorID) {
case A:
factory = AConcreteFactory.getInstance(); // A 팩토리 생성
break;
case B:
factory = BConcreteFactory.getInstance(); // B 팩토리 생성
break;
}
return factory;
}
}

Vendor값에 따라서 ConcreteFactory를 다르게 생산하는 AbstractFactory에 대한 Factory이다.


Factory의 생산에 있어서는 Factory Method Pattern을 사용하고,

각 부품에 해당하는 객체의 생성에 있어서는 Abstract Factory Pattern을 사용한 경우이다.

 

AbstractFactoryFactory의 getFactory()가 팩토리 메서드이다.

제조 업체별 Factory 객체를 생성하는 코드가 AbstractFactoryFactory 클래스의 getFactory()를 사용해 캡슐화되었다.

 

만약 ProductFactory를 만들 때 Factory Method Pattern을 사용하지 않는다면, Client 단에서 VendorID에 따라 분기를 하여야 하는데, 이는 복잡한 소스코드를 야기시킨다.


 - Client

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

VendorID vendorID = VendorID.A;
//VendorID vendorID = VendorID.B;

AbstractFactory factory = AbstractFactoryFactory.getFactory(vendorID);

AbstractProduct1 abstractProduct1 = factory.createAbstractProduct1();
AbstractProduct2 abstractProduct2 = factory.createAbstractProduct2();

abstractProduct1.do1();
abstractProduct1.do2();
abstractProduct2.do3();
}
}

VendorID에 따라서 ConcreteFactory를 생산하고, 이를 AbstractFactory라는 추상적인 개념을 사용하여 ConcreteProduct1, 2를 생산해낸다.

ConcreteProduct1, 2 또한 AbstractProduct를 통해 추상화된 객체로서 다루어진다.


예제에서는 Phone을 구성하여서 해당 정보를 출력하게 했지만, 중요한것은 어떤 구조로 클래스를 짜서 객체를 만드는지에 대한 것이므로 중요하지 않다.


실행 결과는 다음과 같다.


6. Abstract Factory Pattern

관련있는 여러 종류의 객체를 일관된 방식으로 생성하는 경우에 사용한다. 한 회사의 제품을 위한 코드는 한 회사의 부품을 이용할 것이다. 만약 다른 회사의 부품을 지원하도록 코드를 수정해야 한다면 일관성 있게 다른 회사의 클래스로 변경되어야 한다.


이런 경우 부품별로 Factory를 정의하는 대신 관련 객체들을 일관성 있게 생성할 수 있도록 Factory 클래스를 정의하는 것이 효과적일 것이다.

 

Factory Method Pattern은 한 종류의 객체만을 생산한다면, Abstract Factory Pattern은 여러 종류의 객체를 생산한다. 단, 제조업체가 같다던지 등의 관련이 있는 객체의 생성을 모두 지원한다.

 

 

따라서 Factory Method Pattern은 생성되는 객체의 종류 마다 Factory가 많아지고, 제조업체에 따라 다른 객체가 생성되는 등 조건이 붙을수록 추가/변경이 잦다.

 

하지만 Abstract Factory Pattern의 경우, 객체의 종류가 추가된다고 하더라도 소스코드의 추가/변경을 최소화할 수 있다.

 

소스코드에서 봤듯이, 아무리 생성패턴이라고 해도 소스코드의 추가/변경은 불가피하다.

하지만 상황에 따라 이를 최소화하는 방법을 적용한 것이다.

 

- UML & SequenceDiagram

 - AbstractFactory : 실제 팩토리 클래스의 공통 인터페이스, 각 제품의 부품을 생성하는 기능을 추상 메서드로 정의

 - ConcreteFactory : 구체적인 팩토리 클래스로 AbstractFactory 클래스의 추상 메서드를 오버라이드함으로써 구체적인 제품을 생성

 - AbstractProduct : 제품의 공통 인터페이스

 - ConcreteProduct : 구체적인 팩토리 클래스에서 생성되는 구체적인 제품


 

7. 장단점

 

 

 

Comments