본문으로 이동

어댑터 패턴

위키백과, 우리 모두의 백과사전.

소프트웨어 공학에서 어댑터 패턴(adapter pattern)은 인터페이스가 호환되지 않는 클래스들을 함께 사용할 수 있게 해주는 소프트웨어 디자인 패턴(데코레이터 패턴과 이름을 공유하는 래퍼라고도 함)이다.[1]: 244  이는 기존 클래스의 소스 코드를 수정하지 않고도 다른 클래스와 함께 작동하게 만들기 위해 자주 사용된다.

예를 들어, 문서 객체 모델의 인터페이스를 표시 가능한 트리 구조로 변환하는 어댑터가 있다.

개요

[편집]

어댑터[2] 디자인 패턴은 갱 오브 포(Gang of Four)의 23가지 유명한 디자인 패턴 중 하나로, 더 구현하기 쉽고 변경, 테스트, 재사용이 용이한 유연한 객체 지향 소프트웨어를 설계하기 위해 반복되는 설계 문제를 해결하는 방법을 기술한다.

어댑터 디자인 패턴은 다음과 같은 문제를 해결한다.[3]

  • 클라이언트가 요구하는 인터페이스를 갖추지 못한 클래스를 어떻게 재사용할 수 있는가?
  • 인터페이스가 호환되지 않는 클래스들이 어떻게 함께 작동할 수 있는가?
  • 클래스에 대해 대체 인터페이스를 어떻게 제공할 수 있는가?

흔히 (이미 존재하는) 클래스는 단순히 인터페이스가 클라이언트가 요구하는 인터페이스와 일치하지 않는다는 이유로 재사용할 수 없는 경우가 많다.

어댑터 디자인 패턴은 이러한 문제를 해결하는 방법을 기술한다:

  • 클래스(adaptee)의 (호환되지 않는) 인터페이스를 클라이언트가 요구하는 다른 인터페이스(target)로 변환하는 별도의 adapter 클래스를 정의한다.
  • 필요한 인터페이스가 없는 클래스를 사용(재사용)하기 위해 adapter를 통해 작업한다.

이 패턴의 핵심 아이디어는 (이미 존재하는) 클래스의 인터페이스를 변경하지 않고 적응시키는 별도의 adapter를 통해 작업하는 것이다.

클라이언트는 자신이 target 클래스와 직접 작업하는지, 아니면 target 인터페이스가 없는 클래스와 adapter를 통해 작업하는지 알지 못한다.

아래의 UML 클래스 다이어그램을 참고하라.

정의

[편집]

어댑터는 호환되지 않는 두 인터페이스가 함께 작동할 수 있게 해준다. 이것이 어댑터의 실질적인 정의이다. 인터페이스는 호환되지 않을 수 있지만, 내부 기능은 요구 사항을 충족해야 한다. 어댑터 디자인 패턴은 한 클래스의 인터페이스를 클라이언트가 기대하는 인터페이스로 변환함으로써 그렇지 않으면 호환되지 않는 클래스들이 함께 작동할 수 있게 해준다.

사용법

[편집]

어댑터는 래퍼가 특정 인터페이스를 준수해야 하고 다형성 동작을 지원해야 할 때 사용할 수 있다. 반면, 데코레이터 패턴은 런타임에 인터페이스의 동작을 추가하거나 변경할 수 있게 하며, 퍼사드 패턴은 하위 객체에 대해 더 쉽거나 단순한 인터페이스가 필요할 때 사용된다.[1]: 243, 252, 258, 260 

패턴의도
어댑터 또는 래퍼클라이언트가 기대하는 것과 일치하도록 한 인터페이스를 다른 인터페이스로 변환
데코레이터원본 코드를 감싸서 인터페이스에 동적으로 책임을 추가
위임"상속보다 합성"을 지원
퍼사드단순화된 인터페이스를 제공

구조

[편집]

UML 클래스 다이어그램

[편집]
어댑터 디자인 패턴을 위한 예시 UML 클래스 다이어그램[4]

위의 UML 클래스 다이어그램에서, target 인터페이스를 요구하는 client 클래스는 인터페이스가 target 인터페이스와 일치하지 않기 때문에 adaptee 클래스를 직접 재사용할 수 없다. 대신, client는 adaptee를 기반으로 target 인터페이스를 구현하는 adapter 클래스를 통해 작업한다:

  • 객체 어댑터(object adapter) 방식은 런타임에 adaptee 객체에 위임하여(adaptee.specificOperation()) target 인터페이스를 구현한다.
  • 클래스 어댑터(class adapter) 방식은 컴파일 타임에 adaptee 클래스를 상속받아(specificOperation()) target 인터페이스를 구현한다.

객체 어댑터 패턴

[편집]

