[디자인 패턴] - 01. 어댑터(Adapter) 패턴

gyomni·2023년 5월 5일
2

design pattern

목록 보기
1/3
post-thumbnail

소프트웨어 개발은

  • 사용자의 요구사항 분석
  • 설계
  • 구현
  • 테스트
  • 배포

위의 과정을 계속 반복하게 된다.
그리고 이 다섯가지의 항목 중에서 설계를 올바르고 효과적으로 하기 위해서는 디자인 패턴을 적용하는 것이 매우 좋다고 한다.

그럼 디자인 패턴이란 무엇일까?

디자인 패턴(Design Pattern)이란?

소프트웨어 설계 방법으로 개별 클래스의 기능(역할, 책임)을 명확히 정의하고
여러 개의 클래스들 간의 관계를 효과적으로 잘 맺는 방법을 정리해 놓은 것이다.

여기서 클래스들 간의 관례를 잘 맺어야 한다고 하는데, 그 이유에는 여러가지가 있다.

  1. 클래스는 최소한의 단위 기능을 가져야 함
    (*단위 기능이란 하나의 기능을 의미)

  2. 큰 기능은 최소 단위 기능을 갖는 클래스들 간의 관계를 통해 개발됨

  3. 꼭 필요한 것들만으로 구성된 최적화된 소프트웨어 개발이 가능함

  4. 문제 발생 시 원인 규명이 빠름

  5. 최소한의 코드 수정으로 유지보수가 가능해 짐

  6. 기존 기능에 영향을 주지 않고 새로운 기능 추가가 가능해 짐.

=> 1,2 번이 충족 된다면, 나머지 내용을 이루는 데 큰 도움을 준다.

디자인 패턴에는 여러가지가 존재한다.
그 중 가장 대표적인 GoF의 디자인 패턴을 학습해보려 한다.

GoF의 디자인 패턴

전세계 개발자들이 추천하는 가장 유용하고 대표적인 디자인 패턴이다.
1990년대 쯤에 Gangs of Four 라 불리는 4명의 뛰어난 개발자분들이 체계적으로 체계적으로 정리해 놓은 설계 패턴이다.
지금까지도 매우 유용하게 활용되며, 이 패턴들을 기반으로 다양한 응용 패턴들이 도출되기도 한다.
프레임워크나 라이브러리 설계 에서도 이런 디자인패턴이 적용되고 있다.

GoF의 디자인 패턴은 총 23개의 패턴으로 구성되어 있고 3가지로 분류된다.

  • 생성 패턴: Factory Method, Abstract Factory, Builder, Prototype, Singleton

  • 구조 패턴: Adapter, Bridge, Composite, Decorator, Facade FlyWeight, Proxy

  • 행위 패턴: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, Strategy, Visitor

이런 분류는 크게 중요하지는 않고, 소프트웨어 설계에
이 패턴들을 원래 내용 그대로 적용해야 하는 것도 아니다.
각 패턴이 무엇을 위하여 존재하는지, 왜 존재하는지를 이해하는 것이 가장 중요하다!

각 디자인 패턴이 클래스 간의 최적의 관계를 매우 효과적으로 설계할 수 있는 하나의 사례로 받아드리고 그 목적을 이해해야 한다.

디자인 패턴을 배경지식으로써 인지하고, 여러가지 관점에서 변형하고 서로 혼합하여 소프트웨어 설계에 활용하면 된다.

디자인 패턴이 무엇인지는 알았으니 이제 GoF의 디자인 패턴을 하나하나 학습해보자!

이번 게시글은 어댑터 패턴이다!

Adapter

원하는 형태로 변환해 주는 장치이다.


위의 이미지로 볼 수 있듯이
호환성의 문제로 공간에 추가적으로 input, output을 변환시켜 줘야할 때가 있다.

예시로,
한국에서는 220v를 사용하고 있지만 미국에서는 110v를 사용한다.
한국 전자기기를 미국에서 사용하려면?
=> 110v로 변환해주는 무언가가 필요할 것이다.
=> adapter

