어댑터 패턴 (Adapter Pattern)

호환되지 않는 인터페이스를 가진 객체들을 함께 동작시키는 패턴. 기존 코드를 수정하지 않고 새로운 인터페이스로 사용할 수 있게 해줍니다. 사용 방식은 객체 어댑터 방식, 클래스 어댑터 방식이 있습니다. DI로 주입시키는 것이 객체 어댑터, 인터페이스 구현이나 상속을 사용하는 것이 클래스 어댑터이며 큰 차이는 없습니다.

 

핵심 정리 - 언제 사용할까?

  • 서드파티 라이브러리를 우리 시스템 인터페이스에 맞춰 사용할 때

 

실무 팁

  • 객체 어댑터 우선 고려 - 유연성과 확장성이 더 좋음
  • 클래스 어댑터는 제한적: 상속/구현 제약이 많아 실무에서 드물게 사용, 다중 구현을 사용할 경우 안의 로직을 직접 작성해야 하는데 그럴 경우 비즈니스 로직을 담게 되므로 어댑터 패턴을 사용하는 유인이 없어짐. protected와 같은 특정한 조건을 가진 클래스에 어댑터를 적용할 때만 클래스 어댑터를 사용하자!

연습 문제

레거시 라이브러리는 xml를 다운로드 받아서 차트 랜더링을 하는데 새롭게 도입한 분석 라이브러리는 json을 사용해야 하는 상황입니다. 아래 코드를 어댑터 패턴으로 리팩토링 해보세요.

class StockMonitoringApp {
    private final StockDataDownloader xmlDownloader = new StockDataDownloader();
    private final XMLChartRenderer legacy = new XMLChartRenderer();
    private final ThirdPartyAnalyticsLibrary thirdParty = new ThirdPartyAnalyticsLibrary();

    public void analyzeStock(String symbol) {
        // 1. XML 형식으로 데이터 다운로드
        Xml xmlData = xmlDownloader.downloadStock(symbol);

        // 2. 차트 렌더링 (XML 데이터와 잘 작동)
        legacy.renderPriceChart(xmlData);
        legacy.renderVolumeChart(xmlData);

        // 분석 라이브러리 사용하려면 JSON으로 변환 필요
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);

        thirdParty.calculateMovingAverage(jsonData);
        thirdParty.predictTrend(jsonData);
    }

    public void compareStocks(String symbol1, String symbol2) {
        Xml xmlData1 = xmlDownloader.downloadStock(symbol1);
        Xml xmlData2 = xmlDownloader.downloadStock(symbol2);

        // 또 변환...
        Json json1 = XmlToJsonConverter.convertJson(xmlData1);
        Json json2 = XmlToJsonConverter.convertJson(xmlData2);

        thirdParty.analyzeVolatility(json1);
        thirdParty.analyzeVolatility(json2);
    }
}
public class Main {
    public static void main(String[] args) {
        StockMonitoringApp app = new StockMonitoringApp();

        System.out.println("====== 단일 종목 분석 ======");
        app.analyzeStock("AAPL");

        System.out.println("\n====== 종목 비교 ======");
        app.compareStocks("AAPL", "GOOGL");
    }
}
public class Json {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private String data;

    public Json(String data) {
        validateJson(data);
        this.data = data;
    }

    private void validateJson(String data) {
        try {
            MAPPER.readTree(data);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid JSON", e);
        }
    }

    @Override
    public String toString() {
        return "Json{" +
                "data='" + data + '\'' +
                '}';
    }
}


@Getter
public class Xml {
    private String data;

    public Xml(String data) {
        validateXml(data);
        this.data = data;
    }

    private void validateXml(String data) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.parse(new InputSource(new StringReader(data)));
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid XML", e);
        }
    }


    @Override
    public String toString() {
        return "Xml{" +
                "data='" + data + '\'' +
                '}';
    }
}
public class StockDataDownloader {
    public Xml downloadStock(String symbol) {
        System.out.println("[XML 다운로드]" + symbol);
        return new Xml("<stock><symbol>" + symbol + "</symbol><price>100</price><volume>1</volume></stock>");
    }
}

public class XMLChartRenderer {
    public void renderPriceChart(Xml xml) {
        System.out.println("[XML 사용 차트 라이브러리] 가격 차트 렌더링" + xml.getData());
    }

    public void renderVolumeChart(Xml xml) {
        System.out.println("[XML 사용 차트 라이브러리]  거래량 차트 렌더링" + xml.getData());
    }
}