이 어댑터 패턴에서 어댑터는 자신이 감싸는 클래스의 인스턴스를 포함한다. 이 상황에서 어댑터는 감싸진 객체의 인스턴스에 대한 호출을 수행한다.

UML로 표현된 객체 어댑터 패턴
LePUS3로 표현된 객체 어댑터 패턴

클래스 어댑터 패턴

[편집]

이 어댑터 패턴은 기대되는 인터페이스와 기존의 인터페이스를 모두 구현하거나 상속하는 다중 다형성 인터페이스를 사용한다. 특히 다중 상속을 지원하지 않는 자바와 같은 프로그래밍 언어에서는 기대되는 인터페이스를 순수 인터페이스 클래스로 만드는 것이 일반적이다.[1]: 244 

UML로 표현된 클래스 어댑터 패턴
LePUS3로 표현된 클래스 어댑터 패턴

런타임 어댑터 패턴의 추가적인 형태

[편집]

컴파일 타임 솔루션의 동기

[편집]

classA가 classB에게 데이터를 제공해야 한다고 가정하자(예: String 데이터). 컴파일 타임 솔루션은 다음과 같다:

classB.setStringData(classA.getStringData());

그러나 문자열 데이터의 형식을 변경해야 한다고 가정해 보자. 컴파일 타임 솔루션은 상속을 사용하는 것이다:

public class Format1ClassA extends ClassA {
    @Override
    public String getStringData() {
        return format(toString());
    }
}

그리고 아마도 팩토리 패턴을 통해 런타임에 올바른 "형식 지정" 객체를 생성할 것이다.

런타임 어댑터 솔루션

[편집]

"어댑터"를 사용하는 솔루션은 다음과 같이 진행된다:

  1. 중간 "제공자(provider)" 인터페이스를 정의하고, 데이터 소스(이 예제에서는 ClassA)를 감싸며 적절히 형식이 지정된 데이터를 출력하는 해당 인터페이스의 구현체를 작성한다:
    interface StringProvider {
        public String getStringData();
    }
    
    public class ClassAFormat1 implements StringProvider {
        private ClassA classA = null;
    
        public ClassAFormat1(final ClassA a) {
            classA = a;
        }
    
        public String getStringData() {
            return format(classA.getStringData());
        }
    
        private String format(final String sourceValue) {
            // 소스 객체의 데이터를 필요로 하는 객체에서 요구하는
            // 형식으로 소스 문자열을 조작한다.
            return sourceValue.trim();
        }
    }
    
  2. 제공자의 특정 구현을 반환하는 어댑터 클래스를 작성한다:
    public class ClassAFormat1Adapter extends Adapter {
        public Object adapt(final Object anObject) {
            return new ClassAFormat1((ClassA) anObject);
        }
    }
    
  3. adapter가 런타임에 조회될 수 있도록 전역 레지스트리에 등록한다:
    AdapterFactory.getInstance().registerAdapter(ClassA.class, ClassAFormat1Adapter.class, "format1");
    
  4. 코드에서 ClassA의 데이터를 ClassB로 전송하고자 할 때 다음과 같이 작성한다:
    Adapter adapter = AdapterFactory.getInstance()
        .getAdapterFromTo(ClassA.class, StringProvider.class, "format1");
    StringProvider provider = (StringProvider) adapter.adapt(classA);
    String string = provider.getStringData();
    classB.setStringData(string);
    

    또는 더 간결하게:

    classB.setStringData(((StringProvider)AdapterFactory.getInstance()
        .getAdapterFromTo(ClassA.class, StringProvider.class, "format1")
        .adapt(classA))
        .getStringData()
    );
    
  5. 두 번째 형식으로 데이터를 전송하고자 한다면 다른 어댑터/제공자를 조회하면 된다는 장점이 있다:
    Adapter adapter = AdapterFactory.getInstance()
        .getAdapterFromTo(ClassA.class, StringProvider.class, "format2");
    
  6. 그리고 만약 ClassA로부터의 데이터를 Class C에서 이미지 데이터 등으로 출력하고자 한다면:
    Adapter adapter = AdapterFactory.getInstance()
        .getAdapterFromTo(ClassA.class, ImageProvider.class, "format2");
    ImageProvider provider = (ImageProvider) adapter.adapt(classA);
    classC.setImage(provider.getImage());
    
  7. 이런 방식으로 어댑터와 제공자를 사용하면 클래스 계층 구조를 변경하지 않고도 ClassB와 ClassC가 ClassA를 여러 가지 "뷰"로 볼 수 있다. 일반적으로 이는 기존 객체 계층 구조에 소급 적용될 수 있는, 객체 간의 임의적인 데이터 흐름을 위한 메커니즘을 허용한다.

어댑터 패턴의 구현

[편집]

