어댑터 패턴 Java 구현 (Adapter Pattern in Java)
한눈에 보기
InputStreamReader, Arrays.asList 등 JDK 속 어댑터 사례를 살펴보고, 클래스 어댑터와 객체 어댑터 두 구현 변형의 차이와 Java에서 객체 어댑터가 선호되는 이유를 정리한다.
최종 업데이트 2026.09.17
Java 표준 라이브러리에는 어댑터 패턴(Adapter Pattern)이 실제로 적용된 사례가 여러 곳에 숨어 있으며, 구현 방식에 따라 클래스 어댑터와 객체 어댑터라는 두 가지 변형이 존재한다. 이 문서는 InputStreamReader, Arrays.asList 같은 JDK 속 어댑터 사례를 살펴보고, Java에서 두 구현 변형을 어떻게 작성하며 왜 객체 어댑터가 선호되는지 정리한다.
JDK 표준 라이브러리 속 어댑터 사례
가장 대표적인 사례는 입출력 스트림의 InputStreamReader다. Java의 입출력 체계는 바이트 스트림(InputStream)과 문자 스트림(Reader)으로 나뉘어 있어서 두 계층은 직접 호환되지 않는다. InputStreamReader는 Reader를 상속하면서 내부에 InputStream을 보관하고, 바이트를 지정된 문자 인코딩으로 해석해 문자로 돌려준다. 즉 바이트 세계의 객체를 문자 세계의 인터페이스에 맞춰 주는 전형적인 객체 어댑터다.
// InputStream(어댑티)을 Reader(타깃) 규격으로 변환
InputStream in = new FileInputStream("data.txt");
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(reader);
컬렉션 프레임워크에도 어댑터가 여럿 있다. Arrays.asList는 배열을 List 인터페이스로 다룰 수 있게 감싸는 어댑터로, 반환된 리스트는 내부적으로 원본 배열을 그대로 참조하기 때문에 크기를 바꾸는 add나 remove를 호출하면 UnsupportedOperationException이 발생한다. 또 오래된 Enumeration과 현대적인 Iterator 사이를 오가는 Collections.enumeration과 Collections.list도 세대가 다른 두 순회 인터페이스를 잇는 어댑터다.
한 가지 주의할 점이 있다. AWT의 WindowAdapter, MouseAdapter처럼 이름에 어댑터가 들어간 클래스는 GoF의 어댑터 패턴이 아니다. 이들은 리스너 인터페이스의 모든 메서드를 빈 구현으로 채워 두어 필요한 메서드만 오버라이드하게 해 주는 편의용 추상 클래스, 즉 기본 구현 제공 목적의 클래스다. 면접에서 이 둘을 구분해 말하면 이해 깊이를 보여 줄 수 있다.
객체 어댑터 구현
객체 어댑터(Object Adapter)는 합성(Composition)을 사용한다. 어댑터가 타깃 인터페이스를 구현하고, 어댑티를 필드로 보관하면서 요청을 위임한다. 섭씨 온도만 반환하는 레거시 센서를 화씨 기반 인터페이스에 맞추는 예를 보자.
// 타깃: 새 시스템이 요구하는 인터페이스
public interface FahrenheitSensor {
double readFahrenheit();
}
// 어댑티: 섭씨만 지원하는 기존 클래스
public class CelsiusSensor {
public double readCelsius() {
return 25.0;
}
}
// 객체 어댑터: 합성으로 어댑티를 보관하고 위임
public class SensorAdapter implements FahrenheitSensor {
private final CelsiusSensor sensor;
public SensorAdapter(CelsiusSensor sensor) {
this.sensor = sensor;
}
@Override
public double readFahrenheit() {
return sensor.readCelsius() * 9.0 / 5.0 + 32.0;
}
}
합성을 쓰기 때문에 런타임에 어떤 어댑티 인스턴스를 감쌀지 자유롭게 선택할 수 있고, 어댑티의 하위 클래스가 들어와도 그대로 동작한다. 생성자 주입 구조라서 테스트 시 가짜 센서를 넣기도 쉽다. 타깃이 메서드 하나짜리 함수형 인터페이스라면 어댑터 클래스를 따로 만들지 않고 sensor -> sensor.readCelsius() * 9.0 / 5.0 + 32.0 같은 람다 한 줄로 변환을 표현할 수도 있는데, 이 역시 객체 어댑터의 축약형으로 이해하면 된다.
클래스 어댑터 구현과 Java의 제약
클래스 어댑터(Class Adapter)는 상속을 사용한다. 어댑터가 어댑티를 상속받으면서 동시에 타깃 인터페이스를 구현하는 방식이다.
// 클래스 어댑터: 어댑티를 상속하고 타깃을 구현
public class SensorClassAdapter extends CelsiusSensor
implements FahrenheitSensor {
@Override
public double readFahrenheit() {
// 상속받은 메서드를 직접 호출
return readCelsius() * 9.0 / 5.0 + 32.0;
}
}
클래스 어댑터는 위임 코드가 없어 간결하고, 필요하면 어댑티의 protected 메서드에 접근하거나 일부 동작을 오버라이드할 수 있다. 그러나 Java는 클래스 다중 상속을 지원하지 않으므로 타깃이 인터페이스일 때만 이 방식이 가능하고, 특정 어댑티 클래스 하나에 컴파일 타임에 고정된다는 한계가 있다. 어댑티가 final 클래스라면 아예 상속할 수 없다. 이런 이유로 Java 진영에서는 결합도가 낮고 유연한 객체 어댑터가 표준적인 선택으로 통한다.
두 변형 비교
| 기준 | 객체 어댑터(합성) | 클래스 어댑터(상속) |
|---|---|---|
| 결합 방식 | 어댑티를 필드로 보관하고 위임한다 | 어댑티를 직접 상속한다 |
| 어댑티 교체 | 런타임에 인스턴스 교체 가능 | 컴파일 타임에 한 클래스로 고정 |
| 어댑티 하위 클래스 지원 | 다형성으로 자연스럽게 지원 | 지원하지 않는다 |
| 어댑티 동작 재정의 | 불가능(공개 메서드만 사용) | 오버라이드로 가능 |
| Java 적용 제약 | 제약 없음 | 타깃이 인터페이스여야 하고 어댑티가 final이면 불가 |
정리하면 어댑티의 내부 동작까지 손대야 하는 특수한 경우가 아니라면 객체 어댑터를 기본값으로 삼는 것이 좋다. 이는 "상속보다 합성을 선호하라"는 객체지향 설계 원칙과도 일치한다.
실무 적용 팁
실무에서는 외부 결제 SDK, 메시징 클라이언트, 저장소 클라이언트처럼 벤더가 다른 라이브러리를 도입할 때 내부 인터페이스를 먼저 정의하고 벤더별 어댑터를 만드는 방식이 흔하다. 이렇게 하면 벤더 교체 시 어댑터 하나만 새로 작성하면 되고, 비즈니스 코드는 내부 인터페이스에만 의존하므로 영향을 받지 않는다. Spring 프레임워크에서도 HandlerAdapter가 다양한 형태의 핸들러를 DispatcherServlet이 다룰 수 있는 공통 규격으로 맞춰 주는 등 같은 원리가 광범위하게 쓰인다. 다만 어댑터 안에는 형식 변환 로직만 두고 비즈니스 규칙을 넣지 않아야 계층이 깨끗하게 유지된다.
면접에서 이렇게 물어본다
- Java 표준 라이브러리에서 어댑터 패턴이 적용된 사례를 두 가지 이상 들고, 각각 무엇이 타깃이고 무엇이 어댑티인지 설명해 보라.
- 클래스 어댑터와 객체 어댑터의 차이는 무엇이며, Java에서는 왜 객체 어댑터가 선호되는가.
Arrays.asList가 반환한 리스트에add를 호출하면 예외가 발생하는 이유를 어댑터 구조와 연결해 설명해 보라.- AWT의
MouseAdapter는 GoF의 어댑터 패턴인가, 아니라면 어떤 목적의 클래스인가. - 외부 결제 SDK를 도입한다면 어댑터 계층을 어떻게 설계할 것인지 인터페이스 수준에서 설명해 보라.
디자인 패턴에서 같이 읽기
- 어댑터 패턴 (Adapter Pattern)호환되지 않는 인터페이스를 클라이언트가 기대하는 형태로 변환해 함께 동작하게 만드는 구조 패턴이다. 타깃, 어댑티, 어댑터, 클라이언트의 협력 구조와 장단점, 유사 패턴과의 차이를 정리한다.
- 컴포지트 패턴 (Composite Pattern)개별 객체와 객체 묶음을 같은 인터페이스로 다루어 트리 구조를 표현하는 구조 패턴이다. 파일과 폴더 예시 코드로 재귀 구조를 설명하고 투명성과 안전성의 트레이드오프를 다룬다.
- 디자인 패턴 개요 (GoF Design Patterns)반복되는 설계 문제의 검증된 해결 구조인 디자인 패턴을 GoF의 생성, 구조, 행위 세 분류로 조망한다. 패턴 선택 기준과 오남용 경계, 면접 대비 학습 전략까지 정리한다.
- 팩토리 메서드 패턴 (Factory Method Pattern)객체 생성 인터페이스를 상위 클래스에 정의하고 어떤 인스턴스를 만들지는 하위 클래스가 결정하는 생성 패턴이다. 추상 팩토리, 단순 팩토리와의 차이를 비교표로 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기