본문 바로가기

SW 설계/Design Pattern

[Design Pattern] Structural - 아답터 패턴 (Adapter Pattern)

728x90

Adapter Pattern 은 Wrapper 라고도 불리는 패턴으로, Wrapping 과 같은 방법을 사용해서 서로 다른 일을 하게 된 두 interface 역할체들을 묶어주는 일을 해줄 때 사용하는 패턴이다. 현실에서 아답터처럼, 제공해주는 interface 를 다른 interface 와 연결해준다는 의미를 가지고 있다고 보면 된다. 

 

 

Adapter Pattern 에서는 Client 가 기대하고 있고 호출하는 interface 를 Target Interface 라고 한다. 아답터는 이 호출을 다른 인터페이스 측의 함수를 호출하게 되는데, 이 인터페이스는 Adaptee Interface 라고 한다. Client 는 이 Adapter 의 존재를 모른채 호출을 하기 때문에 Interface 연동성에 큰 도움을 주는 패턴이다. 

 

 

전략 패턴에서 사용하였던 Duck 역할체와 (그 때는 abstract 긴 했지만) 다른 일을 수행하도록 하는 Turkey 역할체가 있다고 가정하고, 다음과 같이 하나씩 구현체가 있다고 해보겠다. 

 

 

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

 

 

public class MallardDuck implements Duck {
    @Override
    public void quack(){
        System.out.println("Quack");
    }
    
    @Override
    public void fly(){
        System.out.println("Doing Mallard Duck Fly!");
    }
}

 

 

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

 

 

public class WildTurkey implements Turkey {
    @Override
    public void gobble(){
        System.out.println("Gobble");
    }
    
    @Override
    public void fly(){
        System.out.println("Doing Turkey Fly!");
    }
}

 

 

위와 같은 상황이 있을 때, 경우에 따라서 Turkey 라는 역할체에서 하는 일을 수행할 때 해당 일이 아닌 Duck 역할체의 일을 수행하게 하도록 할 수 있다. 물론 인터페이스를 바꾸는게 best 지만, 기존 인터페이스를 바꾸지 않는게 나은 case 도 있기 때문이다. 이 때 다음과 같은 Adapter Class 를 만들어 볼 수 있다. 

 

 

public class TurkeyAdapter implements Duck {
    
    private Turkey turkey;
    
    public TurkeyAdatper(Turkey turkey){
        this.turkey = turkey;
    }
    
    @Override
    public void quack(){
        this.turkey.gobble();
    }
    
    @Override
    public void fly(){
        for(int i = 0; i < 5; i++){
            this.turkey.fly();
        }
    }
}

 

 

Adapter Pattern 을 적용할 때는 Target / Adaptee Interface 를 잘 생각하며 만들어주는게 좋다. Client 가 요구할 인터페이스는 Duck 인터페이스이다. Client 는 Duck 이 수행할 수 있는 기능을 호출하고 싶은 것이다. 그리므로 Adaptee 는 Turkey 인터페이스가 된다. 다음과 같이 수행을 시켜볼 수 있다. 

 

 

public static void main(String [] args){

    MallardDuck duck = new MallardDuck();
    WildTurkey turkey = new WildTurkey();
    
    // 사용하고 싶은 아답터를 가져온다
    // 당연히 구현체를 직접 넣어주거나, 구현체로 매핑되어 있는 역할체를 넣어줘야 한다
    Duck duckButTurkey = new TurkeyAdapter(turkey);
    
    System.out.println("Mallard Duck ---------");
    duck.quack();
    duck.fly();
    
    System.out.println("Wild Turkey ----------");
    turkey.gobble();
    turkey.fly();
    
    System.out.println("Duck Interf but implementing Turkey ----------")
    duckButTurkey.quack();
    duckButTurkey.fly();
}​

 

 

위와 같이 Target 역할체로 취급받지만 Adaptee 구현체의 일을 할 수 있는 패턴이다. 단, 위에서 gobble과 quack, 각각의 fly 함수들처럼 논리적으로 매핑할 수 있을만한 역할체들 사이를 연결해야 비즈니스 로직에 문제가 생기지 않을 것이다.

 

 

<병합된 사진>

 

 

Adapter Pattern 은 방금 살펴본 바와 같이 Interface 를 연결해주는데 흔하게 사용되지만, 두번째 UML 처럼 Inheritance 관계에서도 사용될 수 있다. 이 때는 Adaptee 와 Target 이 같은 Adapter Class 를 상속받아야 한다는 제약이 있다. 

 

 

또 다른 예시로 Java 에서는 Enumeration 과  Iterator 라는 대표적인 두 인터페이스가 있다. 이 때, 두 인터페이스를 서로 Adapter 로 연결해서 사용할 수 있는 예제를 만들어보자. 

 

 

Enumeration - for Vector, Stack, HashTable ...
- hasMoreElements()
- nextElement()

Iterator 
- hasNext()
- next()
- remove()

 

 

public class EnumerationIterator implements Iterator {

    // Target 이 수행해줄 Adaptee 
    private Enumeration enumeration;
    
    public EnumerationIterator(Enumeration enumeration){
        this.enumeration = enumeration;
    }
    
    @Override
    public boolean hasNext(){
        return this.enumeration.hasMoreElements();
    }
    
    @Override
    public Object next(){
        return this.enumeration.nextElement();
    }
    
    @Override
    public void remove(){
        throw new UnsupportedOperationException();
    }
}

 

 

위와 같이 Adapter 를 만들어주면 Enumeration 으로 가져온 자료구조들에 Iterator 를 사용해줄 수 있다. 다양한 Collection 에 대해서 Iterator 를 활용하는 Application 이 있을 때, Enumeration 을 갖는 자료구조 또한 Adapter Pattern 을 적용하면 쉽게 기능성을 확장시킬 수 있다.

 

 

 

정리하며

 

 

Adapter Pattern 은 어느정도 연관성이 있는 두 Interface 를 연결하여 Client 가 보다 편하게 Interface 들을 다룰 수 있도록 도와주는 패턴이다. 위 EnumerationIterator 처럼, 어플리케이션에서 유용하게 쓰일만한 상황인지에 대한 정확한 판별이 필요하기도하다. 무분별한 사용은 오히려 인 앱 로직을 복잡하게 만들이고 나중에 유지 / 보수를 오히려 어렵게 만들 수도 있기 때문이다 (뭐 따지고 보면 모든 디자인 패턴이 그렇다).  

 

 

위와 같은 상황이 많겠냐 싶을 수는 있겠지만, 개인적으로는 오히려 앱 내에서 [크고 자주 사용되는 인터페이스] 가  [특정하고 작은 인터페이스] 에 대하여 사용성을 주고 싶을 때 유용하게 사용될 수 있을 것 같다. 

 

 

아답터 패턴은 인 앱 상황에 따라 유용하게 사용될 수 있다

 

 

 

출처

 

 

1) 에릭 프리먼, Head First Design Patterns』, O'Reilly Media (2004)

 

 

2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중

 

728x90