어댑터 패턴을 구현할 때 명확성을 위해 제공자 구현에 [ClassName]To[Interface]Adapter와 같은 클래스 이름을 적용할 수 있다. 예를 들어 DAOToProviderAdapter가 있다. 이는 매개변수로 어댑티(adaptee) 클래스 변수를 갖는 생성자 메서드를 가져야 한다. 이 매개변수는 [ClassName]To[Interface]Adapter의 인스턴스 멤버로 전달된다. clientMethod가 호출되면, 어댑티 인스턴스에 접근하여 필요한 데이터를 가져오고 해당 데이터에 대해 원하는 출력을 생성하는 연산을 수행할 수 있다.

자바

[편집]
package org.wikipedia.examples;

interface ILightningPhone {
    void recharge();
    void useLightning();
}

interface IMicroUsbPhone {
    void recharge();
    void useMicroUsb();
}

class Iphone implements ILightningPhone {
    private boolean connector;

    @Override
    public void useLightning() {
        connector = true;
        System.out.println("Lightning connected");
    }

    @Override
    public void recharge() {
        if (connector) {
            System.out.println("Recharge started");
            System.out.println("Recharge finished");
        } else {
            System.out.println("Connect Lightning first");
        }
    }
}

class Android implements IMicroUsbPhone {
    private boolean connector;

    @Override
    public void useMicroUsb() {
        connector = true;
        System.out.println("MicroUsb connected");
    }

    @Override
    public void recharge() {
        if (connector) {
            System.out.println("Recharge started");
            System.out.println("Recharge finished");
        } else {
            System.out.println("Connect MicroUsb first");
        }
    }
}
/* 소스 객체를 감싸면서 대상 인터페이스를 노출함 */
class LightningToMicroUsbAdapter implements IMicroUsbPhone {
    private final ILightningPhone lightningPhone;

    public LightningToMicroUsbAdapter(ILightningPhone lightningPhone) {
        this.lightningPhone = lightningPhone;
    }

    @Override
    public void useMicroUsb() {
        System.out.println("MicroUsb connected");
        lightningPhone.useLightning();
    }

    @Override
    public void recharge() {
        lightningPhone.recharge();
    }
}

public class AdapterDemo {
    static void rechargeMicroUsbPhone(IMicroUsbPhone phone) {
        phone.useMicroUsb();
        phone.recharge();
    }

    static void rechargeLightningPhone(ILightningPhone phone) {
        phone.useLightning();
        phone.recharge();
    }

    public static void main(String[] args) {
        Android android = new Android();
        Iphone iPhone = new Iphone();

        System.out.println("Recharging android with MicroUsb");
        rechargeMicroUsbPhone(android);

        System.out.println("Recharging iPhone with Lightning");
        rechargeLightningPhone(iPhone);

        System.out.println("Recharging iPhone with MicroUsb");
        rechargeMicroUsbPhone(new LightningToMicroUsbAdapter(iPhone));
    }
}

출력

Recharging android with MicroUsb
MicroUsb connected
Recharge started
Recharge finished
Recharging iPhone with Lightning
Lightning connected
Recharge started
Recharge finished
Recharging iPhone with MicroUsb
MicroUsb connected
Lightning connected
Recharge started
Recharge finished

파이썬

[편집]
"""
어댑터 패턴 예제.
"""
from abc import ABCMeta, abstractmethod
from typing import NoReturn

RECHARGE: list[str] = ["Recharge started.", "Recharge finished."]
POWER_ADAPTERS: dict[str, str] = {"Android": "MicroUSB", "iPhone": "Lightning"}
CONNECTED_MSG: str = "{} connected."
CONNECT_FIRST_MSG: str = "Connect {} first."

class RechargeTemplate(metaclass = ABCMeta):
    @abstractmethod
    def recharge(self) -> NoReturn:
        raise NotImplementedError("구현이 필요합니다.")

class FormatIPhone(RechargeTemplate):
    @abstractmethod
    def use_lightning(self) -> NoReturn:
        raise NotImplementedError("구현이 필요합니다.")

class FormatAndroid(RechargeTemplate):
    @abstractmethod
    def use_micro_usb(self) -> NoReturn:
        raise NotImplementedError("구현이 필요합니다.")

class IPhone(FormatIPhone):
    __name__: str = "iPhone"

    def __init__(self):
        self.connector: bool = False

    def use_lightning(self) -> None:
        self.connector = True
        print(CONNECTED_MSG.format(POWER_ADAPTERS[self.__name__]))

    def recharge(self) -> None:
        if self.connector:
            for state in RECHARGE:
                print(state)
        else:
            print(CONNECT_FIRST_MSG.format(POWER_ADAPTERS[self.__name__]))

