Typescript - (디자인 패턴) 추상화 팩토리 vs 팩토리 메서드

DatQueue·2022년 8월 3일
4
post-thumbnail

굉장히

난해한 시간이였다. “팩토리 메서드 패턴” 과 “추상 팩토리 패턴”에 관한 구글에 있는, 어지간한 블로그와 글은 다 본 것 같았다.

팩토리 메서드 패턴으로 작성된 코드와 추상 팩토리 패턴으로 작성된 코드를 연이어 보았다.

각각의 코드를 이해하는데에는 큰 어려움이 없었다. 즉, 문법적인 내용은 큰 어려움이 없었다.

더군다나 하나하나씩 따로 생각하면 전체적 로직의 구조에 관해서도 파악하는데 난관은 없었다.

하지만

도저히, 두 패턴 (팩토리 메서드 , 추상 팩토리) 의 차이를 찾을 수가 없었다. 어떻게든 찾으려한 시간만 합치만 야구 경기를 한 번 보고와도 될 시간이었을 것이다.

두 패턴 모두 인터페이스를 통해 몇 가지의 클래스를 캡슐화시키고 어떠한 팩토리를 통해 최종 객체를 생성한 뒤 , 팩토리에서 정의한 메소드 등을 이용하여 원하는 결과를 클라이언트에서 정의한 값에 따라 얻게 되는 구조였다.

뭔가 추상화 팩토리가 조금 더 캡슐화가 많이 되있어서 팩토리 메서드의 상위 호환인가 생각했는데..

그것도 아니라고 모든 블로그에서 말하고 있었다.

즉, 각자 패턴의 장 단점은 존재할 것이고, 어느 상황에 어떤 패턴을 써야할 지가 명확히 머릿속에서 그려져야 할 것인데 , 단 하나의 명확성도 그려지지 않았다.

그럼에도 불구하고

직접 각 패턴에 관한 코드를 작성해보면서 뭐라할까 … 손의 감각으로써 익힌 뒤 , 머리로 전달하는 방법을 선택하였다. 도저히 눈으로 보고만 있자니 더욱 미쳐버릴 것 같았다.

사실, 이러한 디자인 패턴의 경우엔 객체 지향적인 언어에서 등장하는 개념이라봐도 무방하다.

해당 내용에 관해 찾아볼 때 도저히 자바스크립트언어로써는 이해할 수가 없었고, 타입스크립트로 작성된 로직은 거의 없었다.

대부분의 블로그 또는 글은 Java로 작성된 패턴 로직이 차지하고 있었고 , 나 또한 Java 코드를 통해 이해하는 것이 오히려 더 편했다.

우리가 알아볼 로직은 컴퓨터를 생산하는 것에 관한 코드이다.

간단히 말하자면 컴퓨터를 만드는데에는 마우스, 키보드와 같은 부품이 있을 것이고 Samsung, LG와 같은 Brand가 존재할 것이다. 그에 따른 컴퓨터 생산 로직을 작성할 것이다.

( 비교에 중점을 둘 것이므로 각 패턴에 대한 자세한 분석은 생략한다. )

먼저,

<팩토리 메서드 패턴>으로 작성해보았다.

export {};

interface Keyboard {}

class LGKeyboard implements Keyboard {
  constructor() {
    console.log('LG 키보드 생성');
  }
}

class SamsungKeyboard implements Keyboard {
  constructor() {
    console.log('Samsung 키보드 생성');
  }
}

interface Mouse {}

class LGMouse implements Mouse {
  constructor() {
    console.log('LG 마우스 생성');
  }
}

class SamsungMouse implements Mouse {
  constructor() {
    console.log('Samsung 마우스 생성');
  }
} 

class KeyboardFactory {                
  private keyboard! : Keyboard;
  public createKeyboard(type : string) : Keyboard {
    switch (type) {
      case "LG":
        this.keyboard = new LGKeyboard();
        break;
      case "Samsung":
        this.keyboard = new SamsungKeyboard();
        break;
    }
    return this.keyboard;
  }
}

class MouseFactory {                     
  private mouse! : Mouse;
  public createMouse(type : string) : Mouse {
    switch (type) {
      case "LG":
        this.mouse = new LGMouse();
        break;
      case "Samsung":
        this.mouse = new SamsungMouse();
        break;
    }
    return this.mouse;
  }
}

class ComputerFactory {                            // 객체 생성
  public createComputer(type : string) {
    const keyboardFactory = new KeyboardFactory();
    const mouseFactory = new MouseFactory();

    keyboardFactory.createKeyboard(type);
    mouseFactory.createMouse(type);
    console.log(`${type} computer 생성!`);
  }
}

class Client {
  public static main() {
    const computerFactory = new ComputerFactory();
    computerFactory.createComputer("LG");
  }
}

