OHJINSU BLOG
  1. About
  2. Blog

© 2025 오진수. All rights reserved.
이 블로그는 Makernote로 만들어졌습니다.

profile image오진수 · 프로그래밍 · 

API 스타일을 우선해서 클래스 설계하기


이미지 출처: Scott Graham on Unsplash


관례적인 디자인 패턴도 중요하지만 API 스타일을 우선시해 봅시다. 다시 말하자면 코드를 사용하는 방식을 먼저 정의하는 겁니다. 객체 사이에 세부적인 의존 관계는 그 다음에 조정하는 거죠. 소프트웨어 아키텍처를 설계할 때 유용했던 방법입니다.


이번에 개발한 인테리어 프로그램으로 예를 들어 보겠습니다.


맨 처음에 시스템콘텐츠를 구분해야 했습니다. 구분은 다음과 같습니다.


-   시스템: 한 번 만들어 놓으면 계속 작동하는 것

-   콘텐츠: 비즈니스를 진행해 나가면서 계속 추가해야 하는 것


소프트웨어 설계는 수정을 용이하게 만드는 과정이라고 생각하는데요. 그래서 변경이 잦은 콘텐츠를 중심으로 고려해야 합니다. 인테리어 설계 프로그램에서 콘텐츠는 벽채, 문과 창문, 가구와 마감재 등의 오브젝트들에 해당합니다. 이러한 오브젝트를 필요할 때마다 쉽게 추가할 수 있어야 합니다.


콘텐츠 추가를 시뮬레이션하기


설계에 착수하자마자 아래와 같은 의사 코드를 작성해 보았습니다.


class FurnitureEntity extends Entity {
    override onUpdate() {
        const walls = this.scene.children.filter((e) => e instanceof WallEntity);

        // TODO
    }

    override onMouseDown(event: MouseEvent) {
        const point = this.scene.camera.fromScreen(event.clientX, event.clientY);

        // TODO
    }

    override onRender(context: CanvasRenderingContext2D) {
        context.beginPath();

        // TODO
    }
}


이건 구체적으로 콘텐츠를 추가하는 경우를 시뮬레이션한 코드입니다. 추상적인 설계로부터 시작하지 않았죠.


`Entity`부터 작성한 것이 아니라. `FurnitureEntity`부터 작성했습니다. 즉 Entity를 사용하는 코드를 먼저 작성했어요.


우선 첫째로, 새로운 오브젝트를 추가해야 할 때 `Entity`를 상속한 클래스를 만들도록 설계했습니다. 상속을 이용하면 필수적인 기능 구현을 강제할 수 있으므로 코드를 추가하기 쉽습니다. 예시에서는 매 프레임마다 호출되는 `onUpdate()`, 마우스 클릭 이벤트를 받는 `onMouseDown()`, 오브젝트를 캔버스에 그리는 `onRender()` 메서드를 반드시 구현하도록 강제했습니다.


둘째로, `FurnitureEntity`와 같이 구체적인 코드를 추가할 때 참조해야 하는 객체를 미리 생각해 보았습니다. 이를테면 `FurnitureEntity`는 벽에서 띄어진 거리를 측정해서 이용자에게 보여줄 필요도 있을 겁니다. 그러므로 존재하는 벽 객체를 모두 참조할 수 있어야 하겠죠. 이러한 참조를 주어진 코드처럼 이렇게 호출할 수 있다면 편리하다고 생각했습니다.


const walls = this.scene.children.filter((e) => e instanceof WallEntity);


그렇다면 `FurnitureEntity`는 멤버 변수로 `scene`을 가지고 있어야 한다는 뜻입니다. 그리고 다시 `scene`은 `children`을 가지고 있어야 하겠죠. 여기서 한발짝 더 나아가서 `children`의 타입은 `Entity[]`가 될 것이라고 추론할 수도 있습니다.


구체적인 것에서 추상적인 것으로


즉, Entity를 사용하는`FurnitureEntity`를 작성해 봄으로써 더욱 추상적인 수준에 있는 `Entity` 클래스를 설계할 수 있는 셈입니다. 아마도 이러한 모습이겠죠.


abstract class Entity {
    protected scene: Scene;

    abstract onUpdate();

    abstract onMouseDown(event: MouseEvent);

    abstract onRender(context: CanvasRenderingContext2D);
}


그리고 `Scene` 클래스는 최소한 아래와 같이 생겼을 겁니다.


class Scene {
    children: Entity[];
}


왜냐하면 Scene은 많은 Entity를 가지고 있어야 할테니까요.


여기모든 Entity의 `onUpdate()`, `onMouseDown()`, `onRender()` 메서드는 해당 이벤트가 발생할 때마다 한 번씩 호출되어야 합니다. 다시 말해 `children`을 순회해야 한다는 뜻입니다. 그러므로 순회 작업은 `children`을 가지고 있는 `Scene`이 대신해 주면 좋을 겁니다. 따라서 이렇게 쓸 수 있겠죠.


class Scene extends Entity {
    children: Entity[];

    override onUpdate() {
        for (const child of children) {
            child.onUpdate();
        }
    }

    override onMouseDown(event: MouseEvent) {
        for (const child of children) {
            child.onMouseDown(event);
        }
    }

    override onRender(context: CanvasRenderingContext2D) {
        for (const child of children) {
            child.onRender(context);
        }
    }
}
댓글 0

프로그래밍 카테고리 다른 글