class Android(FormatAndroid):
    __name__: str = "Android"

    def __init__(self) -> None:
        self.connector: bool = False

    def use_micro_usb(self) -> None:
        self.connector = True
        print(CONNECTED_MSG.format(POWER_ADAPTERS[self.__name__]))

    def recharge(self) -> None:
        if self.connector:
            for state in RECHARGE:
                print(state)
        else:
            print(CONNECT_FIRST_MSG.format(POWER_ADAPTERS[self.__name__]))

class IPhoneAdapter(FormatAndroid):
    def __init__(self, mobile: FormatAndroid) -> None:
        self.mobile: FormatAndroid = mobile

    def recharge(self) -> None:
        self.mobile.recharge()

    def use_micro_usb(self) -> None:
        print(CONNECTED_MSG.format(POWER_ADAPTERS["Android"]))
        self.mobile.use_lightning()

class AndroidRecharger:
    def __init__(self) -> None:
        self.phone: Android = Android()
        self.phone.use_micro_usb()
        self.phone.recharge()

class IPhoneMicroUSBRecharger:
    def __init__(self) -> None:
        self.phone: IPhone = IPhone()
        self.phone_adapter: IPhoneAdapter = IPhoneAdapter(self.phone)
        self.phone_adapter.use_micro_usb()
        self.phone_adapter.recharge()

class IPhoneRecharger:
    def __init__(self) -> None:
        self.phone: IPhone = IPhone()
        self.phone.use_lightning()
        self.phone.recharge()

print("Recharging Android with MicroUSB recharger.")
AndroidRecharger()
print()

print("Recharging iPhone with MicroUSB using adapter pattern.")
IPhoneMicroUSBRecharger()
print()

print("Recharging iPhone with iPhone recharger.")
IPhoneRecharger()

C#

[편집]
namespace Wikipedia.Examples;

using System;

interface ILightningPhone
{
	void ConnectLightning();
	void Recharge();
}

interface IUsbPhone
{
	void ConnectUsb();
	void Recharge();
}

sealed class AndroidPhone : IUsbPhone
{
	private bool isConnected;

	public void ConnectUsb()
	{
		this.isConnected = true;
		Console.WriteLine("Android phone connected.");
	}

	public void Recharge()
	{
		if (this.isConnected)
		{
			Console.WriteLine("Android phone recharging.");
		}
		else
		{
			Console.WriteLine("Connect the USB cable first.");
		}
	}
}

sealed class ApplePhone : ILightningPhone
{
	private bool isConnected;

	public void ConnectLightning()
	{
		this.isConnected = true;
		Console.WriteLine("Apple phone connected.");
	}

	public void Recharge()
	{
		if (this.isConnected)
		{
			Console.WriteLine("Apple phone recharging.");
		}
		else
		{
			Console.WriteLine("Connect the Lightning cable first.");
		}
	}
}

sealed class LightningToUsbAdapter : IUsbPhone
{
	private readonly ILightningPhone lightningPhone;

	private bool isConnected;

	public LightningToUsbAdapter(ILightningPhone lightningPhone)
	{
		this.lightningPhone = lightningPhone;
	}

	public void ConnectUsb()
	{
		this.lightningPhone.ConnectLightning();
	}

	public void Recharge()
	{
		this.lightningPhone.Recharge();
	}
}

public class AdapterDemo
{
    static void Main(string[] args)
    {
	    ILightningPhone applePhone = new ApplePhone();
	    IUsbPhone adapterCable = new LightningToUsbAdapter(applePhone);
	    adapterCable.ConnectUsb();
	    adapterCable.Recharge();
    }
}

출력:

Apple phone connected.
Apple phone recharging.

같이 보기

[편집]

각주

[편집]
  1. 1 2 3 Freeman, Eric; Freeman, Elisabeth; Kathy, Sierra; Bates, Bert (2004). 《Head First Design Patterns》. O'Reilly Media. 244쪽. ISBN 978-0-596-00712-6. OCLC 809772256. 2013년 5월 4일에 원본 문서 (paperback)에서 보존된 문서. 2013년 4월 30일에 확인함.
  2. ↑ Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). 《Design Patterns: Elements of Reusable Object-Oriented Software》. Addison Wesley. 139ff쪽. ISBN 0-201-63361-2.
  3. ↑ “The Adapter design pattern - Problem, Solution, and Applicability”. 《w3sDesign.com》. 2017년 8월 28일에 원본 문서에서 보존된 문서. 2017년 8월 12일에 확인함.
  4. ↑ “The Adapter design pattern - Structure and Collaboration”. 《w3sDesign.com》. 2017년 8월 28일에 원본 문서에서 보존된 문서. 2017년 8월 12일에 확인함.
  • 위키미디어 공용에 어댑터 패턴 관련 미디어 분류가 있습니다.