FIF's 코딩팩토리

어댑터 패턴(Adapter Pattern) 정리 본문

Back-End/Design Pattern(디자인 패턴)

어댑터 패턴(Adapter Pattern) 정리

FIF 2019. 6. 10. 13:46
반응형

어댑터 패턴(Adapter Pattern) 정의

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.

어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

 

호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할 수 있다.

이렇게 함으로써 클라이언트와 구현된 인터페이스를 분리시킬 수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에 캡슐화 되기 때문에 클라이언트는 바뀔 필요가 없어진다.

 

어댑터 패턴 클래스 다이어 그램

전기 콘센트를 보면 이해가 쉽다.

한국의 표준 플러그를 일본 전원 소켓에 바로 끼우지 못해 동그란 모양을 일자로 바꿔주는 어댑터를 끼워야 한다.

이와 같이 어댑터는 소켓의 인터페이스를 플러그에서 필요로 하는 인터페이스로 바꿔준다고 할 수 있다.

객체지향에서 어댑터는 일상 생활과 동일하게 어떤 인터페이스를 클라이언트에서 요구하는 형태의 인터페이스에 적응시켜주는 역할을 한다.

Duck.interface

public interface Duck {
          public void quack();
          public void fly();
 }

 

MallardDuck.java

public class MallardDuck implements Duck {
          @Override
          public void quack() {
                   System.out.println("Quack");
          }
          @Override
          public void fly() {
                   System.out.println("I'm flying");
          }
 }

 

Turkey.interface

public interface Turkey {
          public void gobble();
          public void fly();
 }

WildTurkey.java

public 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");
          }
 }

 

Duck객체가 모자라서 Turkey객체를 대신 사용해야 하는 상황이라 가정하자.

인터페이스가 다르기 때문에 Turkey객체를 바로 사용할 수 없다.

 

어댑터 만들기

 

TurkeyAdapter.java

public class TurkeyAdapter implements Duck {
          Turkey turkey;
          public TurkeyAdapter(Turkey turkey) {
                   this.turkey = turkey;
          }
          @Override
          public void quack(){ 
                   turkey.gobble();
          }
          @Override
          public void fly() {
                   turkey.fly();
          }
 }

 

DuckTestDrive

public class DuckTestDrive {
          public static void main(String[] args) {
                  MallardDuck duck = new MallardDuck();
                  WildTurkey turkey = new WildTurkey();
                  Duck turkeyAdapter = new TurkeyAdapter(turkey);
                  System.out.println("The turkey says...");
                  turkey.gobble();
                  turkey.fly();
                  System.out.println("The Duck says...");
                  testDuck(duck);
                  System.out.println("The TurkeyAdapter says...");
                  testDuck(turkeyAdapter);
          }
          public static void testDuck(Duck duck){ 
                   duck.quack();
                   duck.fly();
          }
 }

 

클라이언트 -> request() -> 어댑터 -> translatedRequest() -> 어댑터

클라이언트는 타겟 인터페이스에 맞게 구현, 어댑터는 타겟 인터페이스를 구현하며, 어댑터 인터페이스가 들어있다.

 

어댑터에는 두종류가 있다.

1.     객체 어댑터

2.     클래스 어댑터

 

클래스 어댑터 패턴을 쓰려면 다중 상속이 필요한데, 자바는 기본적으로 다중 상속이 불가능.

두 어댑터의 클래스 다이어그램을 보면 이해가 수월하다.

 

클래스 어댑터

 

객체 어댑터

 

두 클래스 다이어그램에서 Target은 오리, Adapter는 칠면조라고 볼 수 있다.

 

클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑터 모두 서브 클래스로 만들고, 객체 어댑터에서는 구성을 통해 어댑터에 요청을 전달한다는 점을 제외하면 별다른 차이가 없다.

 

구식 Enumeration.

Enumeration을 리턴하는 element() 메소드가 구현되어 있었던 초기 컬렉션 형식(Vector, Stack, Hashtable 등) Enumeration 인터페이스를 이용하면 컬렉션 내에서 각 항목이 관리되는 방식에는 신경 쓸 필요 없이 컬렉션의 모든 항목에 접근 가능하다.

 

신형 Iterator

SUN에서 새로운 컬렉션 클래스를 출시하면서 Enumeration과 마찬가지로 컬렉션에 있는 일련의 항목들에 접근할 수 있게 해주면서 항목을 제거할 수 있게 해주는 Iterator라는 인터페이스를 이용하기 시작했다.

 

개발을 하다보면, Enumerator 인터페이스를 사용하는(구형 코드를 사용하는) 경우가 종종 있지만, 새로 만드는 코드에는 Iterator를 사용하는게 좋다.

이런 경우 어댑터 패턴을 적용하면 좋다.

 

public class EnumerationIterator implements Iterator{
          Enumeration enumeration;
          public EnumerationIterator(Enumeration enumeration) {
                   this.enumeration= enumeration;
          }
          @Override
          public boolean hasNext(){ 
                   return enumeration.hasMoreElements();
          }
          @Override
          public Object next() {
                   return enumeration.nextElement();
          }
          @Override
          public void remove() {
                   throw new UnsupportedOperationException(); //예외 던짐 UnsupportedOperationException 지원
          }
 }

                                                     

Enumeration에선 remove()에 해당하는 기능을 제공하지 않는다.(읽기전용 인터페이스다.)

어댑터 차원에서 완벽하게 작동하는 remove() 메소드를 구현할 방법은 없다.

그나마 가장 좋은 방법으로 예외를 던지는 방법인데, Iterator 인터페이스를 디자인한 사람들은 이런 필요성을 미리 예견하고 remove() 메소드를 구현할 때 UnsupportedOperation을 지원하도록 만들었다.

 

이런 경우는 어댑터가 완벽하게 적용될 수 없는 경우라고 할 수 있다.

하지만 클라이언트에서 충분히 주의를 기울이고 어댑터 문서를 잘 만들어 두면 상당히 쓸만한 해결책이 될 수 있다.

반응형
Comments