안녕하세요! GoF 디자인 패턴 연재, 이번 시간부터는 구조 패턴(Structural Patterns)에 대해 알아봅니다. 구조 패턴은 클래스나 객체들을 조합하여 더 큰 구조를 만드는 것과 관련된 패턴들입니다. 그 첫 번째 주자는 바로 어댑터(Adapter) 패턴입니다!


정의

어댑터 패턴호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록 변환해주는 역할을 하는 패턴입니다. 이름 그대로 '어댑터' 역할을 합니다. 가장 쉽게 떠올릴 수 있는 예는 바로 '돼지코'라고 불리는 여행용 전원 어댑터입니다. 한국의 220V 콘센트와 미국의 110V 플러그는 모양과 전압이 달라 직접 연결할 수 없습니다. 이때 둘 사이에 어댑터를 끼우면, 미국 110V 전자제품을 한국 220V 콘센트에서 사용할 수 있게 되죠.

 

이처럼 어댑터 패턴은 서로 다른 인터페이스 때문에 함께 사용할 수 없는 클래스들을, 중간에서 '어댑터' 클래스를 통해 연결하여 클라이언트가 기대하는 인터페이스로 변환해주는 역할을 합니다.

 

어댑터 패턴은 구현 방식에 따라 '객체 어댑터''클래스 어댑터' 두 가지로 나뉩니다. 여기서는 더 널리 쓰이고 유연한 객체 어댑터 패턴을 중심으로 설명하겠습니다.

  • Target (타겟 인터페이스): 클라이언트가 사용하려는 목표 인터페이스입니다. (예: Korean220V 인터페이스)
  • Client (클라이언트): Target 인터페이스를 사용하여 작업을 요청하는 객체입니다.
  • Adaptee (어댑티): 기존에 존재하지만 인터페이스가 호환되지 않는 클래스입니다. (예: American110VAppliance 클래스)
  • Adapter (어댑터): Target 인터페이스를 구현하고, 내부에 Adaptee 인스턴스를 가집니다. 클라이언트의 요청이 들어오면, Adaptee의 메서드를 호출하여 실제 작업을 위임합니다.

사용예시

앞서 비유로 들었던 110V 가전제품을 220V 콘센트에 연결하는 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 타겟 (Target) 인터페이스 정의

클라이언트가 사용하고자 하는 220V 콘센트 인터페이스입니다.

// Target 인터페이스
public interface Korean220V { void powerOn(); }

2. 어댑티 (Adaptee) 클래스 정의

기존에 존재하던, 인터페이스가 호환되지 않는 110V 가전제품 클래스입니다.

// Adaptee 클래스
public class American110VAppliance {
    public void run() { System.out.println("110V 가전제품이 작동합니다."); }
}

public class VoltageAdapter implements Korean220V {

    // 내부에 Adaptee 객체를 가짐 (Composition)
    private American110VAppliance appliance;

    public VoltageAdapter(American110VAppliance appliance) { this.appliance = appliance; }

    // Target 인터페이스의 메서드를 구현
    @Override
    public void powerOn() { appliance.run(); }
}

클라이언트가 powerOn()을 호출하면, 어댑터는 내부적으로 appliance의 run() 메서드를 호출하여 요청을 변환해줍니다.

3. 클라이언트 (Client) 코드

클라이언트는 Korean220V 인터페이스만 알고 있으며, 어댑터를 통해 110V 제품을 사용할 수 있습니다.

public class Client {
    public static void main(String[] args) {
        // 1. 호환되지 않는 110V 가전제품 생성
        American110VAppliance tv = new American110VAppliance();

        // 2. 어댑터에 110V 가전제품을 연결
        Korean220V adapter = new VoltageAdapter(tv);

        // 3. 클라이언트는 220V 인터페이스를 통해 110V 제품을 사용
        connectTo220VOutlet(adapter);
        
        // 실행 결과:
        // 220V 콘센트에 연결되었습니다.
        // 110V 가전제품이 작동합니다.
    }

    // 클라이언트는 Korean220V 인터페이스에만 의존한다.
    public static void connectTo220VOutlet(Korean220V appliance) {
        System.out.println("220V 콘센트에 연결되었습니다.");
        appliance.powerOn();
    }
}

결론

그럼 어댑터 패턴은 언제 사용하는 걸까요?

  • 기존 코드를 변경하지 않고, 호환되지 않는 인터페이스를 가진 클래스를 사용하고 싶을 때 : 라이브러리나 프레임워크의 클래스를 수정할 수 없을 때 매우 유용합니다.
  • 여러 서브클래스들을 재사용하고 싶지만, 각각의 인터페이스가 목표 인터페이스와 다를 때 : 각 서브클래스마다 어댑터를 만들어 재사용성을 높일 수 있습니다.
  • 기존 클래스와 클라이언트 사이에 결합도를 낮추고 싶을 때 : 클라이언트는 Target 인터페이스에만 의존하므로, 실제 구현(Adaptee)이 변경되어도 클라이언트 코드는 영향을 받지 않습니다.

결합도를 감소시킬 수 있는 고 유지보수성의 패턴이지만 다음과 같은 조건을 고려해야합니다

  • 불필요한 클래스 증가: 간단한 로직임에도 불구하고 어댑터 클래스를 추가로 만들어야 하므로, 시스템의 복잡도가 약간 증가할 수 있습니다.

어댑터 패턴은 기존 시스템에 새로운 구성 요소를 통합하거나, 레거시 코드를 재사용할 때 빛을 발하는 매우 실용적인 패턴입니다. 서로 다른 두 세계를 연결하는 '다리' 역할을 한다고 기억해두시면 좋습니다. 다음 시간에는 어댑터 패턴과 유사하지만 차이가 명확하게 존재하는 브릿지 패턴을 알아보겠습니다

+ Recent posts