트렌드브리프
어댑터 패턴, 기존 코드를 고치지 않고 인터페이스 맞추기

어댑터 패턴, 기존 코드를 고치지 않고 인터페이스 맞추기

· 게시 · 약 4분 분량

어댑터 패턴은 GoF가 정리한 구조 패턴으로, 인터페이스가 다른 기존 클래스를 고치지 않고 클라이언트가 기대하는 형태로 감싼다. 객체 어댑터와 클래스 어댑터의 차이, 자바 InputStreamReader 같은 실제 사례와 실무 판단 기준을 짚었다.

목차
  1. 어댑터 패턴은 무엇을 해결하나
  2. 객체 어댑터와 클래스 어댑터는 어떻게 다른가
  3. 실무에서는 언제 꺼내 쓰나
  4. 데코레이터·퍼사드와 헷갈리지 않으려면

어댑터 패턴은 서로 호환되지 않는 두 인터페이스를 연결하는 구조 패턴이다. 이미 잘 동작하는 클래스가 있는데 그 클래스가 노출하는 메서드 이름과 형태가 지금 쓰려는 코드의 기대와 다를 때, 원본을 건드리지 않고 중간에 변환 계층을 하나 끼워 넣는다. 소프트웨어 설계 패턴을 정리한 GoF(Gang of Four)의 『디자인 패턴』은 어댑터를 객체 사이의 관계를 다루는 구조 패턴으로 분류한다.

이 글은 객체지향 언어에서 쓰는 어댑터 패턴의 개념, 객체 어댑터와 클래스 어댑터라는 두 변형, 그리고 실무에서 언제 꺼내 쓰는지를 다룬다. 특정 프레임워크가 제공하는 어댑터 유틸리티의 세부 사용법은 범위 밖이다.

어댑터 패턴은 무엇을 해결하나

인터페이스 불일치를 해결한다. 콘센트에 비유하면 이해가 빠르다. 유럽식 220V 기기 플러그를 한국 콘센트에 그대로 꽂을 수 없을 때, 기기나 콘센트를 개조하는 대신 사이에 변환 플러그를 끼운다. 어댑터 패턴이 코드에서 하는 일이 이 변환 플러그와 같다.

구성 요소는 네 가지다. 클라이언트가 호출하고 싶어 하는 형태가 타깃(Target) 인터페이스, 실제로 일을 처리하지만 인터페이스가 다른 기존 클래스가 어댑티(Adaptee), 이 둘을 잇는 변환 계층이 어댑터(Adapter), 어댑터를 거쳐 어댑티를 쓰는 쪽이 클라이언트(Client)다. 어댑터는 타깃 인터페이스를 구현한 뒤 들어온 호출을 어댑티가 알아듣는 메서드로 바꿔 넘긴다.

핵심은 원본 클래스를 수정하지 않는다는 점이다. 어댑티가 외부 라이브러리라 소스를 고칠 수 없거나, 이미 여러 곳에서 쓰고 있어 손대기 부담스러울 때 특히 값어치가 있다.

객체 어댑터와 클래스 어댑터는 어떻게 다른가

어댑티를 감싸는 방식이 다르다. 객체 어댑터는 어댑티의 인스턴스를 필드로 들고 있다가 호출을 위임하는 합성(composition) 방식이고, 클래스 어댑터는 어댑티를 상속받아 그 메서드를 물려받는 방식이다. 아래는 로그 출력 인터페이스가 다른 기존 클래스를 객체 어댑터로 감싼 예다.

// 클라이언트가 기대하는 인터페이스 (Target)
interface Logger {
 void log(String message);
}

// 인터페이스가 다른 기존 클래스 (Adaptee)
class LegacyLogger {
 void writeLine(String text) { System.out.println(text); }
}

// 어댑티 인스턴스를 품고 호출을 위임한다 (객체 어댑터)
class LoggerAdapter implements Logger {
 private final LegacyLogger legacy;
 LoggerAdapter(LegacyLogger legacy) { this.legacy = legacy; }
 public void log(String message) {
  legacy.writeLine(message);
 }
}

실무에서는 객체 어댑터를 먼저 고려한다. 자바처럼 클래스 다중 상속을 허용하지 않는 언어에서는 클래스 어댑터로 여러 어댑티를 동시에 상속하기 어렵고, 합성이 상속보다 결합도(두 코드가 서로 얽매이는 정도)가 낮아 나중에 어댑티를 교체하기도 수월하다.

둥근 핀 플러그와 납작한 슬롯 소켓 사이에 어댑터가 놓여 양쪽 인터페이스를 이어 주는 선화 일러스트
플러그도 소켓도 그대로 두고, 사이의 어댑터가 두 인터페이스를 맞춘다.

실무에서는 언제 꺼내 쓰나

인터페이스가 다른 기존 코드를 고치지 않고 재사용해야 할 때 꺼낸다. 대표적으로 자바 표준 라이브러리의 InputStreamReader가 어댑터다. 바이트 단위로 데이터를 읽는 InputStream을, 문자 단위로 읽는 Reader가 기대하는 형태로 바꿔 준다. 오라클의 자바 API 문서는 InputStreamReader를 바이트 스트림에서 문자 스트림으로 이어 주는 다리라고 설명한다.

정리하면 다음 상황에서 유용하다.

반대로 어댑티와 타깃의 인터페이스가 거의 같거나 처음부터 원하는 인터페이스로 새로 짤 수 있다면 어댑터는 불필요한 계층만 늘린다.

데코레이터·퍼사드와 헷갈리지 않으려면

목적으로 구분하면 명확하다. 어댑터는 인터페이스를 바꾸는 게 목적이다. 기능은 그대로 두고 호출 형태만 다른 인터페이스로 변환한다. 반면 데코레이터 패턴은 인터페이스를 그대로 유지하면서 기능을 덧붙이고, 퍼사드 패턴은 여러 하위 시스템을 묶어 단순한 하나의 창구로 감춘다.

실무에서 셋을 가르는 질문은 하나다. 인터페이스를 바꾸려는 건가, 기능을 더하려는 건가, 복잡한 내부를 감추려는 건가. 어댑터를 골랐다면 답은 첫 번째여야 한다. 기능을 더하려고 어댑터를 쓰기 시작하면 변환 계층에 로직이 쌓여 나중에 유지보수가 어려워진다.

더 많은 글 보기 RSS 구독