dh_0e

[Design Pattern] Structural Design Patterns I (Adapter Pattern, Bridge Pattern) 본문

Software Analysis & Design/Design Patterns

[Design Pattern] Structural Design Patterns I (Adapter Pattern, Bridge Pattern)

dh_0e 2025. 11. 5. 23:21

구조 디자인 패턴(Structural Design Patterns)

  • 클래스들과 객체들을 더 큰 구조로 조립하여 구조를 유연하고 효율적으로 유지하도록 돕는 패턴

Structural Design Patterns

 

Adapater Pattern

  • 호환성이 없는 인터페이스를 가진 클래스들을 더 큰 구조로 조립하여 구조를 유연하고 효율적으로 유지하도록 돕는 패턴
    • 이미 구축되어 있는 것을 새로운 어떤 것에 사용하려고 할 때 양 쪽 간의 호환성을 유지해주기 위해 사용

  • ex) 주식 시장 모니터링 앱을 만드는데 여러 소스에서 주식 데이터를 XML 형식으로 다운로드 해서 보여준다고 가정
          어느 시점에 타사의 스마트 분석 라이브러리를 통합하여 개선하려고 했는데 이 라이브러리가 JSON 형식으로 입력 요구

  • 특징
    • 코드 유지: 기존 시스템의 코드를 수정하지 않고 재사용성을 높임
    • 호환성: 두 개의 호환되지 않는 인터페이스를 연결
    • 유연성과 확장성을 제공
    • 2가지 어댑터 구조
      1. 객체 어댑터 구조(Composition/포함): 어댑터가 서비스 객체(adaptee)를 포함(wrapping)하여 기능을 위임
      2. 클래스 어댑터 구조(Inheritance/상속): 어댑터가 서비스 클래스를 상속하고 클라이언트 인터페이스를 구현
        • Java의 단일 상속 제약 때문에 활용도가 제한적

객체 Adpater 구조

 

ex) Object Adapter

객체 Adpater 구현 예시

 

ex) Class Adapter

Class Adapter 구조

  • 클래스 다중 상속은 Java에서 지원하지 않음
    • 다중 구현(extends & implements)로 우회하여 사용할 수 있음
  • C++ 같이 다중 상속을 지원하는 프로그래밍 언어에서 사용할 수 있음

Class Adapter 구현 예시
Adpater Pattern 예시 (못을 원기둥과 사각기둥에 넣을 때)

  • 사용 시기
    • 호환 불가: 새로운 인터페이스와 Legacy가 호환이 되지 않을 때
    • 재사용: 이미 만든 것을 재사용하고자 하나 수정은 하고 싶지 않을 때
    • 구&신 조화: 소프트웨어의 구 버전과 신 버전을 공존시키고 싶을 때

사용 사례

// 1. Adaptee (기존 서비스: 숫자 배열만 받음)
class LegacyChartLibrary {
    public void drawChart(int[] data) {
        System.out.print("차트 그리는 중... 데이터: [ ");
        for (int i : data) {
            System.out.print(i + " ");
        }
        System.out.println("]");
    }
}

// 2. Target (클라이언트가 원하는 인터페이스: 문자열을 주고 싶음)
interface CsvTarget {
    void drawCsvData(String csv);
}

// 3. Adapter (변환기)
// 상속(extends)을 받아서 LegacyChartLibrary의 기능을 가져오고
// 인터페이스(implements)를 맞춰서 클라이언트를 속임
class CsvAdapter extends LegacyChartLibrary implements CsvTarget {

    @Override
    public void drawCsvData(String csv) {
        // --- [핵심] 자료형 변환 로직 (String -> int[]) ---
        
        // 1. 콤마로 쪼개기 ("10,20" -> "10", "20")
        String[] parts = csv.split(",");
        
        // 2. int 배열 만들기
        int[] numbers = new int[parts.length];
        
        // 3. 하나씩 숫자로 변환해서 담기
        for (int i = 0; i < parts.length; i++) {
            numbers[i] = Integer.parseInt(parts[i].trim()); // "10" -> 10 변환
        }
        
        // 4. 부모(Legacy) 메서드 호출! (이제 자료형이 맞음)
        super.drawChart(numbers); 
    }
}

