Adapter
Adapter 패턴이란?
어댑터 패턴Adapter Pattern은 두 개의 호환되지 않는 인터페이스를 연결하는 역할을 하는 패턴입니다. 이 패턴은 클래스나 객체의 인터페이스를 클라이언트에서 기대하는 다른 인터페이스로 변환하여, 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동할 수 있게 합니다. 어댑터 패턴은 마치 전기 어댑터가 다양한 전압과 플러그 형태를 가진 기기들을 연결하는 것처럼, 소프트웨어에서도 서로 다른 인터페이스를 가진 객체들을 연결하여 함께 사용할 수 있도록 해줍니다.
Adapter 패턴 구조
- Client → Adapter
- 클라이언트는
Adapter
의Request()
메소드를 호출합니다.
- 클라이언트는
- Adapter → Adaptee
Adapter
는Adaptee
의SpecificRequest()
메소드를 호출하여 요청을 변환합니다.
- Adaptee → Adapter
Adaptee
는 응답을Adapter
에 반환합니다.
- Adapter → Client
Adapter
는Adaptee
로부터 받은 응답을 필요한 형태로 변환하여 클라이언트에 반환합니다.
- Adapter → Target
Adapter
는Target
인터페이스를 구현합니다.
Adapter 패턴 적용
Adapter 패턴의 필요성
어댑터 패턴은 기존 코드를 변경하지 않고, 새로운 클래스나 인터페이스와 함께 동작하도록 해야 하는 상황에서 매우 유용합니다. 특히, 레거시 시스템을 현대적인 코드베이스에 통합하거나, 외부 라이브러리나 API와의 호환성을 유지해야 할 때 이 패턴을 사용하면 기존 코드를 재사용할 수 있습니다. 예를 들어, 도서관 관리 시스템에서 새롭게 도입한 외부 도서 정보 제공 API와 기존 시스템이 서로 호환되지 않는 경우, 어댑터 패턴을 사용하여 두 시스템이 원활하게 작동하도록 할 수 있습니다.
잘못된 처리
어댑터 패턴을 사용하지 않고, 서로 다른 인터페이스를 가진 클래스들을 직접 연결하려고 하면 코드가 복잡해지고 유지보수성이 떨어질 수 있습니다. 아래는 도서관 관리 시스템에서 새로운 외부 API와 기존 코드가 직접 연결되는 잘못된 예시입니다.
// 기존 시스템에서 사용되는 도서 클래스
public class LibraryBook
{
public string Title { get; set; }
public string Author { get; set; }
public void DisplayInfo()
{
Console.WriteLine($"Title: {Title}, Author: {Author}");
}
}
// 외부 API에서 제공하는 도서 정보 클래스 (호환되지 않는 인터페이스)
public class ExternalBook
{
public string BookTitle { get; set; }
public string BookAuthor { get; set; }
public void ShowDetails()
{
Console.WriteLine($"BookTitle: {BookTitle}, BookAuthor: {BookAuthor}");
}
}
// 클라이언트 코드에서 외부 API를 사용하려면 기존 코드와 외부 API의 인터페이스를 직접 조정해야 함
public class Program
{
public static void Main(string[] args)
{
ExternalBook externalBook = new ExternalBook { BookTitle = "Design Patterns", BookAuthor = "Erich Gamma" };
// 기존 코드와 외부 API가 호환되지 않으므로, 수동으로 데이터를 매핑해야 함
LibraryBook libraryBook = new LibraryBook { Title = externalBook.BookTitle, Author = externalBook.BookAuthor };
libraryBook.DisplayInfo();
}
}
- 직접적인 데이터 매핑 필요 : 기존 코드와 외부 API가 서로 호환되지 않기 때문에, 수동으로 데이터를 매핑하는 과정이 필요합니다. 이로 인해 코드가 복잡해지고, 코드베이스가 확장될 때 유지보수가 어려워집니다.
- 인터페이스의 결합 : 클라이언트 코드가 외부 API의 내부 구조에 의존하게 되어, 외부 API가 변경되면 클라이언트 코드도 함께 수정해야 하는 문제가 발생합니다.
어댑터 패턴 적용 예시
어댑터 패턴을 사용하면, 기존 코드와 외부 API의 인터페이스를 쉽게 연결할 수 있습니다. 어댑터를 통해 서로 다른 인터페이스를 가진 클래스들이 원활하게 작동하도록 할 수 있습니다.
// 타깃 인터페이스 (기존 시스템에서 기대하는 인터페이스)
public interface IBook
{
void DisplayInfo();
}
// 어댑터 클래스: ExternalBook의 인터페이스를 IBook 인터페이스로 변환
public class BookAdapter : IBook
{
private readonly ExternalBook _externalBook;
public BookAdapter(ExternalBook externalBook)
{
_externalBook = externalBook;
}
public void DisplayInfo()
{
// ExternalBook의 ShowDetails 메서드를 IBook의 DisplayInfo 메서드와 매핑
_externalBook.ShowDetails();
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 외부 API에서 제공하는 도서 정보
ExternalBook externalBook = new ExternalBook { BookTitle = "Design Patterns", BookAuthor = "Erich Gamma" };
// 어댑터를 사용해 IBook 인터페이스로 변환
IBook adaptedBook = new BookAdapter(externalBook);
// 기존 시스템 코드와 호환되는 방식으로 사용
adaptedBook.DisplayInfo();
}
}
IBook 인터페이스
기존 시스템에서 기대하는 인터페이스로, 도서 정보를 표시하는 DisplayInfo()
메서드를 정의합니다.
BookAdapter 클래스
IBook
인터페이스를 구현하고, 외부 API 클래스인 ExternalBook
을 래핑하여, IBook
인터페이스에 맞게 변환합니다. 이 어댑터 클래스는 DisplayInfo()
메서드 호출 시 ExternalBook
의 ShowDetails()
메서드를 호출합니다.
클라이언트 코드
어댑터 패턴을 사용하여, 기존 코드와 외부 API의 인터페이스를 연결합니다. 클라이언트 코드에서는 IBook
인터페이스를 사용하므로, 외부 API가 변경되더라도 어댑터만 수정하면 됩니다.
Adapter 패턴의 구성 요소
클라이언트Client
기존 인터페이스를 통해 객체를 사용하는 코드입니다.
타깃Target 인터페이스
클라이언트가 기대하는 인터페이스를 정의합니다.
어댑터Adapter
타깃 인터페이스를 구현하고, 적응하려는 기존 클래스Adaptee의 인터페이스를 타깃 인터페이스로 변환합니다.
적응자Adaptee
기존 인터페이스를 제공하는 클래스입니다. 어댑터는 이 클래스의 메서드를 호출하여 타깃 인터페이스와의 호환성을 제공합니다.
Adapter 패턴 장단점
장점
코드의 유연성 증가
어댑터 패턴을 사용하면, 서로 호환되지 않는 인터페이스를 가진 클래스들을 쉽게 연결할 수 있습니다. 이를 통해 기존 코드를 수정하지 않고도 새로운 기능을 도입할 수 있으며, 외부 라이브러리나 API와의 호환성을 유지할 수 있습니다.
재사용성 향상
기존 코드를 변경하지 않고, 어댑터를 통해 다양한 외부 클래스와의 연결을 지원할 수 있으므로, 코드의 재사용성이 향상됩니다.
단일 책임 원칙 준수
어댑터는 인터페이스 변환이라는 하나의 책임만을 가지므로, 단일 책임 원칙(SRP)을 잘 준수합니다. 이를 통해 시스템의 유지보수성을 높일 수 있습니다.
단점
복잡성 증가
어댑터 패턴을 사용하면 추가적인 클래스(어댑터 클래스)를 생성해야 하므로, 시스템의 복잡성이 증가할 수 있습니다. 이는 특히 여러 개의 어댑터가 필요할 때 더욱 두드러질 수 있습니다.
성능 저하
어댑터 패턴은 인터페이스 변환을 위한 추가 레이어를 도입하므로, 객체 호출 시 성능이 다소 저하될 수 있습니다. 이는 성능이 중요한 시스템에서는 문제가 될 수 있습니다.
단점 해결 방안
어댑터 클래스의 경량화
어댑터 클래스의 복잡성을 줄이기 위해, 어댑터 클래스 내의 로직을 최소화하고, 단순한 인터페이스 변환 역할만 하도록 설계합니다. 이를 통해 어댑터 클래스를 간결하게 유지하고, 유지보수성을 높일 수 있습니다.
조건부 어댑터 적용
어댑터 패턴을 반드시 필요한 경우에만 적용하고, 가능한 경우 직접적인 인터페이스 변환 없이 사용할 수 있는 방법을 고려합니다. 예를 들어, 새로운 클래스나 인터페이스가 기존 코드와 호환되는지 여부를 먼저 확인한 후, 어댑터가 필요할 때만 적용하는 것이 좋습니다.
캐싱 전략 사용
어댑터 패턴을 사용할 때 성능 저하를 최소화하기 위해, 객체의 상태나 결과를 캐싱하는 전략을 사용할 수 있습니다. 이를 통해 중복된 변환 작업을 줄이고, 성능을 개선할 수 있습니다.
public class CachedBookAdapter : IBook
{
private readonly ExternalBook _externalBook;
private string _cachedDetails;
public CachedBookAdapter(ExternalBook externalBook)
{
_externalBook = externalBook;
}
public void DisplayInfo()
{
if (_cachedDetails == null)
{
// 결과를 캐싱하여 성능 향상
using (var writer = new StringWriter())
{
Console.SetOut(writer);
_externalBook.ShowDetails();
_cachedDetails = writer.ToString();
}
}
Console.WriteLine(_cachedDetails);
}
}
이 예시에서는 CachedBookAdapter
클래스를 통해 DisplayInfo
메서드의 결과를 캐싱하여, 성능을 최적화하는 방법을 보여줍니다.
객체지향 원칙과의 관계
Adapter와 캡슐화
어댑터 패턴은 클라이언트와 구체적인 클래스(적응자) 간의 결합을 줄이고, 인터페이스를 통해 간접적으로 접근할 수 있도록 하여 캡슐화를 촉진합니다. 클라이언트는 어댑터를 통해 적응자의 세부 구현에 접근하지 않으므로, 적응자의 내부 구현이 외부에 노출되지 않습니다.
Adapter와 상속
어댑터 패턴 자체는 상속보다는 구성Composition을 중시하는 패턴입니다.
Adapter와 다형성
어댑터 패턴은 다형성을 활용하여, 동일한 인터페이스를 통해 다양한 클래스들이 같은 방식으로 호출될 수 있게 합니다. 어댑터 패턴에서 클라이언트는 특정 클래스가 아닌 인터페이스에 의존하기 때문에, 다양한 구현체를 유연하게 교체하거나 확장할 수 있습니다.
Adapter와 추상화
어댑터 패턴은 추상화의 장점을 극대화합니다. 클라이언트는 구체적인 적응자 클래스의 세부 사항에 대해 알 필요 없이, 인터페이스(추상화된 클래스)를 통해 적응자와 상호작용할 수 있습니다. 이를 통해 클라이언트는 인터페이스에 의존하며, 구체적인 구현이 변경되더라도 클라이언트 코드에 영향을 미치지 않게 됩니다.
Adapter와 단일 책임 원칙
어댑터 패턴은 인터페이스 변환이라는 단일 책임을 맡은 어댑터 클래스를 도입하여, 기존 클래스나 클라이언트 코드의 복잡성을 줄입니다. 어댑터는 오직 인터페이스 변환만을 담당하므로, 클래스의 책임이 명확하게 분리됩니다.
Adapter와 개방_폐쇄 원칙
어댑터 패턴은 기존 코드(클라이언트나 적응자)를 수정하지 않고도 새로운 인터페이스 변환 기능을 추가할 수 있게 해줍니다. 어댑터를 추가하는 방식으로 기존 코드는 수정하지 않고 확장할 수 있으므로 개방_폐쇄 원칙을 잘 준수합니다.
Adapter와 리스코프 치환 원칙
어댑터 패턴을 통해 클라이언트는 인터페이스를 구현한 어떤 클래스도 동일하게 사용할 수 있습니다. 즉, 어댑터가 다른 구현체로 교체되어도 클라이언트의 동작에 영향을 미치지 않으므로, 리스코프 치환 원칙을 잘 준수합니다.
Adapter와 인터페이스 분리 원칙
어댑터 패턴은 클라이언트가 필요로 하는 특정 인터페이스만 제공하도록 하여, 불필요한 메서드나 인터페이스에 의존하지 않게 합니다. 따라서 클라이언트는 필요한 인터페이스만 구현한 어댑터를 통해 적응자와 상호작용하게 됩니다.
Adapter와 의존 역전 원칙
어댑터 패턴은 클라이언트가 구체적인 클래스(적응자)에 의존하지 않고, 추상화된 인터페이스에 의존하도록 합니다. 어댑터를 통해 클라이언트와 적응자 사이의 의존성을 분리하고, 인터페이스에 의존하게 만듦으로써 의존 역전 원칙을 잘 준수합니다.
맺음말
어댑터 패턴은 서로 호환되지 않는 인터페이스를 가진 클래스들을 연결하여, 기존 코드의 수정 없이 새로운 기능을 통합할 수 있는 유연한 방법을 제공합니다. 특히, 외부 라이브러리나 레거시 시스템과의 통합에서 유용하게 사용됩니다. 다만, 어댑터 패턴을 과도하게 사용하면 복잡성이 증가할 수 있으므로, 필요에 따라 신중하게 적용하는 것이 중요합니다. 어댑터 패턴을 적절히 사용하면 코드의 유연성과 재사용성을 높일 수 있으며, 유지보수성이 뛰어난 시스템을 구축할 수 있습니다.
심화학습
여러 인터페이스를 연결하는 Adapter
때때로 어댑터는 하나의 타깃 인터페이스를 여러 적응자Adaptee 클래스와 연결할 필요가 있습니다. 이럴 때, 어댑터 클래스는 여러 적응자 인스턴스를 관리하면서 각기 다른 메서드를 호출해야 합니다. 이를 위해, 어댑터 클래스가 내부적으로 적응자를 선택하는 로직을 포함하거나, 다양한 적응자에 대한 래퍼 클래스를 사용할 수 있습니다.
public class MultiAdapteeAdapter : IBook
{
private readonly ExternalBook _externalBook;
private readonly AnotherExternalBook _anotherExternalBook;
public MultiAdapteeAdapter(ExternalBook externalBook, AnotherExternalBook anotherExternalBook)
{
_externalBook = externalBook;
_anotherExternalBook = anotherExternalBook;
}
public void DisplayInfo()
{
if (_externalBook != null)
{
_externalBook.ShowDetails();
}
else if (_anotherExternalBook != null)
{
_anotherExternalBook.ShowBookInfo();
}
}
}
Adapter와 Decorator
어댑터 패턴과 데코레이터 패턴Decorator Pattern을 결합하여 사용할 수 있습니다. 예를 들어, 어댑터가 인터페이스를 변환하는 동안 데코레이터 패턴을 사용해 추가적인 기능을 동적으로 추가할 수 있습니다. 이를 통해 코드의 유연성을 더욱 높일 수 있습니다.
예를 들어, 도서관 관리 시스템에서 ExternalBook
클래스를 어댑터 패턴을 통해 기존 시스템의 인터페이스에 맞게 변환하고, 동시에 DisplayInfo
메서드 호출 시 로깅 기능을 추가해야 한다고 가정해 보겠습니다.
// 데코레이터 클래스: 추가적인 기능을 동적으로 부여
public class LoggingBookDecorator : IBook
{
private readonly IBook _book;
public LoggingBookDecorator(IBook book)
{
_book = book;
}
public void DisplayInfo()
{
// 추가적인 로깅 기능
Console.WriteLine("Logging: DisplayInfo method called.");
// 실제 기능 호출
_book.DisplayInfo();
}
}
public class Program
{
public static void Main(string[] args)
{
// 외부 API에서 제공하는 도서 정보
ExternalBook externalBook = new ExternalBook
{
BookTitle = "Design Patterns",
BookAuthor = "Erich Gamma"
};
// 어댑터를 사용해 IBook 인터페이스로 변환
IBook adaptedBook = new BookAdapter(externalBook);
// 데코레이터를 사용해 로깅 기능 추가
IBook loggingBook = new LoggingBookDecorator(adaptedBook);
// 기존 시스템 코드와 호환되는 방식으로 사용
loggingBook.DisplayInfo();
}
}
테스트 가능한 Adapter 설계
어댑터 패턴을 사용할 때, 어댑터 클래스가 테스트 가능하도록 설계하는 것이 중요합니다. 이를 위해, 어댑터 클래스 내부에서 적응자 클래스를 직접 생성하기보다는, 외부에서 주입받도록 설계하면 테스트 시 모킹Mock을 통해 다양한 상황을 시뮬레이션할 수 있습니다.
public class BookAdapter : IBook
{
private readonly ExternalBook _externalBook;
public BookAdapter(ExternalBook externalBook)
{
_externalBook = externalBook;
}
public void DisplayInfo()
{
_externalBook.ShowDetails();
}
}
위 코드에서 ExternalBook
객체는 생성 시 주입되므로, 테스트 시에는 ExternalBook
의 Mock 객체를 주입하여 다양한 테스트 시나리오를 만들 수 있습니다.