Design Pattern(2) - 구조 패턴

황승우·2023년 1월 7일
0

design-pattern

목록 보기
2/3

구조 패턴

  • 구조 패턴은 구조를 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조합하는 방법을 설명
  • 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 것처럼 사용할 수 있다.
  • 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다.

어댑터 패턴(Adapter **Pattern)**

  • 자세히보기
    • 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴

      ![스크린샷 2022-11-23 오후 9.59.11.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/80106a98-fa73-4865-9656-7fab8ffd0e12/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-23_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_9.59.11.png)
      class Target {
          public request(): string {
              return 'Target: The default target\'s behavior.';
          }
      }
      
      class Adaptee {
          public specificRequest(): string {
              return '.eetpadA eht fo roivaheb laicepS';
          }
      }
      
      class Adapter extends Target {
          private adaptee: Adaptee;
      
          constructor(adaptee: Adaptee) {
              super();
              this.adaptee = adaptee;
          }
      
          public request(): string {
              const result = this.adaptee.specificRequest().split('').reverse().join('');
              return `Adapter: (TRANSLATED) ${result}`;
          }
      }
      
      function clientCode(target: Target) {
          console.log(target.request());
      }
    • 적용 할 곳

      • 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때
      • 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우
    • 장점

      • 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있음.
      • 클라이언트 코드가 클라이언트 인터페이스를 통해 어댑터와 작동하는 한, 기존의 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있음.
    • 단점

      • 다수의 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡성이 증가. 때로는 코드의 나머지 부분과 작동하도록 서비스 클래스를 변경하는 것이 더 간단

브릿지 패턴(Bridge **Pattern)**

  • 자세히보기 스크린샷 2022-11-24 오전 12.25.33.png
    • 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(기능/구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴

      class Abstraction {
          protected implementation: Implementation;
      
          constructor(implementation: Implementation) {
              this.implementation = implementation;
          }
      
          public operation(): string {
              const result = this.implementation.operationImplementation();
              return `Abstraction: Base operation with:\n${result}`;
          }
      }
      
      class ExtendedAbstraction extends Abstraction {
          public operation(): string {
              const result = this.implementation.operationImplementation();
              return `ExtendedAbstraction: Extended operation with:\n${result}`;
          }
      }
      
      interface Implementation {
          operationImplementation(): string;
      }
      
      class ConcreteImplementationA implements Implementation {
          public operationImplementation(): string {
              return 'ConcreteImplementationA: Here\'s the result on the platform A.';
          }
      }
      
      class ConcreteImplementationB implements Implementation {
          public operationImplementation(): string {
              return 'ConcreteImplementationB: Here\'s the result on the platform B.';
          }
      }
      
      function clientCode(abstraction: Abstraction) {
          console.log(abstraction.operation());
      }
      
      let implementation = new ConcreteImplementationA();
      let abstraction = new Abstraction(implementation);
      clientCode(abstraction);
      
      console.log('');
      
      implementation = new ConcreteImplementationB();
      abstraction = new ExtendedAbstraction(implementation);
      clientCode(abstraction);
    • 적용 할 곳

      • 어떤 기능의 여러 변형을 가진 한개의 클래스를 나누고 정돈하려 할 때 사용
        • ex) 클래스가 다양한 데이터베이스 서버들과 작동할 수 있는 경우
      • 여러 직교(독립) 차원에서 클래스를 확장해야 할 때 사용.
    • 장점

      • 플랫폼 독립적인 클래스들과 앱들을 만들 수 있음.
      • 클라이언트 코드는 상위 수준의 인터페이스를 통해 작동하며, 플랫폼 세부 정보에 노출되지 않음.
      • 새로운 인터페이스(기능)들과 구현들을 상호 독립적으로 도입 가능
    • 단점

      • 결합도가 높은 클래스에 패턴을 적용하여 코드를 더 복잡하게 만들 수 있음.

