Spring/스프링입문을 위한 자바객체지향의 원리와 이해

어댑터 패턴(Adapter Pattern)

1space 2025. 7. 3. 04:01

스프링입문을 위한 자바 객체지향의 원리와 이해』로 공부한 내용을 정리한 글입니다.

 

어댑터 패턴(Adapter Pattern)이란?

소프트웨어 설계에서 서로 다른 인터페이스를 가진 클래스들끼리 통신이 불가능할 때, 중간에서 이 둘을 연결해주는 ‘변환기’ 역할을 수행하는 것이 바로 어댑터 패턴입니다. 마치 전기 콘센트 규격이 달라 직접 연결이 불가능할 때 사용하는 충전기(어댑터)처럼, 코드에서도 형식이 맞지 않는 두 객체를 어댑터를 통해 연결할 수 있습니다.

예를 들어, 휴대폰은 5V DC 전원만을 사용할 수 있지만, 벽의 콘센트는 220V AC 전원입니다. 이 둘을 바로 연결할 수는 없고, 반드시 충전기라는 어댑터를 통해야만 연결이 가능합니다. 이처럼 서로 호환되지 않는 시스템 사이를 중재하여 소통 가능하게 만드는 것이 어댑터 패턴의 핵심입니다.

 

실생활에서의 예: 데이터베이스 연동

다양한 DB 시스템을 하나의 인터페이스로 조작하는 대표적인 사례로는 ODBC나 JDBC가 있습니다. 데이터베이스마다 동작 방식이나 드라이버가 다르지만, JDBC라는 공통 인터페이스를 통해 개발자는 마치 하나의 DB처럼 모든 데이터베이스를 제어할 수 있게 됩니다. 이처럼 각기 다른 대상(DB 드라이버)을 하나의 방식(JDBC)으로 감싸는 구조도 어댑터 패턴의 대표적 활용 예시입니다.

 

어댑터 패턴이 적용되지 않은 코드 구조

처음에는 어댑터 없이 개별 서비스를 직접 사용하는 예시 코드가 나옵니다. ServiceA와 ServiceB는 각각 고유한 이름의 메서드(runServiceA(), runServiceB())를 가지고 있고, 이를 사용하는 클라이언트는 각각의 클래스와 메서드에 직접 접근해야 합니다.

ServiceA sa1 = new ServiceA();
ServiceB sb1 = new ServiceB();

sa1.runServiceA();
sb1.runServiceB();

이 구조에서는 클라이언트가 각 서비스의 구체적인 구현에 의존하기 때문에, 서비스가 변경되면 클라이언트 코드도 함께 수정해야 하는 강결합 구조가 됩니다. 서비스가 늘어나거나 바뀌면 이를 사용하는 코드도 연쇄적으로 영향을 받게 됩니다.

 

어댑터 패턴 적용 후의 구조

이 문제를 해결하기 위해 각 서비스 앞단에 어댑터 클래스를 도입합니다. 어댑터는 내부에서 기존의 구체 클래스(ServiceA, ServiceB)를 사용하지만, 클라이언트에게는 일관된 인터페이스를 제공합니다. 예를 들어 runService()라는 단일 메서드를 통해 어떤 서비스든 호출 가능하게 만드는 것입니다.

public class AdapterServiceA {
    ServiceA sa1 = new ServiceA();

    void runService() {
        sa1.runServiceA();
    }
}

public class AdapterServiceB {
    ServiceB sb1 = new ServiceB();

    void runService() {
        sb1.runServiceB();
    }
}

이제 클라이언트는 더 이상 각각의 서비스에 맞춘 메서드를 호출할 필요가 없습니다. 어댑터 객체를 통해 동일한 메서드 runService()만 사용하면 되므로, 구조가 훨씬 유연하고 변경에 강한 형태로 바뀌게 됩니다.

AdapterServiceA asa1 = new AdapterServiceA();
AdapterServiceB asb1 = new AdapterServiceB();

asa1.runService();
asb1.runService();

 

 

구조적인 이점과 시퀀스 다이어그램

어댑터 패턴이 적용된 구조를 시퀀스 다이어그램으로 보면 다음과 같은 흐름이 보입니다:

  1. 클라이언트가 Adapter 객체를 생성합니다.
  2. Adapter는 내부적으로 원래의 Service 객체를 생성합니다.
  3. 클라이언트는 runService() 메서드를 호출합니다.
  4. Adapter는 내부에서 실제 서비스의 메서드(runServiceA, runServiceB)를 호출합니다.

이 구조는 캡슐화(encapsulation)를 통해 내부 구현을 감추고, 단일 책임 원칙(SRP)의존 역전 원칙(DIP)도 지킬 수 있게 해줍니다. 변화가 자주 일어나는 Service 클래스가 바뀌더라도, 어댑터만 수정하면 되므로 클라이언트는 안정적으로 유지됩니다.

 

마무리 요약

항목 어댑터 미사용 구조 어댑터 사용 구조
호출 방식 runServiceA(), runServiceB() runService()로 통일
클라이언트 의존성 각 서비스에 직접 의존 어댑터에만 의존
변경 시 영향 클라이언트까지 수정 필요 어댑터만 수정하면 됨
설계 품질 강결합, OCP/DIP 위반 느슨한 결합, OCP/DIP 만족
 

 

결국 어댑터 패턴은 변화에 유연하게 대응하고, 일관된 사용 인터페이스를 제공하며, 구조적으로도 안정적인 코드를 만드는 데 매우 유용한 설계 패턴입니다. 스프링 프레임워크에서도 의존성 주입(DI) 구조와 함께 자주 활용되며, 다양한 디자인 패턴들과 결합되어 쓰입니다.

 

 

우리는 이렇게 생긴 두 개의 서비스가 있다고 가정합니다:

// 기존 클래스들
class ServiceA {
    void runServiceA() {
        System.out.println("ServiceA 실행");
    }
}

class ServiceB {
    void startService() {
        System.out.println("ServiceB 실행");
    }
}

둘 다 이름도 다르고 메서드도 다릅니다.
그런데 우리는 클라이언트가 두 클래스의 내부 구조를 몰라도,
"통일된 방식"으로 사용할 수 있게 만들고 싶은 상황입니다.

 

그래서 인터페이스를 만들고, 어댑터를 도입합니다

// 인터페이스 하나로 통일
interface Service {
    void run();  // 공통 인터페이스
}

그리고 각 서비스마다 어댑터 클래스를 만들어줍니다:

// ServiceA 어댑터
class ServiceAAdapter implements Service {
    private ServiceA serviceA = new ServiceA();

    public void run() {
        serviceA.runServiceA();  // 실제 호출은 내부에서 처리
    }
}

// ServiceB 어댑터
class ServiceBAdapter implements Service {
    private ServiceB serviceB = new ServiceB();

    public void run() {
        serviceB.startService();  // 이름이 다르지만 여기서 통일
    }
}

 

 

그리고 클라이언트는 이렇게 사용합니다

public class Client {
    public static void main(String[] args) {
        Service sa = new ServiceAAdapter();
        Service sb = new ServiceBAdapter();

        sa.run();  // 어떤 서비스인지 몰라도 run()만 호출하면 됨
        sb.run();
    }
}

 

정리

[ServiceA]        [ServiceB]
   |                  |
runServiceA()     startService()
   |                  |
   v                  v
[ServiceAAdapter]  [ServiceBAdapter]
          \         /
           \       /
            \     /
             [Service 인터페이스: run()]
                   |
                [Client]
                   |
               service.run()