어댑터 패턴
클래스의 인터페이스를 사용자가 원하는 인터페이스 형태로 적응(변환)시킨다. 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시킨다.
구조
- Target - 사용자가 사용할 응용 분야에 종속적인 인터페이스를 정의하는 클래스
- Adaptee - 인터페이스의 적응이 필요한 기존 인터페이스를 정의하는 클래스
- Adapter - Target 인터페이스에 Adaptee의 인터페이스를 적응시키는 클래스
기본 인터페이스
//클래스 어댑터 - 타겟 인터페이스에 구체 Adaptee를 실행시킨다.
public interface ITarget
{
public void Operate();
}
public class Adaptee
{
public void SpecificOperation()
{
Console.WriteLine($"SpecificOperation");
}
}
public class Adapter : Adaptee, ITarget
{
public void Operate()
{
SpecificOperation();
}
}
//객체 어댑터 - 적응시킬 Adaptee를 입력받아 타겟 인터페이스에서 사용할 수 있다.
public interface ITarget
{
public void Operate();
}
public interface IAdaptee
{
public void SpecificOperation();
}
public class Adaptee : IAdaptee
{
public void SpecificOperation()
{
Console.WriteLine($"SpecificOperation");
}
}
public class Adapter : ITarget
{
private IAdaptee adaptee;
public Adapter(IAdaptee _adaptee)
{
this.adaptee = _adaptee;
}
public void Operate()
{
adaptee.SpecificOperation();
}
}
왜 사용해야 할까?
어댑터 패턴은 호환성이 없는 두 인터페이스 때문에 동시에 사용할 수 없는 클래스들을 함께 동작할 수 있는 해결책을 제시한다.
윈도우에서 동작하도록 만들어진 프로그램 A가 있다고 하자. 이 프로그램이 인기가 많아져서 추가로 맥OS용 프로그램도 만드려고 한다.
하지만 처음부터 끝까지 다시만드는 것은 노력과 자산이 너무 많이 든다. 이를 빠르고 쉽게 해결하는 방법은, 프로그램이 맥OS에서도 돌아갈 수 있도록 적응시키면 된다.
public class WindowToMacAdapter : MacOS
{
WindowProgram program;
public WindowToMacAdapter(WindowProgram windows)
{
program = windows;
}
public void operate()
{
program.Operation();
}
}
WindowsProgram windowProgram = new windowProgram();
MacOSProgram macOSprogram = new WindowToMacAdapter(windowProgram);
macOSprogram.operate(); //윈도우와 맥은 호환성이 없지만 어댑터를 통해 맥을 통한 명령을 통해 윈도우 명령을 실행한다.
어댑터 패턴을 사용하면 기존 클래스를 수정하지 않고 기능을 확장하거나 재사용할 수 있다. 이는 레거시 코드들을 수정하지 않고 재사용할 수 있는 큰 이점이 된다.
예를 들어, 개발자 A씨는 총을 무기로 삼고 싸우는 FPS 게임을 만들기 위해 BaseGun 인터페이스를 준비했다.
public Interface BaseGun
{
public void Shoot(); //사격
public void Reload(); //장전
public void Special(); //스폐셜 공격
}
BaseGun의 구현 클래스로 여러 총 무기를 만들었는데, 예정이 변경되어 총 이외에도 칼, 도끼, 활 등 여러 무기들을 사용하게 되었기 때문에 BaseWeapon 인터페이스를 추가로 구현했다.
public Interface BaseWeapon
{
public void Attack();
public void Defense();
public void SpecialAttack();
}
BaseWeapon과 BaseGun은 함수 선언도 다르기 때문에 BaseGun은 BaseWeapon을 상속받을 수 없다. 그렇기 때문에 총을 사용하려면 총 인터페이스를 새로 구현하거나 BaseGun과 서브클래스까지 전부 수정해야 할지도 모른다. 그러나 어댑터 패턴을 이용하면 기존의 BaseGun 클래스를 수정하지 않고 코드를 재사용할 수 있다.
public class GunToWeaponAdapter : BaseWeapon
{
public BaseGun m_gun;
public GunToWeaponAdapter(BaseGun gun)
{
m_gun = gun;
}
public void Attack()
{
m_gun.Shoot()
}
public void Defense()
{
//BaseGun 인터페이스에 함수 Defense()를 선언하거나 공용(혹은 총 전용) Defense() 함수를 사용한다.
//아님 아예 사용하지 않아도 된다!
}
public void SpecialAttack()
{
m_gun.Special();
}
}
public static int Main(string[] args)
{
BaseGun minigun = new Minigun(); //BaseGun 구현 클래스
BaseWeapon w_minigun = new GunToWeaponAdapter(minigun);
w_minigun.Attack();
if(gauge >= 100)
{
w.minigun.SpecialAttack();
}
return 0;
}
이처럼 어댑터 패턴은 기존 레거시 코드나 클래스를 재사용할 수 있도록 도와주기 때문에 실무에서도 자주 쓰이는 패턴이다.
장점
- 기존 코드를 변경하지 않고 원하는 형태의 인터페이스 구현 클래스로 만들어 재사용할 수 있다.(OCP 보장)
- 기존 코드가 하던 일과 특정 인터페이스 구현 클래스로 변환하는 작업을 분리하여 관리할 수 있다.(SRP 보장)
- 호환 불가능한 인터페이스로 인해 동시에 사용할 수 없는 클래스들을 같은 인터페이스로 취급하여 사용할 수 있다.
- 기존 레거시 시스템을 원하는 인터페이스로 재사용할 수 있다.
단점
- 추가 클래스를 구현해야 하기 때문에 복잡도가 증가한다. 너무 많은 복잡도 때문에 추가 클래스 구현이 꺼려진다면 기존 클래스가 해당 인터페이스를 구현하도록 수정하는 것도 하나의 방법이다.
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
08. [Design Pattern] 컴포지트(Composite) 패턴 (1) | 2023.11.25 |
---|---|
07. [Design Pattern] 브릿지(Bridge) 패턴 (0) | 2023.11.23 |
05. [Design Pattern] 싱글톤(Singleton) 패턴 (0) | 2023.11.16 |
04. [Design Pattern] 프로토타입(Prototype) 패턴 (0) | 2023.11.07 |
03. [Design Pattern] 빌더(Builder) 패턴 (0) | 2023.11.07 |