개발

[디자인 패턴] 5. 어댑터 패턴

2025년 09월 06일
49

지난 글에서는 싱글톤 패턴을 통해 데이터베이스 연결이나 설정 관리와 같은 전역 리소스를 효율적으로 관리하는 방법을 살펴보았습니다. 이제 우리 시스템은 내부적으로 꽤 탄탄한 구조를 갖추었습니다.

하지만 개발은 혼자 하는 것이 아닙니다. 외부 업체가 제공하는 결제 모듈을 붙여야 하거나, 수년 전에 작성된 레거시 코드를 현재 시스템에 통합해야 하는 상황이 빈번하게 발생합니다. 이때 가장 골치 아픈 문제는 **"인터페이스의 불일치"**입니다.

"코드는 완벽한데, 플러그가 안 맞네?" 하는 상황, 바로 이때 필요한 것이 **어댑터 패턴(Adapter Pattern)**입니다.

문제 상황: 규격이 다른 외부 시스템

우리가 지금까지 공들여 만든 결제 시스템은 PaymentStrategy라는 표준 인터페이스를 따르고 있습니다.

// 우리 시스템의 표준 인터페이스
public interface PaymentStrategy {
    void pay(int amount);
}

그런데 이번에 제휴를 맺은 해외 결제 서비스 업체의 라이브러리(ForeignPaymentService)를 전달받았습니다. 문서를 열어보니 메서드 이름도, 매개변수도 우리 시스템과 전혀 다릅니다.

// 외부에서 제공받은 라이브러리 (수정 불가)
public class ForeignPaymentService {
    public void makePayment(double value, String currency) {
        System.out.println(String.format("해외 결제 진행: %.2f %s", value, currency));
    }
}

우리 시스템은 pay(int)를 호출하는데, 외부 라이브러리는 makePayment(double, String)을 요구합니다. 외부 라이브러리 코드를 직접 수정할 수 있다면 좋겠지만, 보통은 컴파일된 .jar 파일 형태로 제공되거나 수정 권한이 없어 불가능합니다. 그렇다고 우리 시스템 전체를 뜯어고치는 건 배보다 배꼽이 더 큰 격이죠.

110V 플러그를 220V 콘센트에 꽂는 법

여행 가서 전압이 다를 때 '여행용 어댑터(돼지코)'를 쓰는 것과 똑같습니다. 중간에 변환기(Adapter)를 끼워주면 됩니다.

1. 어댑터 클래스 구현

어댑터는 우리 시스템의 인터페이스(PaymentStrategy)를 구현(implements)해야 하며, 내부적으로는 외부 라이브러리 객체를 가지고 있어야 합니다.

public class ForeignPaymentAdapter implements PaymentStrategy {
    private final ForeignPaymentService foreignService;

    // 생성자를 통해 외부 라이브러리 객체를 주입받습니다.
    public ForeignPaymentAdapter(ForeignPaymentService foreignService) {
        this.foreignService = foreignService;
    }

    @Override
    public void pay(int amount) {
        // 1. 우리 시스템의 데이터를 외부 시스템에 맞게 변환
        double doubleAmount = (double) amount;
        String defaultCurrency = "USD"; 

        // 2. 외부 시스템의 메서드를 호출 (위임)
        foreignService.makePayment(doubleAmount, defaultCurrency);
    }
}

이제 pay()가 호출되면, 어댑터가 알아서 데이터를 변환한 뒤 makePayment()를 대신 호출해줍니다.

사용 예시: 감쪽같은 통합

클라이언트 코드는 이 객체가 원래 우리 시스템의 것인지, 외부 시스템을 감싼 어댑터인지 알 필요가 없습니다. 그저 하던 대로 pay()만 호출하면 됩니다.

public class Client {
    public static void main(String[] args) {
        // 기존: 우리 시스템의 카드 결제
        PaymentStrategy cardStrategy = new CreditCardStrategy();
        cardStrategy.pay(10000);

        // 신규: 어댑터를 통한 해외 결제 통합
        ForeignPaymentService foreignService = new ForeignPaymentService();
        PaymentStrategy adapter = new ForeignPaymentAdapter(foreignService);
        
        // 클라이언트는 차이를 느끼지 못하고 동일하게 사용합니다.
        adapter.pay(20000);
    }
}

무엇이 좋아졌을까요?

가장 큰 장점은 재사용성개방-폐쇄 원칙(OCP) 준수입니다.

  1. 기존 코드 보존: 외부 라이브러리를 쓰기 위해 기존의 잘 동작하는 코드를 뜯어고칠 필요가 없습니다.
  2. 안전한 통합: 외부 라이브러리의 코드를 직접 수정하지 않으므로, 라이브러리가 업데이트되더라도 어댑터만 살짝 손보면 됩니다.
  3. 단일 책임: 데이터 변환이나 인터페이스 매핑 로직이 비즈니스 로직과 섞이지 않고 어댑터 클래스에 격리됩니다.

마치며

어댑터 패턴은 서로 다른 인터페이스를 가진 두 개의 시스템을 '연결'하는 데 탁월한 효과를 발휘합니다. 레거시 시스템 리팩토링이나 외부 API 통합 시에 필수적인 패턴이라고 할 수 있습니다.

그런데 때로는 객체를 단순히 연결하는 것을 넘어, 기존 기능에 새로운 기능을 덧붙여서 강화하고 싶을 때가 있습니다. 예를 들어, '결제' 기능에 '로그 기록' 기능을 덧입히고, 그 위에 '암호화' 기능까지 겹겹이 입히고 싶다면 어떻게 해야 할까요? 상속을 계속 사용하는 건 너무 복잡해질 텐데요.

다음 글에서는 객체를 마치 러시아 인형(마트료시카)처럼 감싸서 유연하게 기능을 확장하는 **데코레이터 패턴(Decorator Pattern)**에 대해 알아보겠습니다.


댓글을 불러오는 중...