이렇게 adapter는 기존에 있는 interface(미국의 wall outlet)에서도 동작할 수 있도록 해주는 개념이다.

wall outlet = 110v = client
adapter = 110v <-> 220v
plug = 220v = class (외부 요소)

디자인패턴 관점에서 본다면,
클래스 A를 사용해야 하는데 클래스 A에 대한 변경할 수 없다. 이때 adapter 패턴을 적용하면 원하는 형태로 사용할 수 있게된다.

특징

  • 카테고리: 구조적 패턴
  • 동작 원리: 서로 호환성이 없는 interface or class 들을 기존에 구현되어 있는것에 맞게 감싸서 변형시켜 동작할 수 있게 도와줌.

적용 사례

  • 외부 라이브러리 (Auth, Payment, Media...)
    : 로그인 서비스나, 결제 시스템, 비디오 플레이어 같은 라이브러리를 사용중 일때, 라이브러리가 더 이상 현재 시스템 요구사항에 맞지 않아서 코드를 바꾸던지 업데이트를 해야할 때가 있다.
    이런 외부 라이브러리 들은 종종 class나 interface 에 직접 접근할 수 없는 상황이 생긴다.
    그럴 경우 중간에 adapter class를 생성해서 중간 작업을 변환시켜줄 수 있다.

  • 레거시 시스템 부분 확장
    : 흔치는 않지만, 기존의 시스템의 코드를 부분적으로 확장해야하는 상황에서 interface나 class 같은 부분이 앱 전역에서 사용되는 비중이 굉장히 커서 바꾸는 비용이 커질 경우가 있다.
    전체적으로 시스템을 갈아엎기 보다는 adapter class를 부분적으로 만들어주고 중간 중간에 호환이 안되는 부분만 연결시켜서 사용할 수도 있다.

구성 요소

구성 요소에는 client, interface, service / 외부 요소(외부 라이브러리), adapter 가 있다.

client

보통 비즈니스 로직을 담고 있고,
이를 바탕으로 동작을 실행시키는 역할을 한다.

interface

interface를 바탕으로 class가 동작한다.

service / 외부 요소(외부 라이브러리)

새로 도입하려는 class로 변환 대상이다.
기존 시스템(interface)에 연결 시켜줄 객체이다.

adapter

서비스를 직접 호출 한다.
client와 직접적인 상호작용을 담당 하고, adapter 자신은 서비스 객체를 감싸고 있어서 하나의 field를 만들어 참조를 해준다.

client를 통해서 호출이 되면 adapter가 기존 서비스 객체의 interface를 변환시켜준다.

예시

코드로 어댑터 패턴을 구현해보자.
해당 예시는 패턴 이해에 중점을 두었기에 최소한으로 작성했다.

코드로 들어가기 전 상황을 이해하기 위해 간단한 형식으로 class diagram을 그려보았다.

class diagram


User classFruit class를 사용하고,
Fruit class 은 추상 클래스로서 구체적인 구현은 Apple class, Banana class 가 맞게 된다.

여기서 Mango classFruit class 처럼 사용해야 한다고 하자.
그런데 Mango class는 어떠한 이유로 코드를 변경할 수 없어서 코드를 그대로 사용해야 한다. (=> 이 예제에서 어댑터 패턴을 사용해야 하는 이유)

Mango classFruit class처럼 사용하려면?
MangoAdapter class를 도입하여 Fruit class 클래스를 상속 받도록 하고,
Mango class의 객체를 MangoAdapter class가 필드로 받게 한다.

이렇게 하면 Mango class를 변경하지 않고 MangoAdapter class를 통해서 Fruit class 처럼 사용할 수 있게 된다.

코드

위의 class diagram을 토대로 코드를 작성해보자.

fruit.ts
추상 클래스 Fruit 생성

