호환되지 않는 인터페이스를 변환해 함께 쓰게 하는 패턴.
// 우리가 원하는 인터페이스
interface PaymentGateway { void pay(long amount); }
// 외부 라이브러리는 다른 모양이다
class LegacyPay { void doPayment(String amountStr, int currencyCode) { ... } }
// 어댑터가 변환한다
class LegacyPayAdapter implements PaymentGateway {
private final LegacyPay legacy;
public void pay(long amount) {
legacy.doPayment(String.valueOf(amount), 410); // 시그니처를 맞춰 준다
}
}
어디에 쓰나
-
외부 라이브러리를 우리 도메인 인터페이스에 맞춘다
-
레거시 코드를 새 구조에 끼워 넣는다
-
서드파티를 교체 가능하게 만든다 (경계를 우리가 소유한다)
-
"우리 코드가 남의 인터페이스에 맞추는" 것을 뒤집는 장치다
Bridge — 처음부터 분리한다
// 추상(무엇을)과 구현(어떻게)을 별도 계층으로 두고 합성으로 잇는다
abstract class Remote {
protected final Device device; // 구현을 '가진다'
Remote(Device device) { this.device = device; }
void togglePower() { device.setPower(!device.isPowered()); }
}
class AdvancedRemote extends Remote { void mute() { device.setVolume(0); } }
interface Device { void setPower(boolean on); void setVolume(int v); }
Remote 종류 2개 × Device 종류 3개
- 상속으로 하면 클래스가 6개 (조합 폭발)
- 브리지로 하면 2 + 3 = 5개, 새 조합은 자유롭게 만든다
어댑터와 브리지의 차이
시점이 다르다
Adapter 이미 존재하는 것들을 나중에 붙인다 (사후 조치)
Bridge 설계 시점에 두 축을 분리한다 (사전 설계)
형태는 비슷해 보이지만 의도가 완전히 다르다
실무에서 만나는 형태
// 어댑터는 이름에 드러나는 경우가 많다
class SlackNotifierAdapter implements Notifier { ... }
class S3StorageAdapter implements FileStorage { ... }
// 헥사고날 아키텍처의 '어댑터' 가 정확히 이 개념이다
면접 함정
- ❌ "Adapter와 Bridge는 같다" → 사후 변환과 사전 분리로 의도가 다르다.
- ❌ "Adapter는 인터페이스만 바꾼다" → 데이터 변환·단위 변환·예외 변환까지 포함하는 경계 계층이다.
양방향 어댑터
// 두 인터페이스를 서로 변환해야 할 때
class TwoWayAdapter implements OurApi, TheirApi {
// 어느 쪽으로 받아도 동작한다 — 마이그레이션 기간에 유용하다
}
클래스 어댑터 vs 객체 어댑터
- 클래스 어댑터 — 상속으로 구현한다 (다중 상속이 되는 언어에서)
- 자바에서는 인터페이스 하나 + 클래스 하나까지만 가능
- 객체 어댑터 — 합성으로 구현한다 (자바의 일반적 방식)
- 감쌀 대상을 런타임에 바꿀 수 있어 더 유연하다
실무 지침
외부 라이브러리를 쓸 때 ❌ 도메인 코드 곳곳에서 라이브러리 타입을 직접 쓴다
-
라이브러리를 바꾸는 순간 전부 고쳐야 한다 ✅ 우리 인터페이스를 정의하고 어댑터 하나만 라이브러리를 안다
-
교체 시 어댑터만 새로 쓴다
-
테스트에서 가짜 구현을 넣기 쉽다
"경계를 우리가 소유한다" 는 원칙의 구체적 실천이다