복합체 패턴(Composite **Pattern)**

  • 자세히보기
    • 객체들을 트리 구조들로 구성한 후, 이러한 구조들과 개별 객체들처럼 작업할 수 있도록 하는 구조 패턴

      abstract class Component {
          protected parent!: Component | null;
      
          public setParent(parent: Component | null) {
              this.parent = parent;
          }
      
          public getParent(): Component | null {
              return this.parent;
          }
      
          public add(component: Component): void { }
      
          public remove(component: Component): void { }
      
          public isComposite(): boolean {
              return false;
          }
          public abstract operation(): string;
      }
      
      class Leaf extends Component {
          public operation(): string {
              return 'Leaf';
          }
      }
      class Composite extends Component {
          protected children: Component[] = [];
      
           public add(component: Component): void {
              this.children.push(component);
              component.setParent(this);
          }
      
          public remove(component: Component): void {
              const componentIndex = this.children.indexOf(component);
              this.children.splice(componentIndex, 1);
      
              component.setParent(null);
          }
      
          public isComposite(): boolean {
              return true;
          }
      
          public operation(): string {
              const results = [];
              for (const child of this.children) {
                  results.push(child.operation());
              }
      
              return `Branch(${results.join('+')})`;
          }
      }
      
      function clientCode(component: Component) {
          console.log(`RESULT: ${component.operation()}`);
      }
      
      function clientCode2(component1: Component, component2: Component) {
          // ...
      
          if (component1.isComposite()) {
              component1.add(component2);
          }
          console.log(`RESULT: ${component1.operation()}`);
      
          // ...
      }
      
      const tree = new Composite();
      const branch1 = new Composite();
      branch1.add(new Leaf());
      branch1.add(new Leaf());
      const branch2 = new Composite();
      branch2.add(new Leaf());
      tree.add(branch1);
      tree.add(branch2);
      clientCode(tree);
    • 적용 할 곳

      • 나무와 같은 객체 구조를 구현해야 할 때 사용
      • 클라이언트 코드가 단순 요소들과 복합 요소들을 모두 균일하게 처리하도록 하고 싶을 때 사용
    • 장점

      • 다형성과 재귀를 당신에 유리하게 사용해 복잡한 트리 구조들과 더 편리하게 작업할 수 있음.
      • 객체 트리와 작동하는 기존 코드를 훼손하지 않고 앱에 새로운 요소 유형들을 도입할 수 있음.
    • 단점

      • 기능이 너무 다른 클래스들에는 공통 인터페이스를 제공하기 어려울 수 있으며, 어떤 경우에는 컴포넌트 인터페이스를 과도하게 일반화해야 하여 이해하기 어렵게 만들 수 있음.

데코레이터 패턴(Decorator **Pattern)**

  • 자세히보기
    • 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴

      ![스크린샷 2022-11-23 오후 11.16.07.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7f35f1e9-2a71-4634-a7f1-bfdd81a9de55/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-23_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.16.07.png)
      interface Component {
          operation(): string;
      }
      
      class ConcreteComponent implements Component {
          public operation(): string {
              return 'ConcreteComponent';
          }
      }
      
      class Decorator implements Component {
          protected component: Component;
      
          constructor(component: Component) {
              this.component = component;
          }
      
          public operation(): string {
              return this.component.operation();
          }
      }
      
      class ConcreteDecoratorA extends Decorator {
          public operation(): string {
              return `ConcreteDecoratorA(${super.operation()})`;
          }
      }
      
      class ConcreteDecoratorB extends Decorator {
          public operation(): string {
              return `ConcreteDecoratorB(${super.operation()})`;
          }
      }
      
      function clientCode(component: Component) {
          // ...
      
          console.log(`RESULT: ${component.operation()}`);
      
          // ...
      }
    • 적용 할 곳

      • 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 때 사용
      • 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 때 사용
    • 장점

      • 새 자식 클래스를 만들지 않고도 객체의 행동을 확장할 수 있음.
      • 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있음.
      • 객체를 여러 데코레이터로 래핑하여 여러 행동들을 합성할 수 있음.
      • 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있음.
    • 단점

      • 래퍼들의 스택에서 특정 래퍼를 제거하기가 어려움.
      • 데코레이터의 행동이 데코레이터 스택 내의 순서에 의존하지 않는 방식으로 데코레이터를 구현하기가 어려움.

