JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 어댑터 패턴 (Adapter)
1. 어댑터 패턴이란? 🔌
개념: 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록, 한 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환해주는 패턴입니다.
가장 쉬운 예는 여행용 '돼지코' 어댑터나 USB-C to USB-A 변환 어댑터입니다. 한국에서 쓰던 220V 플러그는 미국의 110V 콘센트에 맞지 않지만, 어댑터를 중간에 끼우면 문제없이 사용할 수 있습니다. 어댑터 패턴은 이처럼 소프트웨어 세계에서 '돼지코' 역할을 합니다.
2. 왜 사용할까요?
- 기존 코드 재사용: 수정할 수 없는 레거시 코드나 외부 라이브러리를 현재 시스템의 인터페이스에 맞춰 사용하고 싶을 때 매우 유용합니다.
- 결합도 감소: 클라이언트 코드는 자신이 기대하는 인터페이스(Target)에만 의존하고, 실제 구현체(Adaptee)에 대해서는 전혀 알 필요가 없습니다. 덕분에 나중에 다른 구현체로 바꿔 끼우기도 쉬워집니다.
- 개방-폐쇄 원칙 (OCP) 충족: 기존 코드를 수정하지 않고도 새로운 클래스를 시스템에 통합하여 기능을 확장할 수 있습니다.
3. Java로 구현하는 방법
시나리오: 최신 미디어 플레이어를 만들고 있습니다. 이 플레이어는 mp3 파일만 재생할 수 있습니다. 하지만 mp4나 vlc 파일을 재생하는 기존의 다른 플레이어들을 활용하고 싶습니다.
1단계: 클라이언트가 기대하는 목표(Target) 인터페이스 정의
우리 시스템의 표준 규격입니다. 모든 미디어 플레이어는 이 인터페이스를 따라야 합니다.
// 클라이언트가 사용하고자 하는 표준 인터페이스
public interface MediaPlayer {
void play(String audioType, String fileName);
}
2단계: 호환되지 않는 기존 클래스(Adaptee) 준비
우리가 활용하고 싶은, 인터페이스가 다른 기존의 클래스들입니다.
// '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 등)의 인스턴스를 가집니다. 그리고 표준 메서드가 호출되면, 이를 기존 클래스의 메서드에 맞게 변환하여 호출해 줍니다.
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 플레이어처럼 동일한 방식으로 재생할 수 있습니다.
// 우리의 표준 플레이어
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 클래스를 만들면 됩니다.
- PrinterAdapter 클래스는 우리가 사용하는 표준 인터페이스인 **ModernPrinter를 구현(implements)**합니다.
- 이 클래스는 내부에 LegacyPrinter 객체를 멤버 변수로 가집니다.
- ModernPrinter 인터페이스의 print(String text) 메서드를 오버라이딩합니다.
- 오버라이딩한 print 메서드 내부에서, 입력받은 String 타입의 text를 text.toCharArray()를 이용해 char 배열로 변환합니다.
- 변환된 char 배열을 인자로 하여, 내부의 LegacyPrinter 객체의 printDocument() 메서드를 호출합니다.
이렇게 하면, 시스템의 다른 부분에서는 LegacyPrinter의 존재를 전혀 모른 채, 표준 ModernPrinter 인터페이스를 사용하는 것처럼 영수증을 출력할 수 있습니다.
'프로그래밍 > 알고리즘&자료구조&패턴' 카테고리의 다른 글
| 디자인패턴 - 퍼사드 패턴 (2) | 2025.08.18 |
|---|---|
| 디자인패턴 - 데코레이터 패턴 (1) | 2025.08.18 |
| 디자인패턴 - 빌더패턴 (1) | 2025.08.18 |
| 디자인패턴 - 팩토리패턴 (1) | 2025.08.18 |
| 디자인패턴 - 싱글톤 패턴 (0) | 2025.08.18 |