// 4. Client (사용)
public class Main {
    public static void main(String[] args) {
        // 클라이언트는 "10,20,30" 문자열을 넣지만
        Target adapter = new CsvAdapter();
        
        // 내부적으로 int[]로 변환되어 차트가 그려짐
        adapter.drawCsvData("10,20,30,55,99"); 
    }
}
  • 다음과 같이 자료형이 안 맞는 Client와 Service를 어댑터로 연결 해주기도 함

 

Adapter Pattern 장단점

  • 장점
    • SRP(단일 책임 원칙) 준수: 프로그램의 기본 비즈니스 로직(클라이언트)에서 구현(인터페이스) 분리 가능
      • Adapter 클래스는 “변환”이라는 하나의 책임만 맡음 → SRP 준수
      • 인터페이스(Target) 를 클라이언트가 따로 두면 → 클라이언트가 실제 구현(Service)에 직접 의존하지 않음 → 의존성 분리 효과
    • OCP(개방 폐쇄 원칙) 준수: 기존 클래스 코드를 건들지 않고 클라이언트 인터페이스를 통해 어댑터와 작동 
    • 추가로 필요한 메소드가 있다면 어댑터로 빠르게 구현 가능
      • 버그가 발생해도 기존의 클래스에는 버그가 없으므로 어댑터만 중점적으로 조사 (SRP가 지켜지기 때문)

  • 단점
    • 코드 복잡성 증가: 새로운 인터페이스와 어댑터 클래스 세트를 도입해야 함
    • 서비스(adaptee) 클래스를 변경하는 것이 더 간단할 수도 있음

 


 

Bridge Pattern

  • 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층 구조로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 패턴

Shape과 Colore의 조합을 가지는 클래스 구성에서는 새로운 특성이 추가될 때마다 계층 구조가 크게 늘어남

  • 포함 관계로 전환하여 차원 중 하나를 별도의 클래스 계층 구조로 추출
    • Shape 클래스는 색상 객체를 가리키는 reference 필드를 가짐 (색상 객체와 연결하여 표현가능 - 이 연결이 브릿지) 

  • 구조 관점에서 AbstractionImplementation의 역할이 있음
    • Abstraction: 일부 개체에 대한 상위 수준의 제어/기능 레이어
      • 자체적으로 실제 작업을 수행하는 것은 아니고, 작업들은 구현 레이어에 위임해야 함
    • Implementation: 각 기능에 대한 구현부를 담당하는 레이어
  • Abstraction과 Implemntation을 분리하고 브릿지로 연결하여 변화 대응에 독립적으로 확장될 수 있음

추상화와 구현의 분리

  • ex) 앱의 그래픽 사용자 인터페이스 레이어(Abstraction)과 OS의 API(Implementation)

Bridge Pattern 구조

ex) 가전 기기와 리모콘

Bridge Pattern 구조 예시
Implementation 구현 예시
Abstraction & Client 구현 예시

 

Bridge Pattern 장단점

  • 장점
    • OCP(개방 폐쇄 원칙) 준수: 새로운 추상화들과 구현들을 상호 독립적으로 둘 수 있음
      • 추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있음
    • SRP(단일 책임 원칙) 준수: 추상화의 상위 수준 논리와 구현 플랫폼 세부 정보에 집중할 수 있음

  • 단점
    • 계층 구조가 늘어나 코드 복잡성이 증가할 수 있음
      • 단 하나의 클래스만 만들고 확장 가능성이 없다면 불필요한 작업
    • 설계 구조 파악이 안 되면 코드를 파악하는데 어려움이 있을 수 있음 (Abstraction & Implementation 구분)  

 

Bridge Pattern vs Adapter Pattern

  • 코드 생김새(구조)만 보면 둘은 거의 똑같음
    • 다른 객체를 내 품에 안고(Composition), 대신 일을 시키는(Delegation) 구조
  • 패턴의 사용 시기 및 목적이 완전 다름
구분 어댑터 (Adapter) 브릿지 (Bridge)
사용 시기 다 만들고 나서 (After) 설계 단계에서 미리 (Before)
목적 "수습(땜질)" "설계(확장)"
상황 이미 있는 A랑 B가 안 맞네?

→ "중간에 껴서 맞추자."
앞으로 A랑 B가 제각각 엄청 늘어날 것 같네?
비유 110v → 220v 변환 플러그 리모컨(추상) + TV(구현)
  • 브릿지는 일반적으로 사전에 설계되어서 다양한 부분을 독립적으로 개발
  • 어댑터는 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 함
  • ※ 화살표 주의해서 봐보기 (의존성)