퍼사드 패턴(Facade **Pattern)**

  • 자세히보기
    • 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적 디자인 패턴

      ![스크린샷 2022-11-23 오후 10.41.52.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c16293f2-58de-443b-8478-e6e7af123666/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-23_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_10.41.52.png)
      class Facade {
          protected subsystem1: Subsystem1;
      
          protected subsystem2: Subsystem2;
      
          constructor(subsystem1?: Subsystem1, subsystem2?: Subsystem2) {
              this.subsystem1 = subsystem1 || new Subsystem1();
              this.subsystem2 = subsystem2 || new Subsystem2();
          }
          public operation(): string {
              let result = 'Facade initializes subsystems:\n';
              result += this.subsystem1.operation1();
              result += this.subsystem2.operation1();
              result += 'Facade orders subsystems to perform the action:\n';
              result += this.subsystem1.operationN();
              result += this.subsystem2.operationZ();
      
              return result;
          }
      }
      
      class Subsystem1 {
          public operation1(): string {
              return 'Subsystem1: Ready!\n';
          }
      
          // ...
      
          public operationN(): string {
              return 'Subsystem1: Go!\n';
          }
      }
      
      class Subsystem2 {
          public operation1(): string {
              return 'Subsystem2: Get ready!\n';
          }
      
          // ...
      
          public operationZ(): string {
              return 'Subsystem2: Fire!';
          }
      }
      
      function clientCode(facade: Facade) {
          console.log(facade.operation());
      }
    • 적용 할 곳

      • 복잡한 하위 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 때 사용
      • 하위 시스템을 계층들로 구성하려는 경우 사용
        • 하위 시스템이 변화할 시 해당 하위 시스템을 포함하는 부분만 수정하면 됨.
    • 장점

      • 복잡한 하위 시스템에서 코드를 별도로 분리 가능
    • 단점

      • 앱의 모든 클래스에 결합된 전지전능한 객체(God Objects)가 될 수 있음.
        • God Object란 하나의 객체가 너무 많은 관련없고 다양한 타입과 메소드들을 포함하고 있는 객체를 의미. 이는 단일 책임 원칙에 벗어나며, 이는 추후에 하나의 책임(수행 범위)가 수정될 시 연결되어 있는 다른 책임까지 수정을 발생시키는 비효율을 일으킨다.

