본문 바로가기
Computer Science/Design Pattern

[Design Pattern] 어댑터 패턴(Adapter Pattern)

by hyeinisfree 2022. 6. 6.

어댑터 패턴(Adapter Pattern)이란?

어댑터 패턴(Adapter pattern)은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다.

 

어댑터 패턴 구조

  • Client는 Target 인터페이스를 구현한 Adaptee가 필요하다.
  • Adaptee는 Target 인터페이스를 구현하지 않고 있다.
    • Adaptee는 이미 개발이 완료되어 사용중이다.
    • Adaptee를 변경하는 것이 적절하지 않은 상황이다.

 

1. Client는 Target 인터페이스를 통해 Adaptee를 사용하고자 한다.

interface Target {
  Response request();
}

2. Adaptee는 이미 개발이 완료되어있고, 수정하기 곤란한 상황이다.

class Adaptee {
  Response specificRequest() {
    return new Response();
  }
}

3. Adapter 클래스의 request()는 Adaptee의 specificRequest()를 감싸고 있으며, Target 인터페이스를 구현한다.

class Adapter implements Target {
  private final Adaptee adaptee;

  public Adapter(Adaptee adaptee) {
    this.adaptee = adaptee;
  }

  @Override
  public Response request() {
    return this.adaptee.specificRequest();
  }
}

4. 이제 Adaptee를 다음과 같이 Adapter에 집어넣어서 Target 인터페이스로 사용할 수 있다. 

Target target = new Adapter(adaptee)

 

어댑터 패턴 호출 과정

Client에서는 Target 인터페이스를 호출하는 것 처럼 보인다. 하지만 Client의 요청을 전달받은 (Target 인터페이스를 구현한) Adapter는 자신이 감싸고 있는 Adaptee에게 실질적인 처리를 위임한다. Adapter가 Adaptee를 감싸고 있는 것 때문에 Wrapper 패턴이라고도 불린다.

 

어댑터 패턴 예제

  • 오리를 표현한 인터페이스와 클래스이다.
interface Duck {
  public void quack();  // 오리는 꽉꽉 소리를 낸다
  public void fly();
}

class MallardDuck implements Duck {
  @Override
  public void quack() {
    System.out.println("Quack");
  }
  @Override
  public void fly() {
    System.out.println("I'm flying");
  }
}
  • 칠면조를 표현한 인터페이스와 클래스이다. 두 새의 인터페이스가 다르다.
interface Turkey {
  public void gobble();   // 칠면조는 골골거리는 소리를 낸다
  public void fly();
}

class WildTurkey implements Turkey {
  @Override
  public void gobble() {
    System.out.println("Gobble gobble");
  }
  @Override
  public void fly() {
    System.out.println("I'm flying a short distance");
  }
}
  • 클라이언트는 오리를 사용해서 일을 하고 있다.
class Client {
    private Duck duck;
    
    public Client(Duck duck) {
    	this.duck = duck;
    }
    
    public void doWork() {
    	duck.gobble();
        duck.fly();
    }
}
  • 오리가 부족해서 클라이언트는 칠면조를 사용해서 오리가 일했던 것처럼 사용하고 싶다.
publi class AdapterDemo {
    public static void main(String[] args) {
        Duck duck = new MallardDuck();
       	Client client1 = new Client(duck);
        
        // 오리가 부족해서 칠면조를 대신 사용하고 싶다.
        Duck turkey = new WildTurkey(); // 불가능 -> 어댑터가 필요하다!
        Client client2 = new Client(turkey);
    }
}
  • 어댑터를 구현하여 사용하면 된다.
class TurkeyAdapter implements Duck {
  Turkey turkey;

  public TurkeyAdapter(Turkey turkey) {
    this.turkey = turkey;
  }
  
  @Override
  public void quack() {
    turkey.gobble();
  }
  
  @Override
  public void fly() {
    // 칠면조는 멀리 날지 못하므로 다섯 번 날아서 오리처럼 긴 거리를 날게 한다
    for (int i = 0; i < 5; i++) {
      turkey.fly();
    }
  }
}
  • 어댑터를 사용하면 아래와 같이 칠면조를 오리처럼 사용할 수 있다.
publi class AdapterDemo {
    public static void main(String[] args) {
        Duck duck = new MallardDuck();
        Client client1 = new Client(duck);
        
        // 오리가 부족해서 칠면조를 대신 사용하고 싶다.
        Duck turkey = new TurkeyAdapter(new WildTurkey());
        Client client2 = new Client(turkey);
    }
}

 

어댑터 패턴을 쓰는 이유

의문점 1. 애초에 두개의 인터페이스가 달라서 호환이 안된다면, 하나를 바꿔서 되게 하던지, 아니면 둘다 바꾸면 되지 않나?

위 예제에서 Turkey가 오픈소스가 아니라 미리 컴파일된 클래스 바이너리 파일만을 제공받은 써드파티 라이브러리라면 직접적인 접근이 불가능 할 수 있다. 직접적으로 접근할 수 있는 경우라 하더라도 Adaptee 쪽에서 우리가 변경한 코드로 인해 라이브러리나 벤더쪽 시스템 전체가 깨질 수도 있다.

 

의문점 2. 그러면 우리쪽 인터페이스를 수정하면 되지 않나?

가능할 수 있다. 하지만 바꾸려는 우리쪽 인터페이스를 우리 시스템의 다른 어딘가에서 사용하고 있다면? 그 부분도 수정해줘야 한다. 우리쪽 인터페이스를 수정하고, 이에 영향을 받는 부분들을 수정하다가 예기치 못한 오류가 발생할 가능성이 매우 크다.

 

📗 참고

댓글