어댑터 패턴
실내 온도를 보여 주는 화면은 섭씨를 쓰는데, 오래된 센서 API는 화씨를 반환한다고 하자. 둘 다 숫자이므로 86을 그대로 넘겨도 문법 오류는 없지만, 화면에는 엉뚱한 온도가 표시된다. 호출부가 기대하는 단위에 맞춰 줘야 한다.
변환을 한 경계에 모으기
화면·로그·경보가 각각 센서를 직접 읽으면 세 곳이 모두 화씨 변환을 기억해야 한다. 센서의 호출 방식이 바뀔 때도 세 곳을 함께 고치게 되니, 변환을 맡는 객체 하나를 두고 내부 코드에는 섭씨를 돌려주는 약속만 보여 주면 된다.
기존 인터페이스를 호출부가 기대하는 인터페이스로 맞춰 주는 구조가 어댑터(Adapter) 패턴이다. Design Patterns의 네 저자를 가리키는 GoF의 분류에서는 구조 패턴에 속한다.1
여기서 인터페이스는 호출하는 쪽이 믿고 사용할 약속이다. ICelsiusThermometer의 ReadCelsius()는 섭씨 값을 돌려준다. FahrenheitAdapter는 이 약속을 제공하면서 내부에서 LegacyThermometer.ReadFahrenheit()를 호출한다. 메서드 이름뿐 아니라 반환값의 단위까지 맞춘다.
객체들의 관계
RoomMonitor는 ICelsiusThermometer를 참조하므로 오래된 센서 API를 알 필요가 없다. 어댑터는 기존 센서를 참조한다. 점선과 빈 삼각형은 인터페이스를 구현한다는 뜻이다.
작은 예제부터
using System;
using System.Globalization;
var legacy = new LegacyThermometer(86m);
ICelsiusThermometer sensor = new FahrenheitAdapter(legacy);
new RoomMonitor(sensor).Print();
public interface ICelsiusThermometer
{
decimal ReadCelsius();
}
public sealed class LegacyThermometer
{
private readonly decimal fahrenheit;
public LegacyThermometer(decimal fahrenheit) => this.fahrenheit = fahrenheit;
public decimal ReadFahrenheit() => fahrenheit;
}
public sealed class FahrenheitAdapter : ICelsiusThermometer
{
private readonly LegacyThermometer legacy;
public FahrenheitAdapter(LegacyThermometer legacy) => this.legacy = legacy;
public decimal ReadCelsius() => (legacy.ReadFahrenheit() - 32m) * 5m / 9m;
}
public sealed class RoomMonitor
{
private readonly ICelsiusThermometer sensor;
public RoomMonitor(ICelsiusThermometer sensor) => this.sensor = sensor;
public void Print()
{
string value = sensor.ReadCelsius().ToString("F1", CultureInfo.InvariantCulture);
Console.WriteLine($"Room: {value} Celsius");
}
}Room: 30.0 CelsiusRoomMonitor.Print()에서 ReadCelsius()를 호출하면 어댑터가 기존 ReadFahrenheit()를 읽고, (F − 32) × 5 / 9로 86 °F를 30 °C로 바꾼다. 5m 같은 표기는 decimal 값이므로 정수 나눗셈으로 계산하지 않는다. LegacyThermometer는 고정된 값을 돌려주는 작은 센서 대역이며, 실제 장비에 접속하지는 않는다.
어댑터에 맡길 일
단위, 인자 순서, 반환 데이터 모양, 외부 업체가 제공하는 라이브러리(SDK)의 메서드 이름처럼 호출부가 몰라도 되는 차이를 이 경계에서 다룬다. 외부 호출에 실패할 수 있다면 애플리케이션의 기존 오류 처리 방식에 맞춰 전달해야 한다. 임의의 기본 온도를 반환하면 호출부는 잘못된 값을 실제 측정값으로 받아들이게 된다.
호출부 하나에서 계산 한 번만 필요하다면 변환 함수로 충분하다. 여러 호출부가 같은 약속을 쓰거나 외부 구현을 교체해야 할 때 어댑터 객체를 두는 이유가 생긴다.
다른 패턴과 비교하면
어댑터는 서로 다른 호출 약속을 맞춘다. 데코레이터는 같은 약속을 유지하면서 호출 앞뒤에 동작을 더한다. 퍼사드는 하위 시스템을 사용하기 쉬운 진입점으로 묶으며, 기존 인터페이스와 호환되지 않는 부품을 맞추는 일이 반드시 필요한 것은 아니다.
같이 보기
전체 예제는 .NET 10 콘솔 프로그램에서 실행할 수 있다.
각주
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns. Adapter 저자 발췌. ↩