공부내용 공유하기

[디자인 패턴] 싱글톤, 데코레이터

싱글톤 패턴

세 줄 개념

  • 클래스의 인스턴스화를 단일 인스턴스로 제한하여 하나의 객체 사용을 강제하는 패턴
  • 두 개 이상의 인스턴스화를 방지하는 게 핵심 구현 방법
  • 메모리 사용에서의 이점 + 하나의 데이터 출처를 강제하는 용도로 사용

유용한 사례

Logger

많은 곳에서 사용되며 사이드 이펙트가 없고 인스턴스를 재활용했을 때의 효용이 높음

 

  1. 공유 리소스에 대한 동시 엑세스 제어
  2. 리소스에 대한 엑세스를 전역에서 요구
  3. 하나의 객체만 존재해야 하는 경우

위 세 개의 조건을 모두 만족하는 경우 고려해 볼 수 있음(무조건 사용해야 하는것이 아님)

 

문제점

  1. 싱글톤은 SRP를 위반한다
    싱글톤 객체는 자기 자신의 인스턴스를 감시하는 책임 + 스스로의 구성정보를 제공하는 책임. 두 개의 책임을 가지고 있는 객체
  2. 메모리 관리에 좋지 않다
    인스턴스를 하나로 제한하기 때문에 메모리 관리에 효율적일 수 있으나, 전역 객체로 유지되기 때문에 아무 일도 하지 않는 상황에서도 여전히 메모리를 점유하고 있음
  3. 싱글턴 내부의 데이터는 곧 전역상태
    전역 상태의 사이드 이펙트로 인해 유닛 테스트의 난이도가 올라가고, 소프트웨어의 복잡성을 늘림
  4. 싱글톤은 OCP를 위반한다
    전역 함수로 이루어져 서브 클래스 확장을 통한 추가 기능 구현이 어려움. 기능 수정에 따른 사이드 이펙트가 전역으로 전파

해결법

단일 인스턴스로 사용하고 싶은 객체는 그대로 일반 객체로 선언

해당 객체를 생성하는 팩토리를 선언해서 SRP를 지키도록 만들 수 있음

 

class House {
  private final Kitchen kitchen;
  private final Bedroom bedroom;
  private boolean isLocked;
 
   public House(Kitchen kitchen, Bedroom bedroom) {
    this.kitchen = kitchen;
    this.bedroom = bedroom;
  }
 
  private boolean isLocked() {
    return isLocked;
  }
 
  private boolean lock() {
    kitchen.lock();
    isLocked = true;
  }
}

class HouseFactory {
  House build() {
 
     Sink sink = new Sink;
     Dishwasher dishwasher = new Dishwasher;
     Refrigerator refrigerator = new Refrigerator;
     Kitchen kitchen = new Kitchen(sink, dishwasher, refrigerator);
 
		 Bed bed = new Bed;
     Dresser dresser = new Dresser;
     Bedroom bedroom = new Bedroom(bed, dresser);
 
		 House house = new House(kitchen, bedroom);
 
     return house;
  }
}
 
class Main {
  public static void(String...args) {
    House house = new HouseFactory().build();
    house.lock();
  }
}

Singleton I love you, but you're bringing me down (re-uploaded)

Root Cause of Singletons

 

데코레이터 패턴

세 줄 개념

  • 클래스의 다른 객체의 동작에 영향을 주지 않고 동적으로 개별 객체에 원하는 동작을 하게끔 추가하는 디자인 패턴
  • 상속을 통한 클래스 확장의 대안으로 컴파일 타임이 아닌 런타임에 새로운 동작 추가 가능
  • 객체에 동적으로 추가적인 책임을 부여하기 위해 사용

유용한 사례

런타임 시점에 기존 객체의 동작을 변경하거나 확장하는 경우

기존 코드를 훼손하지 않으면서 런타임에 추가 행동을 할당해야 하는 경우

상속을 사용해서 객체의 행동을 확장하는 것이 어색하거나 불편한 경우

단점

데코레이터 선언을 위한 보일러 플레이트 코드 증가

데코레이터 행위가 다른 데코레이터의 동작에 영향을 받지 않도록 구현하기가 어려움 (데코레이터 스택 내의 순서에 의존하지 않게 구현해야 함)

 

의문점

decorator pattern 과 hoc의 차이점?

데코레이터 패턴이 하는 일을 들여다 보면, 리액트에서 자주 사용하는 HOC(Higher-Order Component)와 개념이 거의 동일해 보임

찾아보니 동일한 의문을 가진 stackoverflow 질문이 있었고, 동일한 개념이라는 의견 존재

다만, 타입스크립트의 데코레이터 와 데코레이터 패턴 은 다른 개념을 나타내는 용어라는 점에 주의

함수형 프로그래밍에서의 함수 합성 개념과 정확하게 일치하는 것으로 보이며, 데코레이터 Wrapper들의 평가 및 실행 순서도 함수의 합성과 동일

 

함수 합성

interface Sum {
  (num: number): number;
}

const sum2: Sum = (num) => num + 2;
const sum3: Sum = (num) => num + 3;
const sum5: Sum = (num) => sum2(sum3(num));
const sum10: Sum = (num) => sum5(sum5(num));

HOC(High Order Component)

const withLabel = <P>(Component: ComponentType<P>) => (props: P) => {
    return <label><Component {...props} /></label>;
};

const Button = ({ children }: ComponentPropsWithRef<'button'>) => {
  return <button>{children}</button>;
};


const ButtonWithLabel = withLabel(Button);

 

styled-components 내부 구현도 데코레이터 패턴을 통해 구현된 게 아닐까? 하는 추측

(나중에 확인해보니 대략적인 줄기는 비슷했음)

추측성 내부 구현 의사코드

const styled = {
  div: (templateStrings: TemplateStringsArray) => {
    const [css] = templateStrings;
	const styleClassName = hash(css);
    const styleElement = document.createElement('style');
    
    styleElement.innerHTML = `
      .${styleClassName}{
        ${css}
      }
    `;
    
    document.head.appendChild(styleElement);

    return function <P extends ComponentPropsWithRef<'div'>>({ className, ...restProps }: P) {
      return (
        <div
          className={className ? `${className} ${styleClassName}` : styleClassName}
          {...restProps}
        />
      );
    };
  },
};

// 실제 사용코드
const Container = styled.div`
  display: flex;
  margin: 3px;
`;

Understand the "Decorator Pattern" with a real world example

데코레이터 패턴

타입스크립트로 작성된 데코레이터 / 디자인 패턴들

React js - What is the difference betwen HOC and decorator