Adapter Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
6/19
post-thumbnail

1. 왜 Adapter Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 인터페이스 불일치로 사용 불가능
public class Application {
    public void processData() {
        // 우리는 Duck 인터페이스를 기대하는데...
        Duck duck = new Turkey();  // ❌ 컴파일 에러!
        duck.quack();
        duck.fly();
    }
}

문제점:

  • 기존 시스템은 특정 인터페이스(Target)를 기대함
  • 새로운 벤더 라이브러리는 다른 인터페이스(Adaptee)를 제공
  • 소스 코드 수정이 불가능하거나 최소화하고 싶음
  • 직접 사용하면 호환되지 않음 (강한 결합)

실제 상황 예시:

  • 레거시 시스템에 새로운 결제 시스템 통합
  • 구 버전 라이브러리(Enumeration)에서 신 버전(Iterator)로 마이그레이션
  • 외부 API와 내부 시스템 인터페이스 불일치

2. Target Interface VS Adaptee Interface

1. Target Interface

  • "클라이언트가 기대하는 인터페이스"
  • 애플리케이션이 사용하고 싶어하는 표준 규격
  • "클라이언트의 언어"
public interface Duck {
    void quack();  // 클라이언트는 이 메서드를 호출하고 싶음
    void fly();
}

왜 Target인가?

  • 클라이언트 코드가 이미 이 인터페이스에 의존
  • 변경할 수 없거나 변경하고 싶지 않음
  • 애플리케이션의 기존 설계 유지

2. Adaptee Interface

  • "실제 사용하려는 기능을 가진 클래스의 인터페이스"
  • 벤더나 외부 라이브러리가 제공하는 실제 구현
  • "제공자의 언어"
public interface Turkey {
    void gobble();  // 다른 이름의 메서드
    void fly();     // 같은 이름이지만 다른 동작
}

왜 Adaptee인가?

  • 이미 구현되어 있고 기능은 우리가 원하는 것
  • 하지만 인터페이스가 Target과 다름
  • 직접 수정할 수 없는 경우가 많음

3. Object Adapter VS Class Adapter

Object Adapter (객체 컴포지션 방식)

특징:

  • Adaptee 객체를 포함(Has-A)
  • 런타임에 Adaptee 교체 가능
  • Java에서 권장되는 방식 (다중 상속 불가)

장점:
1. 더 유연함 - Adaptee의 서브클래스도 Adapt 가능
2. 단일 Adapter로 여러 Adaptee 처리 가능
3. Composition over Inheritance 원칙 준수

// ✅ Object Adapter
public class TurkeyAdapter implements Duck {
    Turkey turkey;  // Adaptee를 포함
    
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;  // 위임
    }
    
    public void quack() {
        turkey.gobble();  // 호출을 변환
    }
}

Class Adapter (상속 방식)

특징:

  • Target을 구현하고 Adaptee를 상속(Is-A)
  • 컴파일 타임에 결정됨
  • 다중 상속이 필요 (Java에서는 불가능, C++에서 가능)

장점:
1. Adaptee의 메서드를 직접 오버라이드 가능
2. 추가 객체 생성 불필요

단점:
1. Java에서는 인터페이스만 다중 구현 가능 (클래스는 단일 상속)
2. 덜 유연함


4. Adapter Pattern 핵심 구조

흐름

  1. Client가 Target 인터페이스로 요청
  2. Adapter가 요청을 받음
  3. Adapter가 Adaptee의 메서드로 변환
  4. Adaptee가 실제 작업 수행

핵심 개념:

  • 변환(Translation): Target 호출 → Adaptee 호출
  • 중개자(Wrapper): Client와 Adaptee 사이 중간 다리
  • 투명성: Client는 Adapter 존재를 모름

5. 예시 코드

Step 1: 기존 인터페이스들 정의

// Target Interface - 클라이언트가 기대하는 인터페이스
public interface Duck {
    void quack();
    void fly();
}

// Target 구현체
public class MallardDuck implements Duck {
    public void quack() {
        System.out.println("Quack");
    }
    
    public void fly() {
        System.out.println("I am flying");
    }
}

// Adaptee Interface - 실제 사용하려는 클래스의 인터페이스
public interface Turkey {
    void gobble();  // quack과 다른 메서드명
    void fly();     // 짧은 거리만 날 수 있음
}

