[2D 메타볼 애니메이션 구현] 4. Metaballs의 업데이트 로직을 옵저버 패턴으로 설계하기

young_pallete·2023년 3월 24일
0
post-thumbnail
post-custom-banner

🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!

🗒️ 이 글의 수정 내역 (마지막 수정 일자: 23.03.24)

  • 23.03.24 - 코드 상에서 key 프로퍼티의 initialization이 누락되었음을 발견했고 수정하였습니다. (commit: c699507)

설계 배경

자. 이전 포스트까지 Metaballs를 생성 패턴을 이용해서 구현해봤어요.
하지만 이 객체를 그대로 쓰기에는 좋지 않다고 생각했어요. 왜냐구요?

  • Metaballs의 상태를 직접적으로 일일이 다루는 건, 각 객체간의 결합도가 너무 커요.
  • 결합도가 크면, 변화하기 쉬운 객체(Metaball)에 대한 의존성이 커지겠군요. 즉, Side Effect가 발생할 가능성이 높다는 이야기이며, 이는 추후 유지보수의 증가로 이어질 거 같아요.

따라서 저는 옵저버 패턴을 이용하여, 상태 업데이트 로직을 좀 더 객체지향적으로 설계해보려 합니다.

왜 옵저버 패턴을 썼을까

옵저버 패턴은 특정 상태가 변경되는 시점을 마치 '이벤트'라고 생각합니다.
그리고 이 이벤트가 발생하면, 주제를 구독하는 것들에게 업데이트하라고 알림을 주게 되는 거죠.

이렇게 관리하면, 결과적으로 상태와 상태에 따른 객체들의 변화를 좀 더 분리해서 사용할 수 있어요. 그리고 그리고 이러한 분리는 곧 객체간의 의존성을 느슨하게 결합할 수 있다는 의미기도 하지요!

따라서, 각 객체가 각자의 일을 하면서도, 상태 변화에 있어서는 일관성 있게 설계해보면 어떨까 싶어 옵저버 패턴을 채택했습니다.

Subject, Observer 추상 클래스 구현

자. 일단 인터페이스들을 추상적으로 구현한 클래스를 만들어보죠.

이 두 개는 다음과 같은 역할을 담당하는 객체를 추상화한 것입니다.

  • Subject: Observer가 구독하고 있는 일종의 'State Theme'입니다.
  • Observer: Subject의 알림에 따라 반응하는 객체입니다.
export abstract class Subject {
  public abstract observers: Set<Observer>;

  public abstract subscribe(observer: Observer): void;

  public abstract unSubscribe(observer: Observer): void;

  public abstract notify(): void;
}

export abstract class Observer {
  abstract key: string;

  abstract update(...args: unknown[]): void;
}

몇 줄만에 우리는 데이터 상태 변화에 따른 로직을 느슨하게 관리할 객체를 만들어버렸어요. 매우 직관적이고 간단하죠?
그리고 이 간단한 인터페이스가 곧, 느슨한 결합을 가능케 하므로, 개발에 있어서 많이 애용하는 패턴이기도 합니다.

물론 옵저버 패턴 역시 많아지면 순서에 따른 Side Effect 발생이라던지, 디버깅에 있어 복잡성이 증가하는 단점이 있지만, 우리가 만들 애니메이션은 그 정도의 복잡성이 발생하지는 않을 것 같았어요. 따라서 단점이 크게 제약을 주진 않을 것 같군요! 🥰

구체 클래스 구현

자. 이제 우리는 animation의 동작에 따라 메타볼들을 업데이트하기 위해, 이를 옵저버 패턴을 이용하여 구현해볼 거에요!

AnimationSubject

이 친구는 애니메이션이 동작할 때마다 구독한 옵저버들에게 업데이트를 하라고 알려줄 거에요.

import {Observer} from '~/src/design-pattern/observer/Observer';
import {Subject} from '~/src/design-pattern/observer/Subject';

interface IAnimationSubjectParams {
  ctx: CanvasRenderingContext2D;
}

export class AnimationSubject implements Subject {
  ctx: CanvasRenderingContext2D;

  observers: Set<Observer>;

  constructor({ctx}: IAnimationSubjectParams) {
    this.ctx = ctx;
    this.observers = new Set<Observer>();
  }

  public subscribe(observer: Observer): void {
    this.observers.add(observer);
  }

  public unSubscribe(observer: Observer): void {
    this.observers.delete(observer);
  }

  public notify(): void {
    this.observers.forEach(observer => {
      observer.update();
    });
  }
}

MetaballsAnimationObserver

23.03.24 - 해당 코드는 해당 commit에서 key가 누락되었음을 확인하여, 이를 수정하였습니다.

export abstract class MetaballsAnimationObserver {
  public abstract metaballs: StaticMetaballs | DynamicMetaballs;

  public abstract update(): void;
}

export class StaticMetaballsObserver implements MetaballsAnimationObserver {
  constructor(public metaballs: StaticMetaballs, public key: string) {}

  update() {
    this.metaballs.moveAll();
  }
}

export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
  constructor(public metaballs: DynamicMetaballs, public key: string) {}

  update() {
    this.metaballs.moveAll();
  }
}

자! 이제 우리는 상태 변화를 하기 위한 설계를 모두 완료했군요!

🎉 마치며

아무래도 크게 이러한 구현과 실제 적용 로직까지 하면 글이 길어질 거고, 집중도가 낮아지기 때문에 이만 글을 마치려 합니다.

다음에는 이제 본격적으로 캔버스에서 requestFrame을 동작시킬 거에요. 야호!
이제 우리가 만든 것들을 레고 조립하듯이, 서로 연결시켜보자구요! 🙆🏻🙆🏻‍♀️

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉
post-custom-banner

0개의 댓글