Design Patterns (1) - Strategy, Observer

김수지·2021년 10월 18일
0

TILs

목록 보기
35/39

Today What I Learned

매일 배운 것을 이해한만큼 정리해봅니다.
오늘은 Head First Design Patterns 책을 읽으며 Strategy pattern과 Observer pattern을 공부해보았습니다.


프로그래밍 책은 궁금은 한데 쭉 진도 나가기가 어려워서(혹은 지겨워서) 요즘엔 3권 정도를 정해두고 틈이 될 때마다 1-2 챕터 정도를 돌려가면서 읽는 중이다. 그 중에 한 권이 'Head First Design Patterns'.
최근에 바닐라 자바스크립트로 상태관리 시스템을 만드는 블로그 글을 연달아 읽었는데 읽으면서 뭔가 내가 주로 쓰는 라이브러리들에서 자주본 것만 같은 구조적 모양새가 보였다. '그치, 상태관리 라이브러리 같은걸 막 만들진 않겠지. 뭔가 사람들이 잘 정립해둔 구조가 있을거야.' 싶은 마음에 자료를 찾다가 발견한 책이 이 책이다. 예전에 면접을 보면서 '디자인 패턴에 대해서 아는대로 얘기해보라.'는 답에 쩔쩔맸던 기억도 나고 그래서, 나중에 핵심만 보고 싶을 때를 위해서 글로 남겨보기로 했다.
(책의 예제 코드는 자바로 되어 있긴 한데, 글에서는 타입스크립트로 풀어 써보려고 한다.)

1. Design Pattern

"누군가가 이미 여러분의 문제를 해결해 놓았습니다."

  • 소프트웨어 개발에 있어서 변화는 뗄레야 뗄 수 없는 조건이다. 그렇기 때문에 이러한 변화를 잘 대응하면서도 재사용성을 높이며 원하는 바를 구현할 수 있는 어플리케이션을 마련하고자 이미 많은 개발자들이 여러 시도를 해왔을 것이다. 이러한 과정에서 어떻게 코드의 구조를 다잡는지에 대한 저마다 패턴이 생겨났다.
  • 어플리케이션 상의 코드를 이해하기 쉽고, 관리하기 쉽고, 유연한 구조를 짜게끔 도와주는 것이 바로 디자인 패턴이다.
  • 디자인 패턴은 라이브러리보다도 높은 단계에 속하고 클래스와 객체를 구성하여 어떤 문제를 해결하는 방법을 제공한다.
  • 많은 사람들이 사용하는 라이브러리나 프레임워크의 구현 과정에서도 디자인 패턴이 사용되는 경우가 많다.
  • 어떻게 하면 변화에 대응하는 더 나은 어플리케이션 구조를 가질 수 있을지에 대한 여러가지 답인 셈이고, 여러 디자인 패턴에서 쓰이는 몇가지 공통적인 객체지향 디자인 원칙들도 존재한다.

몇가지 객체지향 디자인 원칙들

  • 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
    • 객체지향에서는 캡슐화를 통해서 하나의 객체 안에 메소드를 정의하고 상속을 통해 오버라이드하는 방법을 쉽게 선택할 수 있다.
    • 처음 정의해두었던 메소드에 변화를 주려할 때 자칫하면 여러 인스턴스 상에서 의도하지 않은 영향을 받을 수 있다.
    • 변화가 필요한 부분은 따로 뽑아서 캡슐화시켜 관리하고 수정하거나 확장시킬 수 있다.
  • 인터페이스에 맞춰서 프로그래밍한다.
    • 상위 형식에 맞춰서 프로그래밍 한다는 것을 뜻한다.
    • 코드 실행 시 객체가 코드에 의해서 고정되지 않도록 특정 상위 형식(supertype)에 맞춰서 프로그래밍을 하면 다형성을 활용할 수 있다.
    • 변수 선언 시에는 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 실제 대입 시 어떤 객체든 유연하게 주입이 가능하기 때문이다.
  • 상속보다는 구성을 활용한다.
    • "A는 B이다"의 구조보다 "A에는 B가 있다"를 관계를 지향하는 것이 좋다.
    • 두 클래스를 합칠 때 구성(composition)을 이용하면 유연성이 크게 향상된다.
    • 합성을 선택하게 되면 어떠한 특성을 가진 동작을 별도 클래스의 집합으로 캡슐화하여 만들 수 있고, 구성 요소로 사용하는 객체 내 행동 인터페이스를 구현해두면 실행 시에도 행동을 바꿀 수 있다.

2. 2가지 Design Pattern

1. Strategy Pattern

  • Strategy Pattern 정의: 1)알고리즘군을 정의하고 2)각각을 캡슐화하여 3)교환해서 사용할 수 있도록 만드는 패턴.
  • 이 패턴을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
  • 예시: 인물을 정의하고, 해당 인물의 공격 방법을 유연하게 바꾸도록 구성해보자.
// ts file

// 유연하게 대응해야 하는 동작은 interface로 정의한다.
interface WeaponBehavior {
  useWeapon: () => void;
}

// 구현: interface를 받아서 다양한 상세 동작을 정의해둔다.
class SwordBehaviors implements WeaponBehavior {
  useWeapon() {
    console.log("검을 휘두른다");
  }
}
// 구현: interface를 받아서 다양한 상세 동작을 정의해둔다.
class KnifeBehaviors implements WeaponBehavior {
  useWeapon() {
    console.log("칼로 벤다");
  }
}