export default abstract class fruit {
  constructor(protected name: string, protected cost: number) {}

  abstract price(): void;
}

apple.ts
Fruit class를 상속 받는 Apple class 생성


import fruit from "./fruit";

export default class Apple extends fruit {
  price(): void {
    console.log(`${this.name} 사과는 ${Math.ceil(this.cost * 1.2)}원 입니다.`);
  }

constructor(name: string, cost: number) {
    super(name, cost);
  }
}

banana.ts
Fruit class를 상속 받는 Banana class 생성

import fruit from "./fruit";

export default class Banana extends fruit {

  price(): void {
    console.log(
      `${this.name} 바나나는 ${Math.ceil(this.cost * 1.1)}원 입니다.`
    );
  }

constructor(name: string, cost: number) {
    super(name, cost);
  }
}

mango.ts
Mango class는 변경할 수 없지만 Fruit class처럼 사용해야 함

export default class Mango {
  private _name: string = "";
  private _cost: number = 0;

  set name(name: string) {
    this._name = name;
  }
  set cost(cost: number) {
    this._cost = cost;
  }

  get name(): string {
    return this._name;
  }

  get cost(): number {
    return this._cost;
  }

  tax(): number {
    return 1200;
  }
}

mangoAdapter.ts
Mango class를 Apple, Banana 처럼, Fruit class 타입같이 사용하기 위해 MangoAdapter class 추가


import fruit from "./fruit";
import Mango from "./mango";

export default class MangoAdapter extends fruit {
  private mango: Mango;

  constructor(name: string, cost: number) {
    super(name, cost);

    this.mango = new Mango();
    this.mango.name = name;
    this.mango.cost = cost;
  }

  price(): void {
    console.log(
      `${this.mango.name} 망고는 ${
        Math.ceil(this.mango.cost * 1.4) + this.mango.tax()
      }원 입니다.`
    );
  }
}

index.ts

import Apple from "./apple";
import Banana from "./banana";
import Fruit from "./fruit";
import MangoAdapter from "./mangoAdapter";

  const list = Array<Fruit>();

list.push(new Apple("A-1", 1500));
list.push(new Banana("B-1", 1300));
list.push(new Banana("B-3", 1500));

list.forEach((fruit) => {
  fruit.price();
});

Apple, Banana class를 사용하여 출력된 결과를 보면 아래와 같다.

다음으로 Mango class를 사용해보자.

// 어댑터 사용 전
...

const mango = new Mango();
mango.name = "M-2";
mango.cost = 2000;

list.push(mango); // error

Argument of type 'Mango' is not assignable to parameter of type 'Fruit'. Property 'price' is missing in type 'Mango' but required in type 'Fruit'.ts(2345) fruit.ts(7, 12): 'price' is declared here.와 같은 에러 메시지가 뜬다.

Mango 타입에 인지하는 Fruit 타입의 파라미터에 할당할 수 없다다는 에러이다.
-> MangoAdapter class를 통해서 Mango를 Fruit 처럼 사용할 수 있도록 호환할 수 있다.

// 어댑터 사용 후
...
list.push(new MangoAdapter("m-1", 2000));

어댑터를 사용하여 Mango class를 Fruit class타입처럼 사용 가능하게 되었고, 결과가 정상적으로 출력되는 것을 확인할 수 있다,

정리

어댑터 패턴은 코드를 변경할 수 없는 클래스를 원하는 형태로 사용하고자 할 때 적용할 수 있는 패턴이다.

클래스의 코드를 변경하기 어려운 경우에는 다음과 같은 상황이 있다.

  • 이미 많은 프로그램에서 사용되는 공용 클래스로써 공용 클래스가 변경되면 영향을 받는
    다른 프로그램의 코드가 너무 많이 변경되는 경우
  • 어떤 클래스가 버전 업 된 경우 하위버전의 클래스도 지원해야 하는 경우

참고 자료

profile
Front-end developer 👩‍💻✍

0개의 댓글