[ Programming > Design Pattern ]
[디자인 패턴] 컴파운드 패턴
컴파운드 패턴
오늘은 여러 패턴의 조합으로 문제를 해결하는 컴파운드 패턴에 대해서 알아봐요. 컴파운드 패턴은 딱 정해진 형식이 없어요. 단순히 여러 패턴을 조합하여 문제를 해결하는 것이에요. 대표적인 예로 MVC, MVP, VIPER과 같은 많은 아키텍처들이 있어요.
그런데 Head Frist 디자인 패턴 책처럼 한 단계씩 설명하기는 글 내용이 너무 길어질 것 같아요. 그래서 통째로 설명할 수 있게 예시 코드를 통해 설명하고자 해요.
컴파운드 패턴 예시
우선 컴파운터 패턴 예시 코드를 만들기 위해 배경이 필요해요.
피그마 기능을 컴파운드 패턴으로 구현해보자
저가 만들고 싶은 기능은 피그마처럼 화면에 요소를 추가하여 꾸밀수 있는 기능이에요. 생각한 기능들은 다음과 같아요.
기능 정의
화면에 삼각형, 사각형 같은 요소 추가가 가능하다.
추가된 요소는 이동 가능하다.
이동 했던 동작들은 다시 뒤로 되돌릴 수 있다.
여러 사람이 작업하기 때문에 요소에 대한 소유권이 필요하다.
그러면 각 기능을 위해 무엇이 필요할까요? 정리해봐요
필요한 코드들
Viewer에 삼각형, 사각형 등을 렌더링 해야한다.
Viewer와 상호 작용 가능한 요소가 필요하다.
상호 작용을 통해 데이터의 수정(위치 이동)할 수 있는 기능이 필요하다.
데이터가 수정되면 수정된 데이터를 Viewer에 전달하여 렌더링할 수 있어야 한다.
Viewer에서 요소를 이동시키려할 때 소유권을 가져 다른 사람이 함께 이동시키지 못하게 해야한다.
이동 했던 이력을 저장하여 되돌릴 수 있어야한다.
그러면 위 코드들을 보고 어떠한 패턴들을 사용해야할까요? 사람마다 다를 거 같아요. 그래서 저만의 방법으로 해결해보면서 컴파운드 패턴으로 해결해 볼게요. (사실 컴파운드 패턴이 아닐거에요..? 이 글의 끝 부분에 이유를 적었어요.) 근데 한 가지 주의하셨으면 좋겠어요. 저가 만들 코드는 패턴을 적용하기 위해서만 만든 코드에요. 실제로 디자인한다면 이렇게 모든 부분에 패턴을 마구잡이로 적용하진 않을거에요. 패턴을 적용했을때 어떤 영향을 줄지 고려해야하고, 오히려 더 복잡하게 만들어서 다른 사람들을 힘들게 할 수도 있으니까요.
사용할 패턴
Template Method Pattern => 삼각형, 사각형의 인터페이스 추상화
Factory Pattern => 삼각형, 사각형 같은 서로 다른 요소 생성
Command Pattern => 이동의 추상화를 통한 단순화 및 undo/redo 용으로 사용하기 위함
Observer => 데이터의 변화에 따른 Viewer 업데이트
Proxy => 요소의 소유권에 대한 제어
템플릿 메소드 패턴 + 옵저버 패턴 (도형 알고리즘 정형화, Viewer와 Shape 연동)
// 템플릿 메소드 패턴type Position = { x: number; y: number };interface Observable { attach(observer: Observer): void; detach(observer: Observer): void; notify(): void;}interface Observer { update(subject: Observable): void;}class Viewer implements Observer { update(subject: Observable) { if (subject instanceof Shape) { console.log("Viewer update:"); subject.draw(); } }}// 템플릿 메소드abstract class Shape implements Observable{ protected observers: Observer[] = []; protected position: Position; constructor(x: number, y: number) { this.position = { x, y }; } attach(observer: Observer) { this.observers.push(observer); } detach(observer: Observer) { this.observers = this.observers.filter(o => o !== observer); } notify() { this.observers.forEach(o => o.update(this)); } getPosition() { return this.position; } setPosition(pos: Position) { this.position = pos; this.notify(); } abstract draw(): void;}class Triangle extends Shape { draw() { console.log(`Draw Triangle at (${this.position.x}, ${this.position.y})`); }}class Rectangle extends Shape { draw() { console.log(`Draw Rectangle at (${this.position.x}, ${this.position.y})`); }}2. 추상 팩토리 패턴 (다양한 도형의 생성 캡슐화, Simple Factory)
interface ShapeFactory { createTriangle(x: number, y: number): IShape; createRectangle(x: number, y: number): IShape;}class SimpleShapeFactory implements ShapeFactory { createTriangle(x: number, y: number) { return new Triangle(x, y); } createRectangle(x: number, y: number) { return new Rectangle(x, y); }}class ProxyShapeFactory implements ShapeFactory { private ownerId: string; constructor(ownerId: string) { this.ownerId = ownerId; } createTriangle(x: number, y: number) { const shape = new Triangle(x, y); const proxy = new ShapeProxy(shape); proxy.claim(this.ownerId); return proxy; } createRectangle(x: number, y: number) { const shape = new Rectangle(x, y); const proxy = new ShapeProxy(shape); proxy.claim(this.ownerId); return proxy; }}커맨드 패턴 (복잡한 움직임 알고리즘 캡슐화, 이력 관리의 용이성)
interface Command { execute(): void; undo(): void;}class MoveCommand implements Command { private shape: Shape; private prevPosition: Position; private newPosition: Position; constructor(shape: Shape, newPosition: Position) { this.shape = shape; this.prevPosition = shape.getPosition(); this.newPosition = newPosition; } execute() { this.shape.setPosition(this.newPosition); } undo() { this.shape.setPosition(this.prevPosition); }}class CommandManager { private undoStack: Command[] = []; private redoStack: Command[] = []; executeCommand(cmd: Command) { cmd.execute(); this.undoStack.push(cmd); this.redoStack = []; // 새 커맨드 실행 시 redo 초기화 } undo() { const cmd = this.undoStack.pop(); if (!cmd) return; cmd.undo(); this.redoStack.push(cmd); } redo() { const cmd = this.redoStack.pop(); if (!cmd) return; cmd.execute(); this.undoStack.push(cmd); }}프록시 패턴 (움직이는 기능에 소유권 제어 기능 추가, 보호 프록시)
class ShapeProxy extends shape{ private shape: Shape; private ownerId: string | null = null; constructor(shape: Shape) { this.shape = shape; } claim(ownerId: string) { this.ownerId = ownerId; } release(ownerId: string) { if (this.ownerId === ownerId) { this.ownerId = null; } } move(ownerId: string, newPosition: Position) { if (this.ownerId !== ownerId) { console.log("Move denied: not owner"); return; } this.shape.setPosition(newPosition); } attach(observer: Observer) { this.shape.attach(observer); }}사용 예시
const viewer = new Viewer();// Factory로 Shape 생성const proxyFactory = new ProxyShapeFactory("user1");const rect2 = proxyFactory.createRectangle(0, 0);// viewer 에 Shape 등록rect2.attach(viewer);// CommandManager 생성 (Undo/Redo 관리)const commandManager = new CommandManager();const move2 = new MoveCommand(rect2, { x: 50, y: 60 });commandManager.executeCommand(move2);commandManager.undo();commandManager.redo();위 코드를 보면 다음과 같은 설명들을 할 수 있을거에요.
Template Method 패턴으로 Shape의 공통 알고리즘들을 정의하였고, 내부 알고리즘들을 캡슐화 했어요.
Observer 패턴을 통해 Viewer와 Shape간의 연결고리를 약하게 만들어 의존성을 낮췄어요. 이로써 서로 크게 신경쓰지 않아도 되었어요.
Abstract Factory 패턴으로 Shape의 생성 과정을 캡슐화 했어요. 이로써 직접 어떤 Shape들이 있는지 찾이 않고 Factory의 메서드를 통해 찾을 수 있어요.
Proxy 패턴을 통해 소유권에 대한 기능을 Shape Move 기능에서 분리했어요. 왜냐하면 소유권과 도형의 관계성이 떨어진다고 생각했어요. 그리고 Proxy로 빼서 네트워크 환경에서 소유권 정보 교환 알고리즘을 추가 및 관리하는데 더 좋을거라 생각했어요.
Command 패턴을 통해 Move의 내부 알고리즘들을 숨겼어요. 그리고 CommandManager까지 만들어 undo/redo를 모든 Command 객체에 대해서 가능하도록 했어요.
한번 위 예시 비슷한게 다이어그램을 만들어 봤어요
CreateShapeCommand, DeleteShapeCommand같은게 있어서 다르긴 하지만 있어보이게 만들어 봤어요. 위치 조정은 하고싶지만, 마음대로 안돼서 슬프게도 못했어요. 화살표 가 어지러지게 있지만 한번 화살표 따라가며 보세요. 더 자세하게 화살표를 그릴 수도 있는데 그러면 아에 읽지를 못하겠더라구요

위 예시에서 생각해 볼 점
추상 팩토리가 아니라, 단순 팩토리 패턴을 사용하면 createShape(Type)으로 바꿀 수 있는데 어떨까?
더 적은 메서드의 API 제공 가능하지만, 어떠한 도형들을 생성 가능한지는 파라미터를 보고 알아야함.
Shape의 생성/삭제도 redo/undo할 수 있게 Command 패턴을 적용하는 건 어떨까?
꼭 필요하게 될 기능이라고 생각새요. 그래서 생성/삭제도 undo/redo가 가능하게 만들 수 있도록 하면 좋을 것 같아요. 다만, Command가 Factory와 결합이 생겨서 더 많이 복잡해져요. 그래서 Shape을 외부에서 생성해서 Command에 넣어주고 그냥 생성했다고 알려주고 내부에서 Observer에 attach 하는 방식이 좋을 거 같아요.
Command Factory를 만들어서 해결할 수도 있을거에요. 물론 Command Factory가 Shape Factory와 의존성이 생기겠지만, Command의 생성을 분리하여 좀더 확장성 있는 코드가 나올거라 생각해요.
Shape이 더 추가되면 Shape이 이동할 때 처리하는 방법이 달라질 수 있지 않을까?
별모양, 동그라미같은 Shape을 이동하거나 심지어는 위아래로 늘리거나 하는 기능이 있다면, 충분히 그 처리하는 알고리즘이 달라질 수 있을거라고 생각해요. 그래서 저는 CircleMoveStrategy, StarMoveStrategt 같이 ShapeMoveStrategy라는 전략 패턴 인터페이스를 만들어 사용하면 괜찮을 거 같아요.
마지막으로 생각해볼 점 하나는 그러면 위에서 여러가지 패턴을 사용했는데 이를 컴파운드 패턴이라고 할 수 있을까요?
저의 대답은 "NO"에요. 왜냐하면 위의 패턴들이 사용되는 방식들은 저 특수한 경우에 한해서 생각해서 적용한 것이기 때문이에요. MVC, MVP 패턴과 같이 그래픽 요소를 데이터와 연동하여 보여주는 경우 생기는 복잡함을 해결하기 위해 여러가지 패턴을 썼을때, 그 패턴이 자주 나오고 반복되기 때문에 또 똑같이 적용할 수 있다면 그때 컴파운드 패턴이라고 할 수 있을거 같아요.
"어떤 비슷한 문제가 계속 일어난다 => 해당 문제를 똑똑하게 해결하도록 코드를 구현한다 => 비슷한 문제가 발생하는 경우에도 이전과 똑같은 패턴과 같이 구현이 된다. => 패턴이 있다는 것을 발견하며, 좋다고 인정받는다. => 이제 개발자들은 자주 발생되는 해당 패턴을 깔끔하게 정리하고 이름이 생긴다. => 패턴으로 인정받는다."와 같은 과정으로 패턴이 된다고 보면되요.
마무리
하나하나 모두 세세하게 단계별로 설명하기에는 상황을 가정하고 그 코드들을 모두 보여주는게 너무 힘들었어요. 그래서 한번에 저가 설계한대로 만든 것을 보여줬어요. 빠진 로직이 많을 수 있어요. 하지만 패턴을 공부하신 여러분들이라면 각 패턴으로 무슨 장점을 얻고 싶었는지 캐치하셨을거라 생각해요.
이번 장이 사실상 Head First 패턴의 마지막 장이에요. 물론 뒤에 부록으로 나오는 패턴들도 모두 소개할 것 같아요. 그리고 그 나오지 않는 패턴들 중에서도 유명한 패턴들을 다뤄야할 것 같고요. 취업도 잘 안되는데 그때까지 모두 화이팅이에요.

