
어댑터란 우리가 실생활에서 usb 어댑터, 충전 어댑터 등등으로 호환 되지 않는 제품을 사용하기 위해서 연결역할을 해주는 것을 의미한다.
즉 호환되지 않는 제품을 호환시켜주는 개념이다.
이를 코딩의 관점으로 생각해본다면, 클라이언트에서 직접 사용할 수 없는, 객체 및 클래스를 사용할 수 있게 호환시켜주는 패턴이라고 생각하면 편할 것 같다.
어댑터 패턴에는 크게 어댑티를 객체로 의존시켜서 구현하는 객체 어댑터 패턴, 어댑티를 상속하는 어댑터를 구현해서 만드는 클래스 어댑터 패턴이 존재한다.

객체가 객체를 의존 시켜서, 즉 합성시켜서 어댑터 패턴을 이용하는 방식이다.
합성을 이용했기 때문에, 런타임 시점에 Adaptee가 결정되어서 유연하게 코드를 짤 수 있다.(테스트 코드에서도 마찬가지이다)
// Main에서 직접사용할 수 없는 Service클래스를 Adapter클래스를 시용해서
// 사용할 수 있도록 일종의 연결 장치임.
public class Adapter implements Target {
Service adaptee;
public Adapter(Service adaptee) {
this.adaptee = adaptee;
}
@Override
public void method(int data) {
// TODO Auto-generated method stub
adaptee.specificMethod(data);
}
}
// Main에서 사용하고 싶은 클래스이지만, 현재는 호환이 안되어서 직접 사용할 수 없는 클래스
public class Service {
void specificMethod(int data) {
System.out.println("기존 서비스 기능 호출 + " + data);
}
}
// 어댑터의 추상화 모듈
public interface Target {
void method(int data);
}

객체 어댑터가 합성을 통해서 어댑터를 구현했다면 클래스 어댑터는 상속을 통해서 어댑터를 구현한다.
DI를 하지 않아도, 어댑터를 구현할 수 있고, 객체 어댑터와 달리 컴파일 시점에서 어댑터의 관계가 결정된다.
객체 어댑터의 경우 여러 객체를 의존해서, 어댑터를 구현할 수 있지만, 클래스 어댑터의 경우, 클래스에 대한 다중 상속이 되지않는 자바의 특성때문에 불가능하다.
위의 두가지 특성 때문에 상대적으로 덜 유연한 어댑터 패턴이라고 여겨지므로, 객체 어댑터 패턴을 사용하는게 더 낫지 않나라고 생각한다.
// Main에서 직접사용할 수 없는 Service클래스를 Adapter클래스를 시용해서
// 사용할 수 있도록 일종의 연결 장치임.
public class Adapter extends Service implements Target {
@Override
public void method(int data) {
// TODO Auto-generated method stub
super.specificMethod(data);
}
}
// Main에서 사용하고 싶은 클래스이지만, 현재는 호환이 안되어서 직접 사용할 수 없는 클래스
public class Service {
void specificMethod(int data) {
System.out.println("기존 서비스 기능 호출 + " + data);
}
}
// 어댑터의 추상화 모듈
public interface Target {
void method(int data);
}
어댑터 패턴은 호환이 안되는 객체들을 호환되게 해주는데 포커스를 맞춘다.
예를 들어서 다음과 같은 상황을 고려해보자.
public class OlderPrinter {
public void oldPrint(){
System.out.println("old print");
}
}
기존에 사용하던, OlderPrinter를 사용할 때는 단순히, 이를 생성하고 사용하면 된다.
이 상황에서 NewPrinter클래스들을 만들고, 이 클래스들이 가지고 있는 메서드인 newPrint를 추상화해서, Printable인터페이스를 만들자
public interface Printable {
void newPrint();
}
public class NewPrinter1 implements Printable{
@Override
public void newPrint() {
// TODO Auto-generated method stub
System.out.println("new print1");
}
}
public class NewPrinter2 implements Printable{
@Override
public void newPrint() {
// TODO Auto-generated method stub
System.out.println("new print2");
}
}
만든 newPrinter들을 출력을 하자
public static void main(String[] args) throws NumberFormatException, IOException {
List<Printable> printList=new ArrayList<>();
printList.add(new NewPrinter1());
printList.add(new NewPrinter2());
for(Printable printer: printList){
printer.newPrint();
}
}
여기서 olderPrinter도 printList넣어서 같이 출력을 하고자 한다면, 기존의 olderPrinter는 레거시로써 Printable을 구현하지 않았으므로, 호환되지 않아서 정상적으로 동작하지 않을 것이다.
이를 호환시키기 위해서 PrintAdapter를 만들고 어댑터 패턴을 구현하자.
public class PrintAdapter implements Printable{
OlderPrinter printer;
public PrintAdapter(OlderPrinter printer){
this.printer=printer;
}
@Override
public void newPrint() {
// TODO Auto-generated method stub
printer.oldPrint();
}
}
public static void main(String[] args) throws NumberFormatException, IOException {
List<Printable> printList=new ArrayList<>();
printList.add(new NewPrinter1());
printList.add(new NewPrinter2());
printList.add(new PrintAdapter(new OlderPrinter()));
for(Printable printer: printList){
printer.newPrint();
}
}
이를 통해서 기존의 코드를 변경하지 않고, 호환되게 바꿀 수 있어서 OCP를 잘 지킬 수 있다.
레거시 코드와 새 코드가 인터페이스가 다를 때
위의 예시처럼 기존에 printOld(String)을 쓰던 클래스를 Printable.print() 인터페이스에 맞춰야 할 때
"수정할 수 없는 코드에, 새 규칙을 강제해야 할 때"
서드파티 라이브러리, 외부 API를 우리 인터페이스로 감쌀 때
외부 라이브러리에서 제공하는 메서드와 우리의 시스템 구조가 다를 때
우리가 정의한 인터페이스에 맞게 중간에 어댑터를 둠
예: GoogleMapAPI → MapService 인터페이스로 감싸기
인터페이스를 바꾸지 않고 재사용성 확보하고 싶을 때
런타임에 다른 객체를 유연하게 끼워 넣고 싶을 때 (객체 어댑터)
(이건 전략 패턴처럼 유연한 설계가 필요할 때 자주 씀)