Client.main();

결과는 쉽게 유추할 수 있을 것이다.

간단히 위의 로직을 설명해보자.

  • LGKeyboard와 SamsungKeyboard 클래스를 생성하였고 이를 캡슐화하는 Keyboard interface를 정의하였다. 그리고 KeyboardFactory 클래스의 입력 값에 따라 LGKeyboard 객체를 생성할 지, SamsungKeyboard 객체를 생성할 지 결정한다. (여기선 결정만 하는 단계이다.)
  • Keyboard와 동일하게 Mouse또한 작성할 수 있다.

  • 다음으로 ComputerFactory 클래스를 구현한다.

    해당 클래스가 진정 객체를 생성하는 팩토리 클래스이다.

ComputerFactory 클래스는 KeyboardFactory와 MouseFactory의 클래스의 인스턴스 객체인 keyboardFactory와 mouseFactory를 생성한 뒤, 어떤 브랜드의 키보드와 마우스를 생산할 것인지 결정한다.

  • 마지막으론 Client 클래스를 구현한다. 브랜드명을 파라미터 type에 string의 형태로 넣으줌으로써 최종 컴퓨터를 생산할 수 있게 된다.

이처럼 팩토리 메서드를 사용하여, 컴퓨터를 생산해 보았다.

그런데

컴퓨터의 구성품은 마우스, 키보드 뿐만 아니라 본체 구성품들을 비롯해 스피커, 모니터, 여러 선들 등 단순히 몇 가지가 아니다.

기존의 KeyboardFactory , MouseFactory와 같이

class KeyboardFactory {
  private keyboard! : Keyboard;
  public createKeyboard(type : string) : Keyboard {
    switch (type) {
      case "LG":
        this.keyboard = new LGKeyboard();
        break;
      case "Samsung":
        this.keyboard = new SamsungKeyboard();
        break;
    }
    return this.keyboard;
  }
}

class MouseFactory {
  private mouse! : Mouse;
  public createMouse(type : string) : Mouse {
    switch (type) {
      case "LG":
        this.mouse = new LGMouse();
        break;
      case "Samsung":
        this.mouse = new SamsungMouse();
        break;
    }
    return this.mouse;
  }
}

스피커와 모니터를 추가하고 싶다면 SpeakerFactory , MonitorFactory를 클래스로써 만들어주어야 할 것이다.

또한 객체 생성을 수행하게 되는 ComputerFactory에선

class ComputerFactory {                            // 객체 생성 추가
  public createComputer(type : string) {
    const keyboardFactory = new KeyboardFactory();
    const mouseFactory = new MouseFactory();
		const speakerFactory = new SpeakerFactory();
		const monitorFactory = new MonitorFactory();

    keyboardFactory.createKeyboard(type);
    mouseFactory.createMouse(type);
		speakerFactory.createSpeaker(type);
		monitorFactory.createMonitor(type);
    console.log(`${type} computer 생성!`);
  }
}

다음과 같이 객체를 추가하여 생성해줘야 할 것이다. 만약 컴퓨터에 추가해야 할 구성품이 늘어난다면 해당 코드는 훨씬 더 길어질 것이다.

여기서

코드를 살펴보면, 위의 코드로 보았을 때, KeyboardFactoryMouseFactory에서 type을 이용해 브랜드가 LG 일 경우와 Samsung일 경우로 조건을 걸어 LG일 경우 LGMouse , LGKeyboard를 , Samsung일 경우 SamsungMouse , SamsungKeyboard를 선택하도록 하였다.

그런데, 애초에 LG 컴퓨터를 만들 때는 LGMouseLGKeyboard를 사용할 것이고 Samsung 컴퓨터를 만들 때엔 SamsungMouse, SamsungKeyboard를 사용할 것인데 , 굳이 위와 같은 조건을 달아줄 필요는 없을 것이다.

그러므로 KeyboardFactoryMouseFactory는 위의 “브랜드에 따른 컴퓨터 생산 로직 ” 에는 불필요할 것이다. =⇒ 제거해준다.

지금부터

“추상 팩토리 패턴” 을 이용하여 위의 팩토리 패턴을 로직의 방향성에 맞게 개선해보겠다.

참고로 , “개선”이라고 말했지만 절대 “추상 팩토리 패턴”이 더 우월하고 상위 호환이라는 뜻은 아니다. 앞전에도 언급하였듯이 “추상 팩토리 패턴”이 더 필요한 로직이 있고 , “팩토리 메서드 패턴”이 더 필요한 로직이 존재하기 마련이다.

단지, “브랜드에 따라 컴퓨터를 생산” 하는 해당 로직에는 “추상 팩토리 패턴”이 조금 더 좋을 것이라 제시하는 것이다.

위에서 제시하였듯이 KeyboardFactoryMouseFactory는 제거하여 준다.

