-
[디자인 패턴 with TS] 팩토리 메서드 패턴으로 if문 정리하기강의노트/디자인패턴 with TS 2025. 7. 12. 16:55

유저는 모르더라도 개발자는 Mac OS, Windows에서 동작하는 서로 다른 그림판이 있으며, 서로 달라야 함을 알고 있다. 지금 강의에서 사용하고 있는 예제는 웹 서비스이기 때문에 IE과 Chrome 그림판을 생각해볼 수 있다. IE에서는 최신 기능은 고사하고 canvas 기능도 사용하지 못할 수도 있기 때문에 이 두 그림판의 구현이 달라야 한다.
1. 심플 팩토리 패턴 (Simple Factory)
여러가지가 팩토리 패턴 중에서 가장 간단한 형태의 패턴이다. 보통 팩토리 패턴은 타입을 받아서 그 타입에 따른 객체를 반환한다. 이 패턴은 강의에서 배우는 23가지 패턴에 포함되지 않는다. 이 심플 팩토리 패턴을 기반으로 더 복잡한 팩토리 패턴을 배울 것이다.
심플 팩토리 패턴은 단일책임원칙 위반이다. 이 패턴은 타입을 받아서 if문으로 분기 처리하여 객체를 생성하고 있다. 단일책임원칙은 변경의 이유가 1가지여야 한다고 했다. 그런데 그림판의 브라우저 타입이 추가되거나, 그림판을 만드는 방식이 변해 getInstance() 메서드에 인자가 들어가면 또 다시 변경이 발생한다. 이러한 이유로 단일책임원칙에 위배된다.
실무 코드에는 조건문에 들어가는 심플 팩토리 대신 팩토리 메서드 패턴을 권장한다.
import ChromeGrimpan from './ChromeGrimpan.js'; import IEGrimpan from './IEGrimpan.js'; function grimpanFactory(type: string) { if (type === 'ie') { return IEGrimpan.getInstance(); } else if (type === 'chrome') { return ChromeGrimpan.getInstance(); } else if (type === 'safari') { return SafariGrimpan.getInstance(); } else { throw new Error('일치하는 type이 없습니다.'); } } function main() { grimpanFactory('ie'); grimpanFactory('chrome'); grimpanFactory('safari'); } main();디자인 패턴들은 보통 if문을 없애는데 주력한다. if문을 사용하면 else if의 추가나 조건문 내부 실행 코드가 변경 가능이 크며, 이로 인해 대부분 단일책임원칙을 위반하기 때문이다. if문 자제를 쓰지 못하게 되는 SOLID 원칙 중에서 가장 애매한 단일책임원칙은 적당한 선에서는 넘어가기로 한다.
2. 팩토리 메서드 패턴 (Factory method)
팩토리 메서드 패턴을 사용하면 심플 팩토리 메서드 패턴의 if문을 감쪽같이 제거함으로써 SOLID 원칙을 준수할 수 있다. 심플 팩토리와 비교하면, 심플 팩토리는 새로운 타입이 나왔을 때 else if 구문이 늘어나는 구조였다. 이로써 단일책임원칙(SRP) 뿐만 아니라 확장에는 열려있고 변경에는 닫혀있는 개방/폐쇠 원칙(OCP) 또한 위반한다. 반면에 팩토리 메서드 패턴은 if문을 완전히 없애고 별도의 객체 생성 코드를 추가하여 클라이언트 부분에서 사용할 수 있는 구조를 가진다. 기존 코드를 건드리지 않기 때문에 SRP와 함께 OCP 또한 만족한다. 또한 기존 클래스를 상속하여 자식 클래스를 생성할 수도 있다.
단점이라 하면 코드가 복잡해진다. 오히려 if 조건문이 더욱 직관적이다. SRP와 OCP를 만족시키기 위해 코드가 복잡해졌다. 그래서 if문을 제거하고 싶다거나, 조건에 따른 실행 함수의 매개변수가 매번 바뀌는 경우에 팩토리 메서드 패턴을 고려해볼 수 있겠다.
AbstractGrimpan.ts
추상 클래스(abstact class)는 타입스크립트에서만 볼 수 있는 클래스이다. 추상 클래스도 인터페이스와 같은 역할을 한다. 추상 클래스를 상속을 받은 일반 클래스는 무조건 abstact 속성의 메서드를 override 하여 구현해야 한다. 추상 클래스는 구현체의 공동된 부분을 가져오면 된다. 추상 클래스의 장점은 abstract 속성이 아닌 protected나 private 부분도 작성할 수 있다. 이러면 클래스에 실재로 존재하는 메서드가 된다. 구체적인 구현체인 ChromeGrimpan과 IEGrimpan에서는 extends해서 사용한다.
// AbstractGrimpan.ts export default abstract class Grimpan { protected constructor(canvas: HTMLElement | null) { if (!canvas || !(canvas instanceof HTMLCanvasElement)) { throw new Error('canvas 엘리먼트를 입력하세요.'); } } abstract initialize(): void; abstract initializeMenu(): void; static getInstance() {} }AbstractGrimpanFactory.ts
추상 팩토리는 꼭 추상 클래스를 가져와야 한다. ChromeGrimpan이나 IEGrimpan과 같이 구체적인 구현제를 가져와 사용하면 안된다. 사실상 타입만 가져와서 사용했다고 생각하면 된다. 추상 클래스인 Grimpan과 AbstractGrimpanFactory는 인터페이스의 역핳을 하고 ChromeGrimpan과 IEGrimpan은 구체적인 구현체인 콘크리트(Concrete class/implementation)이다.
콘크리트를 작성할 때는 꼭 내부 메서드를 override 해서 구현해주어야 하는데, 실수로 구현을 잊어벌리 수 있기 때문에 throw new Error를 추가해줄 수 있다. 이 부분으로 인해서 리스코프 치환 원칙을 위반할 수 있지만, AbstractGrimpanFactory 단독으로 쓰이는 일은 없으므로 그냥 넘어간다. (이후에 다시 언급하겠다.)
// AbstractGrimpanFactory.ts export default abstract class AbstractGrimpanFactory { static createGrimpan() { throw new Error('하위 클래스에서 구현하셔야 합니다.'); } }index.ts
추상 그림판 클래스의 구체적인 구현체로는 ChromeGrimpan과 IEGrimpan이 있드시, 추상 그림판 팩토리 클래스의 콘크리트 클래스를 만들어준다. AbstractGrimpanFactory를 extends 하면 된다. extends 하면 추상 팩토리 클래스에 존재하는 메서드를 override 해서 구현해주어야 한다. 이때 static을 붙여주어 클라이언트 부분인 main 함수에서 new 키워드를 사용하는 대신, createGrimpan 메서드를 사용해서 추상 팩토리의 구체적인 구현체의 유일한 인스턴스를 생성할 수 있다.
// index.ts import ChromeGrimpan from './ChromeGrimpan.js'; import IEGrimpan from './IEGrimpan.js'; import AbstractGrimpanFactory from './AbstractGrimpanFactory.js'; import Grimpan from './AbstractGrimpan.js'; class ChromeGrimpanFactory extends AbstractGrimpanFactory { static override createGrimpan() { return ChromeGrimpan.getInstance(); } } class IEGrimpanFactory extends AbstractGrimpanFactory { static override createGrimpan() { return IEGrimpan.getInstance(); } } // safari 추가 class SafariGrimpanFactory extends AbstractGrimpanFactory { static override createGrimpan() { return SafariGrimpan.getInstance(); } } function main() { // const grimpan = SafariGrimpanFactory.createGrimpan(); const grimpan = ChromeGrimpanFactory.createGrimpan(); grimpan.initialize(); grimpan.initializeMenu(); } main();ChromeGrimpan.ts
// ChromeGrimpan.ts import Grimpan from './AbstractGrimpan.js'; class ChromeGrimpan extends Grimpan { private static instance: ChromeGrimpan; initialize() {} initializeMenu() {} static override getInstance() { if (!this.instance) { this.instance = new ChromeGrimpan(document.querySelector('canvas')); } return this.instance; } } export default ChromeGrimpan;IEGrimpan.ts
// IEGrimpan.ts import Grimpan from './AbstractGrimpan'; class IEGrimpan extends Grimpan { private static instance: IEGrimpan; initialize() {} initializeMenu() {} static override getInstance() { if (!this.instance) { this.instance = new IEGrimpan(document.querySelector('canvas')); } return this.instance; } } export default IEGrimpan;3. 자바스크립트로 구현된 팩토리 메서드
추상 클래스는 인터페이스와 다르게 컴파일된 JS 파일에 코드가 존재한다. 그렇기 때문에 ChromeGrimpan이나 IEGrimpan에서 extends를 하여 공통된 구현부를 상속을 통해 재사용성을 키울 수 있다.
AbstractGrimpan.ts
인터페이스를 사용하면 추상 클래스와 달리 실제 구현부를 작성할 수 없고, static이나 abstract 사용할 수 없게 된다. 실제 구현부는 ChromeGrimpan이나 IEGrimpan에 작성해야 한다.
// AbstractGrimpan.ts export default interface Grimpan { initialize(): void; initializeMenu(): void; }AbstractGrimpan.js
인터페이스를 사용하게 되면 추상 클래스와 달리 코드가 대부분 남지 않는다.
// 1. 추상 클래스의 컴파일 결과 // export default abstract class Grimpan { // protected constructor(canvas: HTMLElement | null) { // if (!canvas || !(canvas instanceof HTMLCanvasElement)) { // throw new Error('canvas 엘리먼트를 입력하세요.'); // } // } // 2. 인터페이스의 컴파일 결과 export {};ChromeGrimpan.ts
// ChromeGrimpan.ts import Grimpan from './AbstractGrimpan.js'; class ChromeGrimpan implements Grimpan { private static instance: ChromeGrimpan; private constructor(canvas: HTMLElement | null) { if (!canvas || !(canvas instanceof HTMLCanvasElement)) { throw new Error('canvas 엘리먼트를 입력하세요.'); } } initialize() {} initializeMenu() {} static getInstance() { if (!this.instance) { this.instance = new ChromeGrimpan(document.querySelector('canvas')); } return this.instance; } } export default ChromeGrimpan;ChromeGrimpan.js
JS에서는 implements 부분이 사라진다. JS에서는 인터페이스도 추상 클래스처럼 가짜 클래스를 정의하여 무조건 상속 받아야 하며, 하위 클래스에서 메서드를 구현하는 구조를 많이 사용한다. JS에서는 인터페이스나 추상 클래스 타입이 존재하지 않기 때문에 AbstractGrimpanFactroy를 일반 class로 정의하여 사용한다.
// ChromeGrimpan.js class ChromeGrimpan { static instance; constructor(canvas) { if (!canvas || !(canvas instanceof HTMLCanvasElement)) { throw new Error('canvas 엘리먼트를 입력하세요.'); } } initialize() { } initializeMenu() { } static getInstance() { if (!this.instance) { this.instance = new ChromeGrimpan(document.querySelector('canvas')); } return this.instance; } } export default ChromeGrimpan;JS로 구현한 가짜 클래스 예시
JS에서 인터페이스 대용으로 이렇게 일반 클래스 객체를 정의하여 사용한다. 하지만 인터페이스와 추상 클래스를 지원하여 표현력이 풍부한 TS를 사용하는 것이 훨씬 더 낫다고 생각한다.
// AbstractGrimpanFactory.js class AbstractGrimpanFactory { static createGrimpan() { throw new Error('하위 클래스에서 구현하셔야 합니다.'); } } export defualt AbstractGrimpanFactory;4. 클라이언트 측면에서 더 확장성 높은 코드
index.ts 파일의 클라이언트 부분 코드(main)를 수정하면 훨씬 추상 클래스를 간단한 형태로 활용할 수 있을 것이다. 이제 클라이언트 부분에서는 IE 그림판이든 Chrome 그림판이든 상관없이 AbstractGrimpanFactory 모양이기만 하면 된다. 이렇게 모양을 통일시켜줌으로써, 클라이언트 입장에서 구체적인 구현은 신경쓰지 않고 동일한 형태로 사용할 수 있다. 하지만 해당 코드는 팩토리 메서드 코드 상에서 리스코프 치환 원칙 위반으로 타입 에러가 발생하게 된다.
// function main() { // // const grimpan = SafariGrimpanFactory.createGrimpan(); // const grimpan = ChromeGrimpanFactory.createGrimpan(); // grimpan.initialize(); // grimpan.initializeMenu(); // } // main(); // 타입 에러 발생: abstract static 을 사용하지 못하기 때문 function main(factory: typeof AbstractGrimpanFactory) { // const grimpan = SafariGrimpanFactory.createGrimpan(); const grimpan = factory.createGrimpan(); grimpan.initialize(); grimpan.initializeMenu(); } main(ChromeGrimpanFactory);만약, 억지로 리스코프 치완 원칙도 준수해줘야 한다면 아래와 같은 형태가 될 수 있다.
// AbstractGrimpanFactory.ts export default abstract class AbstractGrimpanFactory { static createGrimpan() { // throw new Error('하위 클래스에서 구현하셔야 합니다.'); // 리스코프 치환 원칙을 준수하는 예시 코드 return Grimpan.getInstance() as unknown as Grimpan; } }위와 같은 현상의 원인은 Abstract static을 타입스크립트에서 사용할 수 없기 때문이다.
다음 포스팅에서는 이에 대한 해결책으로 사용되는 추상 팩토리 (Abstract Factory) 패턴을 살편본다.
출처: TS/JS 디자인 패턴 with Canvas
TS/JS 디자인 패턴 with Canvas: 제로초에게 제대로 배우기 강의 | 제로초(조현영) - 인프런
제로초(조현영) | 타입스크립트/자바스크립트로 그림판을 만들어보며 다양한 디자인 패턴의 쓰임과 장단점을 알아봅니다. canvas api를 배울 수 있는 것은 보너스!, 디자인 패턴 배워서 저한테 도
www.inflearn.com
'강의노트 > 디자인패턴 with TS' 카테고리의 다른 글
[디자인 패턴 with TS] SOLID 원칙 (7) 2025.07.10 [디자인 패턴 with TS] 싱글턴 (Singleton) (0) 2025.07.07 [디자인 패턴 with TS] 강의 내용 (1) 2025.07.07