public class ThirdPartyAnalyticsLibrary {
    public String calculateMovingAverage(Json jsonData) {
        System.out.println("[JSON 사용 라이브러리] JSON 데이터로 일 이동평균 계산" + jsonData.toString());
        return "";
    }

    public String predictTrend(Json jsonData) {
        System.out.println("[JSON 사용 라이브러리] JSON 데이터로 추세 예측" + jsonData.toString());
        return "BULLISH";
    }

    public String analyzeVolatility(Json jsonData) {
        System.out.println("[JSON 사용 라이브러리] JSON 데이터로 변동성 분석" + jsonData.toString());
        return "";
    }
}

현재 문제점

  • 변환 로직이 비즈니스 로직에 섞임 - 클라이언트가 XmlToJsonConverter를 직접 호출
  • 의존성이 구체 클래스에 결합 - ThirdPartyAnalyticsLibrary에 직접 의존
  • 확장성 부족 - 다른 분석 라이브러리로 교체 시 모든 클라이언트 코드 수정 필요
  • 관심사 분리 실패 - 데이터 변환 책임이 클라이언트에 있음

요구사항

  • Client (클라이언트): 어댑터를 사용하는 비즈니스 로직
    • StockMonitoringApp
  • Target Interface (타겟 인터페이스): 클라이언트가 원하는 인터페이스 (내 시스템 코드/레거시)
    • StockAnalysis (XML 기반)
    • 많은 자료에서 "클라이언트 인터페이스"라고 표현 -> 클라이언트에서 사용해야하는! 인터페이스(혼동 주의!)
  • Adaptee/Service (어댑티): 새로 도입해야 하는 대상
    • ThirdPartyAnalyticsLibrary (JSON 기반)
  • Adapter (어댑터): 타겟과 어댑티를 연결
    • AnalysisClassAdapter AnalyticsObjectAdapter

 

객체 어댑터 방식

  • Target 인터페이스 (StockAnalysis)
    • 서드파티 구체 클래스인 ThirdPartyLibrary는 JSON을 매개변수로 받는데 레거시와 호환될 수 있도록 매개변수를 XML을 받는 인터페이스 선언
    • Target 인터페이스는 나의 회사 코드(클라이언트)에서 사용할
  • Adapter 클래스 (AnalyticsObjectAdapter)
    • Target 인터페이스 구현 + Adaptee(ThirdPartyLibrary) 주입받아 사용함
    • Adapter 클래스는 변환 로직만 담고 있어야 함
  • Client 클래스(StockMonitoringApp)
    • 클라이언트 클래스는 서드파티 구체 클래스를 직접 활용하는게 아니라 Target 인터페이스를 통해 Adapter 객체를 사용하여야 함

 

클래스 어댑터 방식

  • Target 인터페이스 (StockAnalysis)
    • 서드파티 구체 클래스인 ThirdPartyLibrary는 JSON을 매개변수로 받는데 레거시와 호환될 수 있도록 매개변수를 XML로 받는 인터페이스 선언
    • Target 인터페이스는 나의 회사 코드(클라이언트)에서 사용할 표준 계약
  • Adapter 클래스 (AnalysisClassAdapter)
    • Target 인터페이스 구현 + Adaptee(ThirdPartyLibrary)를 상속받음 (자바의 경우 이중 상속이 되지 않기에 이런식으로 사용)
    • Adapter 클래스는 변환 로직만 담고 있어야 함
  • Client 클래스 (StockMonitoringApp)
    • 클라이언트 클래스는 서드파티 구체 클래스를 직접 활용하는게 아니라 Target 인터페이스를 통해 Adapter 객체를 사용하여야 함
    • 객체 어댑터와 사용 방법은 동일 (인터페이스에 의존)

연습문제 정답 - 객체 어댑터 (Object Adapter)

1. Target 인터페이스 (클라이언트가 원하는 인터페이스)

public interface StockAnalysis {
    void calculateMovingAverage(Xml xmlData);
    void predictTrend(Xml xmlData);
    void analyzeVolatility(Xml xmlData);
}

2. Adapter 구현 (Composition 방식)

public class AnalyticsObjectAdapter implements StockAnalysis {
    private ThirdPartyAnalyticsLibrary thirdParty;  // DI로 주입

    public AnalyticsObjectAdapter(ThirdPartyAnalyticsLibrary thirdParty) {
        this.thirdParty = thirdParty;
    }

    @Override
    public void calculateMovingAverage(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        thirdParty.calculateMovingAverage(jsonData);  // 위임
    }

