정의


클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴.


상황


객체지향 프로그래밍의 구현에는 흔히 toString 이라는 개념이 있다.

toString은 쉽게 말해서 객체를 String으로 표현하는 메소드이다.

물론 이 String이 객체를 대표한다고 할 수는 없으나, 일반적으로 이 객체를 설명하기 위한 내용이 담긴다.

언어 차원에서 기본적으로 제공해주기도 하지만, 실습을 해보자.

toString 대신 print 메소드를 정의해서 객체의 클래스명을 출력하도록 해보자.

그리고 서로 다른 클래스들의 print 메소드를 확인할 수 있도록 interface를 정의할것이다.

코드를 보자.


interface Item
{
    void print();
}

class Blank : Item
{
    public void print() { Console.WriteLine("Blank"); }
}

class Money : Item
{
    public void print() { Console.WriteLine("Money"); }
}

class Test
{
    public static void Main()
    {
        Item[] t = {
            new Money(),
            new Blank()
        };

        foreach(Item item in t)
        {
            item.print();
        }
    }
} 

Blank와 Money 클래스는 Item을 인터페이스로 갖는다.

이제 Item의 리스트를 통해 여러 객체의 print 메소드를 호출할 수 있게 되었다.

그런데 오래전에 개발한 Star 클래스에서는 print 메소드가 없고, 같은 기능을 하는 write 메소드만 존재한다고 하자.

개발 도중이나 작은 프로젝트 단위에선 write를 print로 바꿔버리면 그만이지만, 이미 write에 종속되어있는 다양한 클래스들에 의해 수정에 무리가 있을 수 있다.

이런 경우 Star 클래스의 메소드명을 변경하지 않고 Item 인터페이스를 만족하게 하는 방법은 무엇일까?

적응자 패턴은 이런 상황에서의 해결책을 제공해준다.

한가지 솔루션은 Star 클래스의 서브클래스를 만들어 Item 인터페이스를 만족하도록 변형하는것이다.

코드를 통해 살펴보자.


class Star
{
    public void write()
    {
        Console.WriteLine("Star");
    }
}

class StarAdapter : Star, Item
{
    public void print()
    {
        write();
    }
}

이제 StarAdapter 객체를 대신 추가하는것으로 Item 인터페이스를 만족하게 된다.

메인 코드를 통해 이를 확인해보자.


class Test
{
    public static void Main()
    {
        Item[] t = {
            new Money(),
            new Blank(),
            new StarAdapter()
        };

        foreach(Item item in t)
        {
            item.print();
        }
    }
} 

StarAdapter객체가 무사히 Item의 list안에 들어있는것을 확인할 수 있다. (엄밀히 따지면 StarAdapter를 출력해야 한다.)

하지만 항상 이런식의 방법이 가능하다는 보장은 없다.

예를들면 Item이 interface가 아니라 class인 경우는 다중 상속이 필요하기 때문에 불가능하다.

또한 Star클래스에 다른 역할을 하는 print 메소드가 있을 수 있다. 위 방법으로 해결이 불가능하진 않지만, 문제의 여지는 존재한다.

이런 경우에는 Adapter클래스가 객체의 인스턴스를 가지고 있는것으로 대체할 수 있다.

코드를 통해 확인해보자.


class StarAdapter : Item
{
    private Star instance = new Star();
    public void print()
    {
        instance.write();
    }
}

첫 방법에 비하면 깔끔하지 못하지만, 기존의 객체를 수정하지 않는 점에서 유용한 방법이다.

흔히 첫번째 방법을 클래스 적응자__라고 하고, 두번째 방법을 __객체 적응자 라고 한다.

이제 적응자 패턴의 장단점을 알아보자.


장단점


적응자 패턴은 두가지 구현에 따라 각각 장단점이 있다. 먼저 클래스 적응자의 장단점을 살펴보자.

클래스 적응자의 장점

  1. Adapter(StarAdapter) 클래스는 Adaptee(Star) 클래스를 상속하기 때문에 Adaptee에 정의된 행동을 재정의 할 수 있다.
  2. 한 개의 객체(Adapter)만 사용하기 때문에 추가적인 객체 생성의 필요가 없다.

클래스 적응자의 단점

  1. Adapter 클래스는 Adaptee 클래스를 상속하기 때문에 다형성을 이용할 수 없다. 즉, Adaptee 클래스를 서브 클래싱하는 다른 클래스들은 개조가 불가능하다.

이제 객체 적응자의 장단점을 살펴보자.

객체 적응자의 장점

  1. 클래스 적응자의 단점을 해결할 수 있다. 즉, 적응자 패턴에 다형성을 이용하여 Adaptee 클래스의 서브 클래스도 개조가 가능하다.

객체 적응자의 단점

  1. Adaptee 클래스의 행동을 재정의하기가 매우 어렵다. 상속이 아니기 때문에 당연하다.

클래스 다이어그램


UML

클래스 적응자의 클래스 다이어그램이다. 객체 적응자의 경우는 저기서 인스턴스 부분을 추가해주면 된다.


결론


적응자 패턴은 서로 일치하지 않는 인터페이스를 가진 클래스 사이의 호환성을 지켜주는 패턴이다.


yukariko

It's fun!