// Adaptee 구현체
public class WildTurkey implements Turkey {
    public void gobble() {
        System.out.println("Gobble Gobble");
    }
    
    public void fly() {
        System.out.println("I'm flying a short distance");
    }
}

Step 2: Adapter 구현 (Object Adapter)

// Turkey를 Duck처럼 보이게 하는 Adapter
public class TurkeyAdapter implements Duck {
    Turkey turkey;  // Adaptee를 포함
    
    // 생성자에서 Adaptee 주입
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    
    // Target 메서드를 Adaptee 메서드로 변환
    public void quack() {
        turkey.gobble();  // quack() → gobble()
    }
    
    // 동작 차이도 보정 가능
    public void fly() {
        // Turkey는 짧게만 날 수 있으므로 5번 반복
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }
}

Step 3: 클라이언트 코드

public class DuckTestDrive {
    public static void main(String[] args) {
        // 일반 Duck
        Duck duck = new MallardDuck();
        
        // Turkey 객체
        Turkey turkey = new WildTurkey();
        
        // Turkey를 Duck으로 Adapt! (핵심!)
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        System.out.println("=== Turkey 원본 ===");
        turkey.gobble();
        turkey.fly();
        
        System.out.println("\n=== 일반 Duck ===");
        testDuck(duck);
        
        System.out.println("\n=== Adapter를 통한 Turkey ===");
        testDuck(turkeyAdapter);  // Turkey를 Duck처럼 사용!
    }
    
    // Duck 인터페이스만 받는 메서드
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

출력 결과

=== Turkey 원본 ===
Gobble Gobble
I'm flying a short distance

=== 일반 Duck ===
Quack
I am flying

=== Adapter를 통한 Turkey ===
Gobble Gobble
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance
I'm flying a short distance

6. 실전 예제: Enumeration → Iterator Adapter

문제 상황

  • 구형 컬렉션(Vector, Hashtable): Enumeration 인터페이스 사용
  • 신형 컬렉션(ArrayList, HashMap): Iterator 인터페이스 사용
  • 구형 코드를 신형 인터페이스로 변환 필요
// Adaptee: 구형 인터페이스
public interface Enumeration<E> {
    boolean hasMoreElements();
    E nextElement();
}

// Target: 신형 인터페이스
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();  // Enumeration에는 없는 메서드!
}

Adapter 구현

public class EnumerationIterator<E> implements Iterator<E> {
    Enumeration<E> enumeration;  // Adaptee
    
    public EnumerationIterator(Enumeration<E> enumeration) {
        this.enumeration = enumeration;
    }
    
    // 메서드 이름만 변환
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }
    
    public E next() {
        return enumeration.nextElement();
    }
    
    // Adaptee가 지원하지 않는 기능!
    public void remove() {
        throw new UnsupportedOperationException(
            "Enumeration doesn't support remove()"
        );
    }
}

반대 방향 Adapter (Iterator → Enumeration)

public class IteratorEnumeration<E> implements Enumeration<E> {
    Iterator<E> iterator;
    
    public IteratorEnumeration(Iterator<E> iterator) {
        this.iterator = iterator;
    }
    
    public boolean hasMoreElements() {
        return iterator.hasNext();
    }
    
    public E nextElement() {
        return iterator.next();
    }
    // remove()는 Enumeration에 없으므로 무시
}

사용 예시