// Character에는 WeaponBehavior가 있다: 추상 클래스를 정의한다.
abstract class Character {
  weapon: WeaponBehavior;

  constructor(weapon: WeaponBehavior) {
    this.weapon = weapon;
  }

  abstract fight(): void;
  setWeapon(weapon: WeaponBehavior) {
    this.weapon = weapon;
  }
}

// 상속: 캐릭터 추상 클래스를 상속하는 기사 클래스를 정의한다.
class Knight extends Character {
  fight() {
    this.weapon.useWeapon();
  }
}

const knight = new Knight(new SwordBehaviors());
knight.fight();
// 콘솔로 출력: "검을 휘두른다"

// 클래스 바깥에서 변화하는 동작을 따로 관리한다: 무기를 변경한다.
knight.setWeapon(new KnifeBehaviors());

knight.fight();
// 콘솔로 출력: "칼로 벤다"

2. Observer Pattern

  • Observer Pattern 정의: 한 객체의 상태가 바뀌면, 그 객체에 의존하는 다른 객체들에게 연락이 가고, 자동으로 내용이 갱신되는 방식. 일대다(one-to-many) 의존성을 가짐
  • 어떤 객체의 상태를 느슨하게 결합된 다른 객체들에게 전달하려고 할 때 이 패턴을 사용함
  • 용어 정리
    • Subject: 데이터를 관리하는 객체, 데이터의 변경이 발생했을 때 내용을 Observer로 전달
    • Observer: Subject를 구독(subscribe)하는 객체, 데이터 변경이 발생했을 때 내용을 전달 받는 객체
    • Observable:
    • 일대다 관계: Subject(단독, one)를 구독하고 있는 여러 Observer(여럿, many)들의 구도
    • 느슨한 결합(loose coupling): 객체 간 상호작용이 있지만 서로에 대해 잘 모르는 것, 변경사항이 발생하여도 유연하게 처리 가능
  • Observer pattern은 GUI 및 다른 부분에서도 광범위하게 쓰인다.
  • Observer pattern의 느슨한 결합
    • Subject는 자신을 구독하고 있는 Observer에 대해 자세히 알지 못하나 상태 변경 시 알림을 보내고(subject), 받을 수 있는(observer) 관계가 형성되어 있다.
    • Subject와 Observer는 서로 독립적으로 재사용 가능한 객체이다.
    • Subject나 Observer가 바뀌더라도 서로에게 영향을 미치지는 않는다.
  • 예시: 날씨 데이터 객체와 이를 구독하고 있는 기상 객체를 만들어보자.
// ts file

interface Subject {
  registerObserver: (obj: Observer) => void;
  removeObserver: (obj: Observer) => void;
  notifyObservers: () => void;
}

interface Observer {
  /** Subject의 상태 변경 시 전달 받음 */
  update: (observables: Record<string, any>) => void;
}

class WeatherData implements Subject {
  observers: Observer[];
  observables: Record<string, any>;
  private changed: boolean = false;

  constructor() {
    this.observers = [];
    this.observables = { temperature: 0, humidity: 0, pressure: 0 };
  }

  registerObserver(obj: Observer) {
    console.log("옵저버를 등록", obj);
    this.observers = [...this.observers, obj];
    console.log("총 등록된 옵저버들", this.observers);
  }

  removeObserver(obj: Observer) {
    console.log("옵저버를 제거", obj);
    this.observers = this.observers.filter((observer) => observer !== obj);
    console.log("남아 있는 옵저버들", this.observers);
  }

  notifyObservers() {
    if (this.changed) {
      this.observers.forEach((observer) => {
        console.log("알림을 줄 옵저버", observer);
        observer.update(this.observables);
      });
    }
    this.changed = false;
  }

  private measurementsChanged() {
    this.changed = true;
    this.notifyObservers();
  }

  setMeasurements(values: {
    temperature?: number;
    humidity?: number;
    pressure?: number;
  }) {
    this.observables = {
      temperature: values.temperature || this.observables.temperature,
      humidity: values.humidity || this.observables.humidity,
      pressure: values.pressure || this.observables.pressure,
    };
    this.measurementsChanged();
  }

  subscribeSubject(observer: Observer) {
    this.registerObserver(observer);
  }

  unsubscribeSubject(observer: Observer) {
    this.removeObserver(observer);
  }
}

class CurrentConditionObserver implements Observer {
  update(observables: Record<string, any>) {
    console.log("subject의 상태가 바뀌었다", observables);
  }
}

const weatherData = new WeatherData();
const currentCondition = new CurrentConditionObserver();

weatherData.subscribeSubject(currentCondition);
// 콘솔로 출력: 옵저버를 등록 CurrentConditionObserver {}
// 콘솔로 출력: 총 등록된 옵저버들 [ CurrentConditionObserver {} ]
// 콘솔로 출력: 알림을 줄 옵저버 CurrentConditionObserver {}
weatherData.setMeasurements({ temperature: 20, humidity: 60, pressure: 10 });
// 콘솔로 출력: subject의 상태가 바뀌었다 { temperature: 20, humidity: 60, pressure: 10 }
weatherData.unsubscribeSubject(currentCondition);
// 콘솔로 출력: 옵저버를 제거 CurrentConditionObserver {}
// 콘솔로 출력: 남아 있는 옵저버들 []
profile
선한 변화와 사회적 가치를 만들고 싶은 체인지 메이커+개발자입니다.

0개의 댓글