    @Override
    public void predictTrend(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        thirdParty.predictTrend(jsonData);
    }

    @Override
    public void analyzeVolatility(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        thirdParty.analyzeVolatility(jsonData);
    }
} 

3. Client 수정

class StockMonitoringApp {
    private final StockDataDownloader xmlDownloader = new StockDataDownloader();
    private final XMLChartRenderer legacy = new XMLChartRenderer();
    private final StockAnalysis analytics;  // 인터페이스에 의존!

    public StockMonitoringApp(StockAnalysis analytics) {
        this.analytics = analytics;
    }

    public void analyzeStock(String symbol) {
        Xml xmlData = xmlDownloader.downloadStock(symbol);

        legacy.renderPriceChart(xmlData);
        legacy.renderVolumeChart(xmlData);

        // 변환 로직 없이 바로 사용!
        analytics.calculateMovingAverage(xmlData);
        analytics.predictTrend(xmlData);
    }

    public void compareStocks(String symbol1, String symbol2) {
        Xml xmlData1 = xmlDownloader.downloadStock(symbol1);
        Xml xmlData2 = xmlDownloader.downloadStock(symbol2);

        // 변환 없이 깔끔
        analytics.analyzeVolatility(xmlData1);
        analytics.analyzeVolatility(xmlData2);
    }
}

4. Main 코드

java
public class Main {
    public static void main(String[] args) {
        // 의존성을 단계별로 생성
        ThirdPartyAnalyticsLibrary thirdParty = new ThirdPartyAnalyticsLibrary();
        StockAnalysis analytics = new AnalyticsObjectAdapter(thirdParty);
        StockMonitoringApp app = new StockMonitoringApp(analytics);

        System.out.println("====== 단일 종목 분석 ======");
        app.analyzeStock("AAPL");

        System.out.println("\n====== 종목 비교 ======");
        app.compareStocks("AAPL", "GOOGL");
    }
}

 

연습문제 정답 - 클래스 어댑터

1. Target 인터페이스 (동일)

public interface StockAnalysis {
    String calculateMovingAverage(Xml xmlData);
    String predictTrend(Xml xmlData);
    String analyzeVolatility(Xml xmlData);
}

2. Adapter 구현 (Inheritance 방식)

public class AnalysisClassAdapter extends ThirdPartyAnalyticsLibrary  // 상속
                                   implements StockAnalysis {          // 구현

    @Override
    public String calculateMovingAverage(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        return super.calculateMovingAverage(jsonData);  // 부모 메서드 호출
    }

    @Override
    public String predictTrend(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        return super.predictTrend(jsonData);
    }

    @Override
    public String analyzeVolatility(Xml xmlData) {
        Json jsonData = XmlToJsonConverter.convertJson(xmlData);
        return super.analyzeVolatility(jsonData);
    }
}

3. Client 코드 (동일)

class StockMonitoringApp {
    private final StockDataDownloader xmlDownloader = new StockDataDownloader();
    private final XMLChartRenderer legacy = new XMLChartRenderer();
    private final StockAnalysis analytics;  // 인터페이스 의존

    public StockMonitoringApp(StockAnalysis analytics) {
        this.analytics = analytics;
    }

    public void analyzeStock(String symbol) {
        Xml xmlData = xmlDownloader.downloadStock(symbol);

        legacy.renderPriceChart(xmlData);
        legacy.renderVolumeChart(xmlData);

        analytics.calculateMovingAverage(xmlData);
        analytics.predictTrend(xmlData);
    }

    public void compareStocks(String symbol1, String symbol2) {
        Xml xmlData1 = xmlDownloader.downloadStock(symbol1);
        Xml xmlData2 = xmlDownloader.downloadStock(symbol2);

        analytics.analyzeVolatility(xmlData1);
        analytics.analyzeVolatility(xmlData2);
    }
}

4. Main 코드

public class Main {
    public static void main(String[] args) {
        // 클래스 어댑터는 new만 하면 됨 (이미 ThirdParty 기능을 상속으로 보유)
        AnalysisClassAdapter adapter = new AnalysisClassAdapter();
        StockMonitoringApp app = new StockMonitoringApp(adapter);

        System.out.println("====== 단일 종목 분석 ======");
        app.analyzeStock("AAPL");

        System.out.println("\n====== 종목 비교 ======");
        app.compareStocks("AAPL", "GOOGL");
    }
}

 

 

참조 

+ Recent posts