프로젝트의 복잡성이 올라감에 따라, 구조적인 문제를 해결하기 위해 디자인 패턴 공부를 공부하고, 제 생각을 정리합니다.
목차
문제 상황
function main(type: Type) {
let obj: Object;
if (type === 'A') {
obj = new ObjectA();
} else if (type === 'B') {
obj = new ObjectB();
} else if (type === 'C') {
obj = new ObjectC();
}
obj.method1();
obj.method2();
return obj;
}
여기서 새로운 타입이 추가되면, main
함수를 수정해야 한다. 점점 main문의 가독성은 떨어지고, ObjectA
, ObjectB
, ObjectC
가 무슨일이 있을때마다, 신경을 써야하고 종국에는 유지보수가 힘든 코드베이스가 될것이다. 그렇기때문에 코드의 관심사에 따라 코드를 분리를 해야한다.
조건문 부분은 객체를 생성하는 코드로, 그리고 아래쪽 메소드를 사용하는 코드는 객체를 사용하는 코드로 분리할 수 있을 것 같다.
단순 팩토리
function objFactory(type: Type) {
let obj: Object;
if (type === 'A') {
obj = new ObjectA();
} else if (type === 'B') {
obj = new ObjectB();
} else if (type === 'C') {
obj = new ObjectC();
}
return obj;
}
function otherUseCase(type: Type) {
const obj: Object = objFactory(type); // <--
obj.method3();
...
}
function main(type: Type) {
const obj: Object = objFactory(type); // <--
obj.method1();
obj.method2();
return obj;
}
이제 생성관련 변경은 objFactory
에서만 변경하면되고, 해당 함수를 가져다가 여러곳에서 쓸 수 있을것 같다.
하지만, 소프트웨어는 항상 변하기 마련이다. 이번에는 만약 생성하는 객체의 스타일이 복잡하고 다양해 지면 어떻게 될까? 단순한 팩토리였지만 이제 팩토리도 복잡해질 것이다. 그때마다 기존 objFactory
에 계속해서 수정해야 할까?
function objFactory(type: Type) {
let obj: Object;
if (type === 'A') {
obj = new ObjectA();
} else if (type === 'B') {
obj = new ObjectB();
} else if (type === 'C') {
obj = new ObjectC();
} else if (type === 'D') {
...
} else if (type === 'FancyA') {
...
} else if (type === 'FancyB') {
...
}... // 300 줄 코드
return obj;
}
팩토리 메소드
interface Object {}
interface SimpleObject extends Object {}
interface FancyObject extends OBject {}
abstract class Factory {
abstract createObj(type: Type): Object; // <--
useObj(type: Type) {
const obj: Object = this.createObj(type); // <-- 생성은 서브클래스에 맡긴다.
obj.method1();
obj.method2();
}
otherUseCase(type: Type) {
const obj: Object = this.createObj(type); // <-- 생성은 서브클래스에 맡긴다.
obj.method3();
}
}
class SimpleFactory {
public createObj(type: Type): SimpleObject {
let obj: SimpleObject;
if (type === 'A') {
obj = new ObjectA();
} else if (type === 'B') {
...
}
...
return obj;
}
}
class FancyFactory {
public createObj(type: Type): FancyObject {
let obj: FancyObject;
if (type === 'A') {
obj = new FancyObjectA();
} else if (type === 'B') {
...
}
...
return obj;
}
}
function otherUseCase(type: Type) {
const fac: Factory = new FancyFactory(); // or new SimpleFactory();
fac.otherUseCase(type);
...
}
function main(type: Type) {
const fac: Factory = new SimpleFactory(); // or new FancyFactory();
fac.useObj(type);
...
}
이제 새로운 Object
타입군이 추가된다 해도 Factory
를 상속하는 서브클래스를 추가만 하면 쉽게 확장할 수 있다. useCase의 로직 안에 createObj
메소드가 사용된다는 점에서 템플릿 메서드
와 비슷하다고 생각할 수도 있겠지만, 팩토리 메소드
는 보다 특수한 경우라고 볼 수 있다.
추상 팩토리
여기서 다 나아가서 Object
뿐 아니라 Abject
, Bbject
같은 새로운 객체의 생성이 필요한 경우라면 어떻게 될까? 이제 팩토리에 createObj
말고 새로운 create<something>
메소드들을 추가하는 식으로 관련 생성 로직을 추상화 할 수 있다. 이렇게 하면 하나의 팩토리에서 관련있는 여러 클래스 인스턴스들을 생성할 수 있게된다.
abstract class Factory {
abstract createObj(): Object;
abstract createBbj(): BObject;
abstract createAbj(): AObject;
abstract createCbj(): CObject;
...
}
function main(type: Type) {
const fac: Factory = new SimpleFactory();
const obj = fac.createObj();
const abj = fac.createAbj();
...
...
}
생각
프론트엔드 개발자 입장에서 생각했을때, 메소드 팩토리
는 각 서브클래스는 각각의 컴포넌트들이고 추상 클래스(Factory
)는 커스텀 훅의 역할을 하는 것 같다는 생각이 들게 한다. 상속이라는 것도 사실 context
나 커스텀훅을 사용하는 것과 비슷하다는 생각을 했다. 오히려 상속보다 구성방식에 가까워서 훨씬 유연해 보이기도 한다.
평소에 프론트엔드 개발자의 입장에서 클래스를 어떻게 활용 할 수 있을까 고민을 했었는데, 클래스가 아닌 방법으로 이미 객체지향의 원칙을 지키며 개발하고 있었을 것 같다는 생각을 하게됐다. 그리고 view(dom)가 tree 구조로 되어 있다는 점에서, 클래스의 상속구조와 비슷하다는 생각도 들게한다. 때문에 view(dom)와 연관된 로직보단 순수한 로직쪽에서 클래스의 활용도가 높을 것 같다는 생각이 들었다. 그래서 canvas를 사용할때 많이 쓰는건가.
디자인 패턴을 공부하면서, 패턴 자체를 공부한다기보다, 어떤 케이스에서 코드를 어떻게 나누는게 좋을지, 어떤 관심사로 나눌 수 있을지에 대한 공부에 집중하면 좋을 것 같다는 생각을 하게 됐다.