그 대신, SamsungComputerFactoryLGComputerFactory 클래스를 정의하여 준다. 그리고 이들을 “캡슐화” 하는 ComputerFactory인터페이스를 정의한다.

코드를 확인해보자.

기존의 LGKeyboard , LGMouse , SamsungKeyboard , SamsungMouse클래스는 동일하다.

export {}

  // Abstract Factory
  // 특정 컴퓨터는 같은 제조사인 구성품들로 생산되어야한다.
  // 다시 말하면, SamsungComputer객체는 항상 삼성 마우스, 키보드, 모니터 객체들이 묶여서 사용된다.
  // 즉, 객체를 일관적으로 생산해야할 필요가 있다.

interface Keyboard {}

class LGKeyboard implements Keyboard {
  constructor() {
    console.log('LG 키보드 생성');
  }
}

class SamsungKeyboard implements Keyboard {
  constructor() {
    console.log('Samsung 키보드 생성');
  }
}

interface Mouse {}

class LGMouse implements Mouse {
  constructor() {
    console.log('LG 마우스 생성');
  }
}

class SamsungMouse implements Mouse {
  constructor() {
    console.log('Samsung 마우스 생성');
  }
} 

interface ComputerFactory {
  createKeyboard() : Keyboard;
  createMouse() : Mouse;
}

class LGComputerFactory implements ComputerFactory {
  public createKeyboard() : LGKeyboard {
    return new LGKeyboard();
  }
  public createMouse() : LGMouse {
    return new LGMouse();
  }
}

class SamsungComputerFactory implements ComputerFactory {
  public createKeyboard() : SamsungKeyboard {
    return new SamsungKeyboard();
  }
  public createMouse() : SamsungMouse {
    return new SamsungMouse();
  }
}

class FactoryOfComputerFactory {
  private computerFactory! : ComputerFactory;
  public createComputer(type : String) {
    switch (type) {
      case "LG" :
        this.computerFactory = new LGComputerFactory();
        break;

      case "Samsung" :
        this.computerFactory = new SamsungComputerFactory()
    }
    this.computerFactory.createKeyboard();
    this.computerFactory.createMouse();
    console.log(`${type} computer 생성!`)
  }
}

class Client {
  public static main() {
    const factoryOfComputerFactory = new FactoryOfComputerFactory();
    factoryOfComputerFactory.createComputer("LG");
  }
}

Client.main();  // 결과는 동일

SpeakerMonitor와 같은 다른 부품들을 추가한다면?

Speaker와 Monitor 인터페이스를 만든 후 다음과 같이 ComputerFactory 인터페이스에서 참조하게 한다.

interface ComputerFactory {
  createKeyboard() : Keyboard;
  createMouse() : Mouse;
  createSpeaker() : Speaker;
  createrMonitor() : Monitor;
}

class LGComputerFactory implements ComputerFactory {
  public createKeyboard() : LGKeyboard {
    return new LGKeyboard();
  }
  public createMouse() : LGMouse {
    return new LGMouse();
  }
	public createSpeaker() : LGSpeaker {      // 추가
		return new LGSpeaker();
	}
	public createMonitor() : LGMonitor {      // 추가
		return new LGMonitor();
	}
}

따로 SpeakerFactoryMonitorFactory를 만들어 브랜드에 따른 조건을 다는 것보다 훨씬 깔끔한 코드가 만들어진다.

분석하기

팩토리 메서드 패턴이

조건에 따라 객체 생성을 팩토리 클래스로 위임하여, 팩토리 클래스에서 객체를 생성하는 패턴이었다면,

추상 팩토리 패턴은

서로 관련이 있는 객체들(LGMouse와 LGKeyboard 또는 SamsungKeyboard와 SamsungMouse)을 묶어서 팩토리 클래스(LGComputerFactory 또는 SamsungComputerFactory)로 만들고, 해당 팩토리를조건에 따라 생성하게 되는 새로운 팩토리(FactoryOfComputerFactory)를 만들어 객체를 생성한다.

만약,

“브랜드에 따른 컴퓨터 생산” 이 아닌

서로 다른 회사의 부품을 통해 자동차를 생산하는 경우라면 어떨까?

예를 들면, 현대 자동차는 물론 현대 계열사의 부품을 사용하기도 하지만 해당 계열사 뿐 아니라 다양한 제조회사를 통해 전장 부품을 얻게 된다. 그리고 해당 제조회사의 전장 부품을 이용하는 업체는 현대 자동차 뿐만 아니라 쌍용, 기아 등 여러 업체가 될 수도 있다.

이럴 경우, 우리가 수행했던 “추상 팩토리 패턴”처럼 진행하는 것이 바람직할까?

