본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 어댑터 패턴 (Adapter Pattern)

어댑터 패턴은 아무 관련없는 인터페이스간의 호환성을 도와준다.

Context

사실 adapter는 일상생활에서도 많이 볼 수 있는 형식과 같다. 흔히 말하는 돼지코와 같은 원리이다.

먼저 아래와 같은 상황이 주어진다고 해보자, 최초 어플리케이션 설계시 ClientCar을 가지고 있으며 움직일 때마다 car::depart메서드를 이용한다.

이를 코드로 나타내면 아래와 같다.


interface Car {
    void move();
}


class Sonata implements Car {
    @Override
    public void move() {
        System.out.println("Sonata is going!");
    }
}
class Client {
    private Car car;

    public Client(Car car) {
        this.car = car;
    }

    void move() {
        this.car.move();
    }

    public static void main(String[] args) {
        Client client = new Client(new Sonata());

        client.move();
    }
}

Problem

어느 날부터 Client는 갑자기 기차를 이용하여 이동이 가능하다는 기능이 필요해졌다. 이미 스트레티지 패턴을 이용하여 나름 확장 가능하도록 설계했지만 구조적으로 풀 수 없는 문제이다.

사실 우리는 이미 근본적인 답을 알고 있다.

  1. 기차와 자동차 인터페이스의 상위 인터페이스를 만든다.
  2. 해당 상위 인터페이스를 클라이언트가 의존하도록 한다.

하지만 위와 같이 애플리케이션 구조의 전반을 흔드는 리팩토링을 한다면 모든 자동차와 기차의 구현 클래스와 의존 코드를 수정해야한다는 엄청난(?) 일을 수행해야한다.

위와같은 이유 때문에 개발자는 아래와 같은 막코딩(?)을 하게되고 어마어마한 쓰레기 남기게 된다. 심지어 기능 확장에 대해서 기존 Client의 코드를 수정했으므로 OCP 위반이다.

moveViaTrain이라니 끔직하다.

interface Train {
    void go();
}

class KTX implements  Train {
    @Override
    public void go() {
        System.out.println("KTX is going!");
    }
}

class Client {
    private Car car;
    private Train train;

    public Client(Car car, Train train) {
        this.car = car;
        this.train = train;
    }

    void moveViaCar() {
        car.move();
    }

    void moveViaTrain() {
        train.go();
    }

    public static void main(String[] args) {
        Client client = new Client(new Sonata(), new KTX());

        client.moveViaCar();
        client.moveViaTrain();

    }
}

Solution

흔이 외국에 여행할 때 사용하는 돼지코와 같은 원리다! 돼지코는 기존의 포트의 모양을 적절한 위치의 적절한 모양으로 위임한다. Adapter Pattern도 이와 같은 내용이다. 호환시켜야하는 인터페이스를 적절하게 위임한다.

UML의 CarAdapter가 이 내용을 담당한다. Train 타입 필드를 가져 Train::goCar::depart메서드로 위임시켜버리는 코드를 가지고있다. (메서드명이 좀 이상하다. 명확히 구분하기 위해 이렇게 작성했다.)

class CarAdapter implements Car {
    private Train train;

    public CarAdapter(Train train) {
        this.train = train;
    }

    @Override
    public void move() {
        train.go();
    }
}

위와 같은 형식으로 어댑터를 만들면 최초 Context의 Client코드를 수정하지 않고 아래와 같이 쓸 수 있게된다.

class Client {
    private Car car;

    public Client(Car car) {
        this.car = car;
    }

    void move() {
        car.move();
    }

    public static void main(String[] args) {
        Client client = new Client(new CarAdapter(new KTX())); // Adaptee 되었다.

        client.move();
    }
}

위의 경우 Train, Car는 공통된 이동수단이라는 부분을 추상화할 수 있다. 하지만 어댑터 패턴은 이런 경우 뿐만 아니라 정말 아무 관련없는 인터페이스 간의 호환성을 보장하기 위해서도 쓰일 수 있다는 것을 참고하자.

Summary

어댑터 패턴은 아무런 관계 없는 인터페이스간의 호환성을 보장하기 위해 사용된다.

  1. 어댑터 패턴은 OCP를 만족한다. 별다른 의존코드의 변경없이 잘 동작한다.
  2. 패치수준의 패턴이다. 어플리케이션의 리팩토링의 범위와 리스크가 매우 크다고 판단할 때 쓰며 의외로 잘 동작한다.