public class AdapterDemo {
    public static void main(String[] args) {
        // 구형 컬렉션
        Vector<String> vector = new Vector<>();
        vector.add("Apple");
        vector.add("Banana");
        vector.add("Cherry");
        
        // Enumeration → Iterator로 Adapt
        Enumeration<String> enumeration = vector.elements();
        Iterator<String> iterator = new EnumerationIterator<>(enumeration);
        
        // 이제 Iterator 인터페이스로 사용 가능!
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

7. Adapter Pattern 구현 이슈

1. 얼마나 많은 변환이 필요한가?

간단한 변환:

  • 메서드 이름만 다름 (quack()gobble())
  • 매개변수 순서만 다름
  • 간단한 데이터 타입 변환
public void quack() {
    turkey.gobble();  // 단순 위임
}

복잡한 변환:

  • 완전히 다른 동작 (fly() 5번 반복)
  • 여러 메서드를 조합
  • 복잡한 데이터 구조 변환
public void fly() {
    for (int i = 0; i < 5; i++) {
        turkey.fly();
    }
}

2. 양방향 투명성 (Two-way Adapter)

하나의 Adapter가 Target과 Adaptee 양쪽 인터페이스 모두 구현:

// 양방향 Adapter
public class TwoWayAdapter implements Duck, Turkey {
    Duck duck;
    Turkey turkey;
    
    // Duck으로도, Turkey로도 사용 가능!
    public void quack() { 
        if (turkey != null) turkey.gobble();
        else if (duck != null) duck.quack();
    }
    
    public void gobble() { 
        if (duck != null) duck.quack();
        else if (turkey != null) turkey.gobble();
    }
    
    public void fly() {
        if (duck != null) duck.fly();
        else if (turkey != null) turkey.fly();
    }
}

3. 지원하지 않는 기능 처리

Adaptee가 Target의 모든 기능을 지원하지 않을 때:

public void remove() {
    // 방법 1: 예외 던지기
    throw new UnsupportedOperationException();
    
    // 방법 2: 아무것도 안하기 (적절한 경우)
    // 방법 3: 기본 동작 제공
}

8. 핵심 정리

Adapter Pattern의 정의

Adapter Pattern은 한 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환합니다. 호환되지 않는 인터페이스 때문에 함께 동작할 수 없는 클래스들을 함께 작동하도록 만듭니다.

구성 요소

요소역할책임
Target클라이언트가 사용하는 인터페이스애플리케이션의 요구사항 정의
AdapterTarget을 구현하고 Adaptee를 포함인터페이스 변환 및 호출 위임
Adaptee실제 기능을 가진 클래스실제 작업 수행
ClientTarget 인터페이스 사용비즈니스 로직 실행

언제 사용하는가?

  • 레거시 코드 통합: 오래된 시스템과 새 시스템 연결
  • 라이브러리 교체: 다른 벤더의 라이브러리로 전환
  • 인터페이스 불일치: 사용하고 싶은 클래스의 인터페이스가 맞지 않을 때
  • 재사용성 향상: 기존 코드를 수정하지 않고 새 기능 통합
  • 표준 인터페이스 적용: 다양한 구현을 단일 인터페이스로 통일

Object Adapter vs Class Adapter

특성Object AdapterClass Adapter
구현 방식Composition (Has-A)Inheritance (Is-A)
Java 지원✅ 지원❌ 다중 상속 불가
유연성높음 (런타임 교체 가능)낮음 (컴파일타임 고정)
권장도⭐⭐⭐⭐⭐⭐⭐

핵심 원칙

  1. 단일 책임: Adapter는 오직 인터페이스 변환만 담당
  2. 개방-폐쇄: 기존 코드 수정 없이 새 기능 추가
  3. 의존성 역전: Client는 추상화(Target)에만 의존
  4. 느슨한 결합: Client와 Adaptee는 Adapter를 통해서만 상호작용

장점 vs 단점

장점:

  • 기존 코드 수정 없이 호환성 확보
  • 재사용성 증가
  • 단일 책임 원칙 준수
  • 런타임에 Adapter 교체 가능 (Object Adapter)

단점:

  • 전체 코드 복잡도 증가 (클래스 수 증가)
  • 간단한 변환에도 새 클래스 필요
  • 과도한 사용 시 코드 이해 어려움

실전 팁

// ✅ Good: 명확한 Adapter 이름
public class LegacyPaymentAdapter implements ModernPaymentGateway

// ❌ Bad: 모호한 이름
public class PaymentConverter

// ✅ Good: 변환 로직만 포함
public void pay(double amount) {
    legacySystem.makePayment(amount * 100);  // 달러 → 센트 변환
}

// ❌ Bad: 비즈니스 로직 포함
public void pay(double amount) {
    if (amount > 1000) sendEmail();  // Adapter 책임 초과!
    legacySystem.makePayment(amount * 100);
}

9. 관련 패턴

  • Decorator: 인터페이스는 변경하지 않고 기능만 추가
  • Facade: 복잡한 시스템을 단순한 인터페이스로 제공
  • Proxy: 동일한 인터페이스로 접근 제어
  • Bridge: 구현과 추상화를 분리

Adapter vs Decorator vs Proxy:

Adapter:   다른 인터페이스로 변환
Decorator: 같은 인터페이스, 기능 추가
Proxy:     같은 인터페이스, 접근 제어
profile
다들 응원합니다.

0개의 댓글