추상 팩토리 팩턴”의 주된 개념은 여러 관련된 객체들을 하나의 팩토리안에서 한꺼번에 담아 캡슐화하는 것이다. 그런데 휴대폰처럼 자회사 부품만 쓰는 경우가 아닌 , 자동차처럼 여러 제조부품회사의 부품을 조합해 생산하는 경우는 “관련된 객체”는 맞지만 “하나의 팩토리”안에 넣을 순 없을 것이다.

아래 로직을 통해 알아보자.

만약 HyundaiCarFactory와 , KIACarFactory가 존재하고 현대자동차에 전장 부품을 납품하는 제조 회사 A , B , C , D 회사가 있다고 하자.

이때, 현대 자동차는 바퀴는 A회사의 바퀴를 사용하고, 조명은 B회사, 범퍼는 C회사 , 문짝은 D회사의 부품을 사용하게 되고,

기아 자동차는 바퀴는 B회사의 바퀴를 사용하고, 조명은 A회사, 범퍼는 D회사, 문짝은 C회사의 부품을 사용한다고 하자.

그럼 다음과 같이 코드를 작성할 수 있을 것이다.

<추상 팩토리 패턴 적용>

interface CarFactory {
  createWheels() : Wheels;
  createLights() : Lights;
  createBumper() : Bumper;
  createDoor() : Door;
}

class HyundaiCarFactory implements CarFactory {
  public createWheels() : AWheels {
    return new AWheels();
  }
  public createLights() : BLights {
    return new BLights();
  }
  public createBumper() : CBumper {
    return new CBumper();
  }
  public createDoor() : DDoor {
    return new DDoor();
  }
}

class KIACarFactory implements CarFactory {
  public createWheels() : BWheels {
    return new BWheels();
  }
  public createLights() : ALights {
    return new ALights();
  }
  public createBumper() : DBumper {
    return new DBumper();
  }
  public createDoor() : CDoor {
    return new CDoor();
  }
}

지금이야 각 부품( Wheel, Lights ,Bumber , Door )마다 전부 다른 제조 회사로써 정의하였지만 만약

아래와 같이 , 기아자동차가 바퀴도 A회사의 바퀴를 이용하고, 조명도 A회사의 조명도 이용할 수 도 있다.

class KIACarFactory implements CarFactory {
  public createWheels() : AWheels {
    return new AWheels();
  }
  public createLights() : ALights {
    return new ALights();
  }
  public createBumper() : DBumper {
    return new DBumper();
  }
  public createDoor() : CDoor {
    return new CDoor();
  }
}

만약 부품이 더 추가된다면 어떨까 ?

class KIACarFactory implements CarFactory {
  public createWheels() : AWheels {
    return new AWheels();
  }
  public createLights() : ALights {
    return new ALights();
  }
  public createBumper() : DBumper {
    return new DBumper();
  }
  public createDoor() : CDoor {
    return new CDoor();
  }
	public createBrake() : ABrake{
	    return new ABrake();
	  }
	public createSideMirror() : CMirror{
	    return new CMirror();
	  }
	public createBackMirror() : DMirror{
	    return new DMirror();
	  }
}

정말 보기만 해도 지저분하다.

추상 팩토리”의 “핵심” 이라고 할 수 있는 “일관성”은 어디서도 찾아볼 수 없다.

나의 생각

“팩토리 메서드 패턴” 과 “추상 팩토리 패턴”은 어떻게 보면 둘 다 “추상화”를 통해 “캡슐화”를 진행하고 비슷한 로직을 “팩토리화”한다는 점에서 굉장히 비슷해 보일 수 있다.

사실 여기서, “그렇지 않다!” 라고 말할 순 없다. 비슷한 역할을 하는 것이 맞음에 틀림없기 때문이다.

현 시점에서도 정확히 말할 순 없지만, 어떻게 보면 “추상 팩토리 패턴”이 “팩토리 메서드 패턴” 보다 조금 더 “캡슐화”를 하였다고 말할 수 있을 거 같다. 쉽게 말하자면 연관되어있는 로직끼리 조금 더 세밀하게 추상화를 시키는 것이다.

해당 게시글은 디자인 패턴, 그 중에서도 생성패턴의 대표적 패턴인 "팩토리 메서드 패턴" 과 "추상화 팩토리 패턴"에 관한 비교 글이다.
정확한 사실 전달이 아닌 생각을 공유하고자 작성한 글인만큼 다양한 생각들을 나누고 싶다.

이번 주제에 관해서 조금 더 명확한 생각이 머릿속에서 그려지면 또 다뤄보고자 한다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

1개의 댓글

comment-user-thumbnail
2024년 9월 24일

저도 두 패턴의 차이점이 잘 이해되지 않았는데 이 글 보고 명확히 이해할 수 있었습니다
좋은 글 감사합니다!

답글 달기