플라이웨이트 패턴(Flywieght **Pattern)**

  • 자세히보기
    • 각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 구조 디자인 패턴(고유한 객체만 플라이웨이트)

      ![스크린샷 2022-11-25 오후 12.59.33.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ea033e54-f2c9-4b80-8084-74cdf4a71e8a/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-25_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_12.59.33.png)
      class Flyweight {
          private sharedState: any;
      
          constructor(sharedState: any) {
              this.sharedState = sharedState;
          }
      
          public operation(uniqueState): void {
              const s = JSON.stringify(this.sharedState);
              const u = JSON.stringify(uniqueState);
              console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`);
          }
      }
      
      class FlyweightFactory {
          private flyweights: {[key: string]: Flyweight} = <any>{};
      
          constructor(initialFlyweights: string[][]) {
              for (const state of initialFlyweights) {
                  this.flyweights[this.getKey(state)] = new Flyweight(state);
              }
          }
      
          private getKey(state: string[]): string {
              return state.join('_');
          }
      
          public getFlyweight(sharedState: string[]): Flyweight {
              const key = this.getKey(sharedState);
      
              if (!(key in this.flyweights)) {
                  console.log('FlyweightFactory: Can\'t find a flyweight, creating new one.');
                  this.flyweights[key] = new Flyweight(sharedState);
              } else {
                  console.log('FlyweightFactory: Reusing existing flyweight.');
              }
      
              return this.flyweights[key];
          }
      
          public listFlyweights(): void {
              const count = Object.keys(this.flyweights).length;
              console.log(`\nFlyweightFactory: I have ${count} flyweights:`);
              for (const key in this.flyweights) {
                  console.log(key);
              }
          }
      }
      
      const factory = new FlyweightFactory([
          ['Chevrolet', 'Camaro2018', 'pink'],
          ['Mercedes Benz', 'C300', 'black'],
          ['Mercedes Benz', 'C500', 'red'],
          ['BMW', 'M5', 'red'],
          ['BMW', 'X6', 'white'],
          // ...
      ]);
      factory.listFlyweights();
      
      function addCarToPoliceDatabase(
          ff: FlyweightFactory, plates: string, owner: string,
          brand: string, model: string, color: string,
      ) {
          console.log('\nClient: Adding a car to database.');
          const flyweight = ff.getFlyweight([brand, model, color]);
      
          flyweight.operation([plates, owner]);
      }
      
      addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red');
      
      addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red');
      
      factory.listFlyweights();
    • 적용 할 곳

      • 프로그램이 많은 수의 객체들을 지원해야 해서 사용할 수 있는 RAM을 거의 다 사용했을 때만 사용
        • 앱이 수많은 유사 객체들을 생성해야 할 때
        • 이것이 대상 장치에서 사용할 수 있는 모든 RAM을 소모할 때
        • 이 객체들에 여러 중복 상태들이 포함되어 있으며, 이 상태들이 추출된 후 객체 간에 공유될 수 있을 때
    • 장점

      • 프로그램에 유사한 객체들이 많다고 가정하면 많은 RAM을 절약할 수 있음.
    • 단점

      • 플라이웨이트 메서드를 호출할 때마다 콘텍스트 데이터의 일부를 다시 계산해야 한다면 CPU 주기를 사용해서 대신 RAM을 절약하고 있는 것일지도 모름.
      • 코드가 복잡해지므로 새로운 사람이 코드 확인시 왜 개체(entity)의 상태가 그런 식으로 분리되었는지 식별하기 어려움.

프록시 패턴(Proxy **Pattern)**

  • 자세히보기
    • 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴

    • 원래 객체에 대한 접근을 제어하므로, 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 함.

    • 어떤 객체를 사용하고자 할때, 객체를 직접적으로 참조하는 것이 아닌 해당 객체를 대항하는 객체를 통해 대상 객체에 접근하는 방식을 사용하면 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있고, 실제 객체의 기능이 필요한 시점까지 객체의 생성을 미룰 수 있다.

      ![스크린샷 2022-11-24 오후 12.53.32.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/500ee6f9-ba02-432a-bdc8-e3e651e81d03/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-24_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_12.53.32.png)
      interface Subject {
          request(): void;
      }
      
      class RealSubject implements Subject {
          public request(): void {
              console.log('RealSubject: Handling request.');
          }
      }
      
      class Proxy implements Subject {
          private realSubject: RealSubject;
      
          constructor(realSubject: RealSubject) {
              this.realSubject = realSubject;
          }
      
          public request(): void {
              if (this.checkAccess()) {
                  this.realSubject.request();
                  this.logAccess();
              }
          }
      
          private checkAccess(): boolean {
              console.log('Proxy: Checking access prior to firing a real request.');
      
              return true;
          }
      
          private logAccess(): void {
              console.log('Proxy: Logging the time of request.');
          }
      }
      
      function clientCode(subject: Subject) {
          subject.request();
      }
      
      const realSubject = new RealSubject();
      clientCode(realSubject);
      
      const proxy = new Proxy(realSubject);
      clientCode(proxy);
    • 적용 할 곳

      • 가상프록시
        • 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것 처럼 동작하도록 만들고 싶을 때 사용하는 패턴
        • 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현
      • 원격프록시
        • 원격 객체에 대한 접근을 제어 로컬 환경에 존재하며, 원격 객체에 대한 대변자 역할을 하는 객체 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것 처럼 동작하게 하는 패턴.(예: Google Docs)
      • 보호프록시
        • 주체 클래스에 대한 접근을 제어하기 위한 경우에 객체에 대한 접근 권한을 제어하거나 객체마다 접근 권한을 달리하고 싶을 경우 사용하는 패턴
        • 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정 가능
    • 장점

      • 클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어 가능
      • 클라이언트들이 신경 쓰지 않을 때 서비스 객체의 수명 주기를 관리 가능.
      • 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동
      • 서비스나 클라이언트들을 변경하지 않고도 새 프록시들을 도입할 수 있음.
    • 단점

      • 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있음.
      • 새로운 클래스들을 많이 도입해야 하므로 코드가 복잡해질 수 있음.
      • 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능이 저하될 수 있음.

참고

https://refactoring.guru/ko/design-patterns

https://velog.io/@ha0kim/Design-Pattern-구조-패턴Structural-Patterns

http://wiki.hash.kr/index.php/구조패턴

https://jhtop0419.tistory.com/109

https://koreapy.tistory.com/1127

https://en.wikipedia.org/wiki/God_object

https://velog.io/@newtownboy/디자인패턴-프록시패턴Proxy-Pattern

https://velog.io/@gmtmoney2357/디자인-패턴-프록시-패턴Proxy-Pattern-데코레이터-패턴Decorator-Pattern

https://javabom.tistory.com/146

profile
백엔드 개발자

0개의 댓글