어댑터 패턴(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. 그러면 우리쪽 인터페이스를 수정하면 되지 않나?
가능할 수 있다. 하지만 바꾸려는 우리쪽 인터페이스를 우리 시스템의 다른 어딘가에서 사용하고 있다면? 그 부분도 수정해줘야 한다. 우리쪽 인터페이스를 수정하고, 이에 영향을 받는 부분들을 수정하다가 예기치 못한 오류가 발생할 가능성이 매우 크다.
댓글