본문 바로가기

Backend/디자인 패턴

Adapter Pattern (어댑터 패턴)

어댑터 패턴은 다른 인터페이스를 사용해도 대상 객체를 사용할 수 있도록 객체를 약간 조정합니다.

이런 방법으로 다른 인터페이스를 사용해도 객체의 함수에 접근할 수 있도록 합니다.

 

class OurSpec {
  method1(): void {
    console.info('method1');
  }
}

interface ShareSpec {
  method2(): void;
}

class AdapterSpec implements ShareSpec {
  private readonly ourSpec: OurSpec;
  
  constructor(ourSpec: OurSpec) {
    this.ourSpec = ourSpec;
  }

  method2(): void {
    this.ourSpec.method1();
  }
}

const spec = new AdapterSpec(new OurSpec());
spec.method2(); // "method1"

 

어댑터 패턴의 대표적인 구현 방식은 역시나 합성인 것 같습니다. 상속을 사용할 수도 있긴 합니다만, 사실 별 차이는 없습니다.

위 코드의 상황은 이렇습니다. 우리는 구현할 때 OurSpec 이라는 스펙에 맞춰서 method1 을 개발했습니다.

하지만 다른 팀 혹은 외부 클라이언트에선 ShareSpec 을 구현한 method2 가 필요한 것이죠.

 

이럴때 객체 합성을 통해 인터페이스를 지원하는 Adapter 에 특정 인터페이스를 지원하지 않는 객체를 집어 넣어 사용하는 방법입니다.

위의 예시 코드는 이러한 방법을 나타낸 것이구요. 예시가 너무 간단해서 OurSpec 을 그냥 ShareSpec 을 구현하게끔 변경하는게 낫지 않느냐? 라고 할 수도 있지만, 이것또한 상황에 따라 다를 수 있을 것 같습니다.

 

위의 예시는 너무 간단하지만 예를 들어 이것이 하나의 라이브러리라고 할 때, 이것을 새로운 스펙에 맞게 전부 변경하는 일은 기존 코드의 수정을 너무도 많이 하게 됩니다. 이럴땐 차라리 위처럼 합성을 통해 적게 수정을 가져가는 것이 괜찮은 방법일 수 있습니다.

혹은 외부 라이브러리라서 인터페이스를 바꾸고 싶어도 바꿀 수 없는 경우에도 고려해볼 수 있을 것 같습니다.(생각보다 실무에선 이런 상황이 꽤 있는 것 같네요)

 

어댑터 패턴을 사용하면 기존 코드의 변경을 최소화할 수 있고 재사용성을 증가시킬 수 있습니다.

다만 아무래도 복잡성이 증가하고 잘 관리되지 않을 경우 상당히 커다란 기술 부채로 이어질 수 있을 것 같습니다.