어댑터 패턴이란
어댑터 패턴(Adapter Pattern)은 서로 호환되지 않는 인터페이스 때문에 함께 사용할 수 없는 클래스들을 연결해주는 디자인 패턴입니다. 이 패턴은 기존의 클래스를 수정하지 않고도 새로운 인터페이스를 통해 클래스들을 함께 작동시킬 수 있도록 해줍니다.
어댑터 패턴은 기존 시스템에 새로운 라이브러리나 프레임워크를 통합하고자 할 때, 또는 서로 다른 시스템 간의 데이터를 교환해야 할 때 매우 유용합니다. 클라이언트는 어댑터를 통해 호환되지 않는 인터페이스를 가진 클래스와 상호작용할 수 있으며, 이는 시스템의 유연성과 재사용성을 크게 향상시킵니다.
쉽게 이해하기 위해 실생활에서 찾아보자면
일본 여행갈때 110V 어댑터 필수죠.
그 어댑터가 이 어댑터 맞습니다 (가가 가가?)
220V제품을 어댑터를 이용해 110V 콘센트와호환시키죠.
어댑터 패턴의 구성
1. Client
어댑터를 사용하는 코드입니다. 클라이언트는 Target 인터페이스를 통해 작업을 요청합니다
2. Target (220V 제품)
클라이언트가 사용하고자 하는 인터페이스입니다. 어댑터 패턴에서는 이 인터페이스를 만족시키기 위해 어댑터가 사용됩니다.
3. Adapter (어댑터)
핵심이죠. Target 인터페이스와 Adaptee 인스턴스 사이의 중간자 역할을 합니다.
Target 인터페이스를 구현하며, 내부적으로 Adaptee의 인스턴스를 사용하여 요청을 처리합니다.
4. Adaptee (110V 콘센트)
어댑터를 통해 재사용되는 기존 클래스입니다. 이 클래스는 Target 인터페이스와 호환되지 않는 인터페이스를 가지고 있습니다.
어댑터 패턴(Adapter Pattern)은 보통 변경이 불가한 라이브러리들이나 호환 되지않는 인터페이스들을 재사용 할때 자주 사용합니다
어댑터 패턴 다이어그램
예제: Audio 플레이어에 기능을 확장시켜보면서 이해해보자
구조적 패턴(structural pattern)부터는 before after코드로 정리하는게 이해가 쉬울 것 같습니다.
시나리오는 아래와 같습니다.
1. 기존 AudioPlayer는 mp3밖에 지원을 하지 않는다
2. AdvancedMediaPlayer라는 Vlc와 mp4를 지원하는 라이브러리(인터페이스 클래스)를 찾게됐다.
3. 근데 호환을 시키려고 하니 기존 라이브러리는 손을 댈 수 없는 상황이다.
4. 그래서 adapter 인터페이스를 하나 만들어서 연결을 시켜주도록 해보자
우선 1,2,3번에 해당하는 before 코드부터 봅시다!
구성은 아래와 같습니다.
before 코드
// 기존 mp3 파일만 재생가능한 player의 인터페이스
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// 기존 mp3 파일만 재생가능한 player의 구현체
public class AudioPlayer implements MediaPlayer{
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
// vlc, mp4 파일도 읽을 수 있는 인터페이스
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// vlc 파일도 읽을 수 있는 구현체
public class VlcPlayer implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
throw new UnsupportedOperationException("Unsupported operation: playMp4");
}
}
// mp4 파일도 읽을 수 있는 구현체
public class Mp4Player implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
throw new UnsupportedOperationException("Unsupported operation: playVlc");
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
여기서 adapter pattern을 사용하여 이어주면 됩니다.
after 코드
동일한 부분이 많지만, 그래도 전부 작성해보겠습니다.
구성은 아래와 같습니다.
// 기존 mp3 파일만 재생가능한 player의 인터페이스 - 타겟(target) 인터페이스
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// 기존 mp3 파일만 재생가능한 player의 구현체 - 타겟 구현체
public class AudioPlayer implements MediaPlayer {
private MediaPlayerAdapter mediaPlayerAdapter;
public void setMediaPlayerAdapter(MediaPlayerAdapter mediaPlayerAdapter) {
this.mediaPlayerAdapter = mediaPlayerAdapter;
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaPlayerAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
// vlc, mp4 파일도 읽을 수 있는 인터페이스 - 어댑티(adaptee) 인터페이스
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// vlc 파일도 읽을 수 있는 구현체 - 어댑티(adaptee) 구현체
public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
throw new UnsupportedOperationException("Unsupported operation: playMp4");
}
}
// mp4 파일도 읽을 수 있는 구현체 - 어댑티(adaptee) 구현체
public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
throw new UnsupportedOperationException("Unsupported operation: playVlc");
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// 어댑터 클래스 - 핵심
public class MediaPlayerAdapter implements MediaPlayer{
private AdvancedMediaPlayer advancedMediaPlayer;
public MediaPlayerAdapter(AdvancedMediaPlayer advancedMediaPlayer) {
this.advancedMediaPlayer = advancedMediaPlayer;
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer.playMp4(fileName);
}
}
}
// Client 코드
public class Main {
public static void main(String[] args) {
// AudioPlayer 인스턴스 생성
AudioPlayer audioPlayer = new AudioPlayer();
// .mp3 파일 재생
audioPlayer.play("mp3", "beyond the horizon.mp3");
// .mp4 파일 재생을 위한 MediaPlayerAdapter 설정
MediaPlayerAdapter mp4Adapter = new MediaPlayerAdapter(new Mp4Player());
audioPlayer.setMediaPlayerAdapter(mp4Adapter);
audioPlayer.play("mp4", "alone.mp4");
// .vlc 파일 재생을 위한 MediaPlayerAdapter 설정
MediaPlayerAdapter vlcAdapter = new MediaPlayerAdapter(new VlcPlayer());
audioPlayer.setMediaPlayerAdapter(vlcAdapter);
audioPlayer.play("vlc", "far far away.vlc");
System.out.println();
audioPlayer.play("avi", "mind me.avi");
}
}
하나하나씩 따라쳐보시면 느낌이 오실겁니다.
실무사용 예
Java에 InputStreamReader와 OutputStreamWriter 가 있네요.
이 클래스들은 바이트 스트림과 문자 스트림 간의 어댑터 역할을 합니다.
예를 들어 InputStream을 받아서 Reader 인터페이스를 구현하여, 바이트를 문자로 변환할 수 있게 해줍니다.
이를 통해 바이트 기반 스트림을 문자 기반 스트림으로 사용할 수 있습니다.
아래는 간단한 예제입니다.
InputStream inputStream = new FileInputStream("example.txt");
Reader reader = new InputStreamReader(inputStream);
결론
장점
1. 호환성 증가: 서로 다른 인터페이스를 가진 클래스들을 함께 사용할 수 있게 해줌으로써, 기존 시스템과 새로운 라이브러리나 모듈 간의 호환성 문제를 해결합니다.
2. 재사용성 향상: 기존에 존재하는 클래스나 코드를 변경하지 않고도 새로운 인터페이스로 재사용할 수 있게 해줍니다.
3. 유지 보수성 개선: 시스템의 유연성을 높여주며, 향후 시스템 변경이나 확장이 필요할 때 수정이 용이해져 유지 보수성이 개선됩니다.
단점
1. 오버헤드 증가: 어댑터 클래스를 추가함으로써 시스템의 복잡성이 증가할 수 있고, 런타임 성능에 미세한 오버헤드가 발생할 수 있습니다.
2. 코드 이해도 및 가독성 저하: 새로운 개발자가 시스템을 처음 접할 때, 어댑터 패턴의 존재와 작동 방식을 이해하는 데 시간이 걸릴 수 있습니다.
3. 과도한 사용 문제: 어댑터 패턴을 과도하게 사용하면, 설계가 불필요하게 복잡해지고, 각각의 클래스 간의 관계를 추적하기 어려워질 수 있습니다.
이는 유지 관리의 어려움으로 이어질 수 있습니다.
참고자료
GOF 디자인패턴 책
'디자인패턴' 카테고리의 다른 글
[디자인패턴-생성] 싱글톤 패턴: 싱글톤 인스턴스 생성의 3가지 방법 (WITH 자바) (0) | 2024.02.13 |
---|---|
[디자인패턴-생성] 프로토타입 패턴: 문서 템플릿 예제를 통한 GOF 디자인 패턴의 이해 (WITH 자바) (0) | 2024.02.12 |
[디자인패턴-생성] 추상팩토리 패턴: 가구 예제를 통한 GOF 디자인 패턴의 이해 (WITH 자바) (1) | 2024.02.12 |
[디자인패턴-생성] 팩토리메서드 패턴: 커피 예제를 통한 GOF 디자인 패턴의 이해(WITH 자바) (0) | 2024.02.10 |
[디자인패턴-생성] 빌더 패턴: 커피 예제를 통한 GOF 디자인 패턴의 이해 (0) | 2024.01.31 |