프로그래밍/알고리즘&자료구조&패턴

디자인 패턴 - 어댑터 패턴

lazy_web_devloper 2025. 8. 18. 10:50
728x90
반응형

JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 어댑터 패턴 (Adapter)

1. 어댑터 패턴이란? 🔌

개념: 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록, 한 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환해주는 패턴입니다.

가장 쉬운 예는 여행용 '돼지코' 어댑터USB-C to USB-A 변환 어댑터입니다. 한국에서 쓰던 220V 플러그는 미국의 110V 콘센트에 맞지 않지만, 어댑터를 중간에 끼우면 문제없이 사용할 수 있습니다. 어댑터 패턴은 이처럼 소프트웨어 세계에서 '돼지코' 역할을 합니다.

2. 왜 사용할까요?

  • 기존 코드 재사용: 수정할 수 없는 레거시 코드나 외부 라이브러리를 현재 시스템의 인터페이스에 맞춰 사용하고 싶을 때 매우 유용합니다.
  • 결합도 감소: 클라이언트 코드는 자신이 기대하는 인터페이스(Target)에만 의존하고, 실제 구현체(Adaptee)에 대해서는 전혀 알 필요가 없습니다. 덕분에 나중에 다른 구현체로 바꿔 끼우기도 쉬워집니다.
  • 개방-폐쇄 원칙 (OCP) 충족: 기존 코드를 수정하지 않고도 새로운 클래스를 시스템에 통합하여 기능을 확장할 수 있습니다.

3. Java로 구현하는 방법

시나리오: 최신 미디어 플레이어를 만들고 있습니다. 이 플레이어는 mp3 파일만 재생할 수 있습니다. 하지만 mp4나 vlc 파일을 재생하는 기존의 다른 플레이어들을 활용하고 싶습니다.

1단계: 클라이언트가 기대하는 목표(Target) 인터페이스 정의

우리 시스템의 표준 규격입니다. 모든 미디어 플레이어는 이 인터페이스를 따라야 합니다.

Java
 
// 클라이언트가 사용하고자 하는 표준 인터페이스
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

2단계: 호환되지 않는 기존 클래스(Adaptee) 준비

우리가 활용하고 싶은, 인터페이스가 다른 기존의 클래스들입니다.

Java
 
// 'mp4' 파일을 재생하는 기능을 가진 기존 플레이어 (Adaptee 1)
class Mp4Player {
    public void playMp4(String fileName) {
        System.out.println("MP4 파일 재생 중: " + fileName);
    }
}

// 'vlc' 파일을 재생하는 기능을 가진 기존 플레이어 (Adaptee 2)
class VlcPlayer {
    public void playVlc(String fileName) {
        System.out.println("VLC 파일 재생 중: " + fileName);
    }
}

3단계: 어댑터(Adapter) 클래스 구현

이 패턴의 핵심입니다. 어댑터는 우리의 표준 인터페이스(MediaPlayer)를 구현하고, 내부적으로 기존 클래스(Mp4Player 등)의 인스턴스를 가집니다. 그리고 표준 메서드가 호출되면, 이를 기존 클래스의 메서드에 맞게 변환하여 호출해 줍니다.

Java
 
public class MediaAdapter implements MediaPlayer {

    // 내부에 호환되지 않는 기존 객체를 가지고 있다.
    private Mp4Player mp4Player;
    private VlcPlayer vlcPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("mp4")) {
            mp4Player = new Mp4Player();
        } else if (audioType.equalsIgnoreCase("vlc")) {
            vlcPlayer = new VlcPlayer();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        // 표준 play() 메서드 호출을 실제 객체의 메서드로 변환하여 호출
        if (audioType.equalsIgnoreCase("mp4")) {
            mp4Player.playMp4(fileName);
        } else if (audioType.equalsIgnoreCase("vlc")) {
            vlcPlayer.playVlc(fileName);
        }
    }
}

4단계: 클라이언트 코드에서 사용하기

클라이언트는 이제 어댑터를 통해 mp4나 vlc 파일을 mp3 플레이어처럼 동일한 방식으로 재생할 수 있습니다.

Java
 
// 우리의 표준 플레이어
public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        // 기본적으로 mp3만 재생 가능
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("MP3 파일 재생 중: " + fileName);
        }
        // 다른 포맷들은 어댑터를 통해 재생
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println(audioType + " 포맷은 지원하지 않습니다.");
        }
    }
}

// 메인 실행부
public class Main {
    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();

        audioPlayer.play("mp3", "beyond_the_horizon.mp3");
        audioPlayer.play("mp4", "alone.mp4");
        audioPlayer.play("vlc", "far_far_away.vlc");
        audioPlayer.play("avi", "mind_me.avi");
    }
}

4. 어댑터 패턴과 스프링 (Spring Framework)

어댑터 패턴은 스프링 프레임워크의 유연성을 구성하는 핵심 원리 중 하나입니다.

  • HandlerAdapter in Spring MVC: 스프링의 DispatcherServlet은 다양한 종류의 핸들러(예: @Controller가 붙은 메서드, HttpRequestHandler 구현체 등)를 처리해야 합니다. DispatcherServlet은 이들을 직접 실행하는 방법을 모르지만, 각 핸들러 종류에 맞는 **HandlerAdapter**를 통해 실행 명령을 내립니다. 즉, HandlerAdapter가 서로 다른 종류의 핸들러들을 DispatcherServlet이 사용할 수 있는 표준 방식으로 변환해주는 어댑터 역할을 합니다.
  • SLF4J (Simple Logging Facade for Java): SLF4J는 다양한 로깅 라이브러리(Logback, Log4j 등)를 동일한 방식으로 사용할 수 있게 해주는 인터페이스 모음입니다. slf4j-api라는 표준 인터페이스에 logback-classic 같은 바인딩(어댑터) 라이브러리를 추가하면, Logback의 기능을 SLF4J 인터페이스로 사용할 수 있게 됩니다.

5. 문제 제시 및 답변 💡

문제: 새로운 시스템을 개발 중인데, 고객사의 오래된 LegacyPrinter 클래스를 사용하여 영수증을 출력해야 합니다. 이 LegacyPrinter는 printDocument(char[] data) 메서드를 가지고 있어, 출력할 내용을 char 배열로만 받습니다. 하지만 우리 시스템은 모든 출력 기능을 print(String text)라는 표준 메서드를 가진 ModernPrinter 인터페이스에 맞춰 개발했습니다.

LegacyPrinter의 소스 코드를 수정할 수 없는 상황에서, 이 프린터를 우리 시스템에 통합하려면 어떻게 해야 할까요?

답변:

어댑터 패턴을 사용하여 PrinterAdapter 클래스를 만들면 됩니다.

  1. PrinterAdapter 클래스는 우리가 사용하는 표준 인터페이스인 **ModernPrinter를 구현(implements)**합니다.
  2. 이 클래스는 내부에 LegacyPrinter 객체를 멤버 변수로 가집니다.
  3. ModernPrinter 인터페이스의 print(String text) 메서드를 오버라이딩합니다.
  4. 오버라이딩한 print 메서드 내부에서, 입력받은 String 타입의 text를 text.toCharArray()를 이용해 char 배열로 변환합니다.
  5. 변환된 char 배열을 인자로 하여, 내부의 LegacyPrinter 객체의 printDocument() 메서드를 호출합니다.

이렇게 하면, 시스템의 다른 부분에서는 LegacyPrinter의 존재를 전혀 모른 채, 표준 ModernPrinter 인터페이스를 사용하는 것처럼 영수증을 출력할 수 있습니다.

728x90
반응형