관리 메뉴

SW

[개발서적] 헤드퍼스트 디자인 패턴 Ch7. 어댑터 패턴과 퍼사드 패턴 본문

개발서적/헤드퍼스트 디자인패턴

[개발서적] 헤드퍼스트 디자인 패턴 Ch7. 어댑터 패턴과 퍼사드 패턴

SWKo 2024. 8. 29. 20:20

1. 객체지향 어댑터

어댑터는 클라이언트로부터 요청을 받아서 새로운 업체에서 제공하는 클래스를 클라이언트가 받아들일 수 있는 형태의 요청으로 변환해주는 중개인 역할을 합니다.

Duck과 Turkey 예시를 보겠습니다.

public interface Duck {
    void quack();
    void fly();
}

public interface Turkey {
    void gobble();
    void fly();
}

public class TurkeyAdapter implements Duck {
    Turkey turkey;

    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    @Override
    public void quack() {
        turkey.gobble();  // 터키의 골골거리는 소리를 꽥꽥거리는 소리로 대체
    }

    @Override
    public void fly() {
        for (int i = 0; i < 5; i++) {  // 터키는 짧게 날기 때문에 5번 반복하여 날도록 한다
            turkey.fly();
        }
    }
}

 

이제 클라이언트 코드에서는 Turkey 객체를 Duck 인터페이스로 사용하여 다음과 같이 호출할 수 있습니다.

public class DuckTestDrive {
    public static void main(String[] args) {
        MallardDuck duck = new MallardDuck();

        WildTurkey turkey = new WildTurkey();
        Duck turkeyAdapter = new TurkeyAdapter(turkey);

        System.out.println("The Turkey says...");
        turkey.gobble();
        turkey.fly();

        System.out.println("\nThe Duck says...");
        testDuck(duck);

        System.out.println("\nThe TurkeyAdapter says...");
        testDuck(turkeyAdapter);
    }

    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

 

  1. TurkeyAdapter 클래스는 Duck 인터페이스를 구현하고, 내부에 Turkey 객체를 가집니다.
  2. TurkeyAdapter의 quack() 메서드는 Turkey 객체의 gobble() 메서드를 호출하여 꽥꽥 소리 대신 골골 소리를 냅니다.
  3. TurkeyAdapter의 fly() 메서드는 터키가 짧게 날기 때문에, 이 동작을 여러 번 반복하여 Duck의 fly() 메서드를 흉내냅니다.
  4. testDuck() 메서드는 Duck 인터페이스를 사용하여 객체를 테스트하며, 이 메서드를 통해 TurkeyAdapter도 Duck처럼 동작하게 됩니다.

어댑터 패턴을 사용하면 서로 다른 인터페이스를 가진 클래스들이 함께 동작하도록 할 수 있습니다.

이 예시에서는 Turkey를 Duck 인터페이스로 사용할 수 있게 하여 코드의 유연성을 높이고, 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있음을 보여줍니다.

 

2. 어댑터 패턴 알아보기

- 타깃 인터페이스 : Duck

- 어뎁티 인터페이스 : Turkey

- TurckeyAdapter에서는 타깃 인터페이스인 Duck을 구현했습니다.

 

1. 클라이언트에서 타깃 인터페이스(Duck)로 메서드(fly, quack)를 호출해서 어댑터에 요청을 보냅니다.

2. 어댑터(TurkeyAdapter)는 어댑티 인터페이스(Turkey)로 그 요청을 어댑티에 관한 하나 이상의 메서드(gobble) 호출로 변환합니다

3. 클라이언트는 호출 결과를 받긴 하지만 중간에 어댑터(TurkeyAdapter)가 있다는 사실을 모릅니다.

 

3. 어댑터 패턴의 정의

어댑터 패턴은 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환합니다. 

인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와줍니다.

4. 객체 어댑터와 클래스 어댑터

 

5. 실전 적용 

어댑터 패턴에서 Enumeration과 Iterator가 자주 언급되는 이유는, 이 두 인터페이스의 역할과 변환이 어댑터 패턴의 전형적인 예이기 때문입니다. 이를 통해 어댑터 패턴이 어떻게 사용되는지 더 잘 이해할 수 있습니다.

Enumeration과 Iterator의 차이

  • Enumeration (Java 1.0):
    • Enumeration은 Java의 초기 컬렉션 인터페이스입니다.
    • 주요 메서드:
      • boolean hasMoreElements(): 더 가져올 요소가 있는지 확인합니다.
      • Object nextElement(): 다음 요소를 반환합니다.
    • 사용법은 비교적 단순하지만, 유연성이 부족합니다. 예를 들어, 요소를 제거하는 기능이 없습니다.
  • Iterator (Java 1.2):
    • Iterator는 Enumeration의 진화된 형태로, 컬렉션을 순회(iterate)하는 더 현대적이고 강력한 방법을 제공합니다.
    • 주요 메서드:
      • boolean hasNext(): 다음 요소가 있는지 확인합니다.
      • Object next(): 다음 요소를 반환합니다.
      • void remove(): 현재 요소를 제거합니다.

어댑터 패턴의 적용

Java 컬렉션 프레임워크가 발전하면서, 기존에 Enumeration을 사용하는 코드를 Iterator를 사용하는 코드로 변경해야 하는 상황이 발생할 수 있습니다. 하지만, 이미 작성된 코드를 변경하는 대신, 어댑터 패턴을 활용해 Enumeration을 Iterator로 변환하는 방법을 사용할 수 있습니다.

import java.util.Enumeration;
import java.util.Iterator;

public class EnumerationIterator implements Iterator<Object> {
    Enumeration<?> enumeration;

    public EnumerationIterator(Enumeration<?> enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    @Override
    public Object next() {
        return enumeration.nextElement();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Remove not supported");
    }
}

어댑터 패턴의 동작

  1. 클라이언트 호출: 클라이언트는 Iterator 인터페이스를 기대하고 있습니다.
  2. 어댑터 역할: EnumerationIterator 어댑터는 Enumeration을 받아 Iterator로 변환합니다.
  3. 요청 변환: 클라이언트가 Iterator의 메서드(hasNext(), next())를 호출하면, 어댑터는 이 요청을 Enumeration의 메서드(hasMoreElements(), nextElement())로 변환하여 호출합니다.
  4. 결과 반환: 클라이언트는 Iterator를 사용하여 결과를 얻습니다. 이 과정에서 Enumeration이 실제로 사용되었지만, 클라이언트는 이를 알지 못합니다.

 

6. 퍼사드 패턴

홈시어터 퍼사드(Home Theater Facade)는 퍼사드(Facade) 패턴의 대표적인 예시 중 하나로, 복잡한 서브시스템들을 단순화하여 사용자가 쉽게 사용할 수 있도록 도와줍니다. 퍼사드 패턴은 여러 개의 클래스로 이루어진 복잡한 시스템에 단순한 인터페이스를 제공하는 구조적 디자인 패턴입니다.

홈시어터 퍼사드의 개념

현대적인 홈시어터 시스템은 여러 개의 서브시스템으로 구성되어 있습니다. 예를 들어:

  • TV 또는 프로젝터
  • DVD 플레이어 또는 블루레이 플레이어
  • 사운드 시스템 (앰프, 스피커 등)
  • 게임 콘솔
  • 조명 시스템

이 각각의 장치는 고유한 기능과 인터페이스를 가지고 있으며, 사용자가 이를 모두 개별적으로 제어하려면 복잡한 절차가 필요합니다.

홈시어터 퍼사드 패턴의 역할

퍼사드 패턴을 적용하여 홈시어터 시스템을 설계하면, 복잡한 과정을 단순화할 수 있습니다. 홈시어터 퍼사드는 여러 서브시스템을 제어하기 위해 고안된 하나의 단순한 인터페이스를 제공합니다.

예를 들어, 사용자가 영화를 보고 싶을 때 다음과 같은 복잡한 과정을 거쳐야 할 수 있습니다:

  1. TV 또는 프로젝터를 켜기
  2. DVD 플레이어를 켜고, 디스크를 넣기
  3. 사운드 시스템을 켜고, 입력 소스를 DVD로 설정하기
  4. 조명을 어둡게 하기

퍼사드를 사용하면, 이러한 과정을 하나의 메서드 호출로 단순화할 수 있습니다.

 

public class HomeTheaterFacade {
    private Amplifier amp;
    private DvdPlayer dvd;
    private Projector projector;
    private Screen screen;
    private TheaterLights lights;
    private PopcornPopper popper;

    public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd, Projector projector, Screen screen, TheaterLights lights, PopcornPopper popper) {
        this.amp = amp;
        this.dvd = dvd;
        this.projector = projector;
        this.screen = screen;
        this.lights = lights;
        this.popper = popper;
    }

    public void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        popper.on();
        popper.pop();
        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);
        dvd.on();
        dvd.play(movie);
    }

    public void endMovie() {
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        dvd.stop();
        dvd.eject();
        dvd.off();
    }
}

홈시어터 퍼사드의 동작

  • watchMovie() 메서드: 이 메서드를 호출하면 퍼사드는 여러 서브시스템의 복잡한 상호작용을 처리하여, 사용자가 영화 시청을 준비하는 모든 단계를 수행합니다.
  • endMovie() 메서드: 이 메서드는 영화를 종료할 때 필요한 모든 단계를 자동으로 수행합니다.
public class HomeTheaterTestDrive {
    public static void main(String[] args) {
        // 홈시어터 서브시스템 객체들 생성
        Amplifier amp = new Amplifier("Top-O-Line Amplifier");
        Tuner tuner = new Tuner("Top-O-Line AM/FM Tuner", amp);
        DvdPlayer dvd = new DvdPlayer("Top-O-Line DVD Player", amp);
        CdPlayer cd = new CdPlayer("Top-O-Line CD Player", amp);
        Projector projector = new Projector("Top-O-Line Projector", dvd);
        TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
        Screen screen = new Screen("Theater Screen");
        PopcornPopper popper = new PopcornPopper("Popcorn Popper");

        // 홈시어터 퍼사드 생성
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(
                amp, dvd, projector, screen, lights, popper);

        // 영화를 보는 과정
        homeTheater.watchMovie("Raiders of the Lost Ark");

        // 영화를 종료하는 과정
        homeTheater.endMovie();
    }
}

7. 어댑터 패턴 vs 퍼사드 패턴

어댑터 패턴과 퍼사드 패턴은 둘 다 구조적 디자인 패턴으로, 기존 코드와의 인터페이스를 조정하거나 단순화하는 데 사용됩니다. 하지만 이 두 패턴은 서로 다른 목적과 상황에서 사용됩니다. 각각의 패턴의 차이점과 사용 목적을 비교해 보겠습니다.

1. 목적 (Purpose)

  • 어댑터 패턴 (Adapter Pattern):
    • 목적: 서로 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 합니다.
    • 상황: 기존의 클래스나 인터페이스를 수정할 수 없거나 수정하지 않고도 다른 인터페이스를 사용해야 할 때 사용됩니다.
    • 사용 예시: Enumeration을 Iterator로 변환하거나, 서로 다른 API를 사용하여 동일한 기능을 수행해야 할 때.
  • 퍼사드 패턴 (Facade Pattern):
    • 목적: 복잡한 시스템의 인터페이스를 단순화하여, 서브시스템의 기능을 쉽게 사용할 수 있도록 합니다.
    • 상황: 복잡한 서브시스템을 가진 라이브러리나 클래스들을 단순화된 인터페이스로 제공하여 사용 편의성을 높이고 싶을 때 사용됩니다.
    • 사용 예시: 홈시어터 시스템의 여러 구성 요소를 간단한 인터페이스로 묶어 사용하거나, 복잡한 API를 쉽게 사용할 수 있게 하는 단순화된 클래스 제공.

2. 구조 (Structure)

  • 어댑터 패턴:
    • 어댑터 패턴은 두 개의 이미 존재하는 인터페이스를 연결합니다. 어댑터 클래스가 한 인터페이스를 구현하고, 내부적으로 다른 인터페이스를 사용하는 객체를 호출하여, 두 인터페이스 간의 호환성을 제공합니다.
    • 예: TurkeyAdapter는 Duck 인터페이스를 구현하면서 Turkey 객체를 사용하여 quack()을 gobble()로 변환합니다.
  • 퍼사드 패턴:
    • 퍼사드 패턴은 복잡한 서브시스템을 단순화하기 위해 설계된 새로운 인터페이스를 제공합니다. 퍼사드는 여러 서브시스템에 대한 메서드 호출을 간단하게 조합하여 제공하는 하나의 간단한 인터페이스를 만듭니다.
    • 예: HomeTheaterFacade는 여러 서브시스템(앰프, DVD 플레이어, 프로젝터 등)을 관리하여 watchMovie()와 같은 단순한 메서드로 복잡한 작업을 수행할 수 있게 합니다.

3. 사용 방법 (Usage)

  • 어댑터 패턴:
    • 어댑터 패턴은 기존 시스템에 새로운 기능이나 호환성을 추가해야 할 때 사용됩니다.
    • 특정 인터페이스를 요구하는 클라이언트 코드가 기존 클래스와 호환되지 않을 때, 어댑터를 통해 그 차이를 메워줍니다.
  • 퍼사드 패턴:
    • 퍼사드 패턴은 복잡한 시스템의 사용을 단순화하고자 할 때 사용됩니다.
    • 복잡한 API나 서브시스템을 사용하는 대신, 퍼사드를 통해 간단한 명령으로 여러 작업을 수행할 수 있습니다.

4. 클라이언트의 인식 (Client Awareness)

  • 어댑터 패턴:
    • 클라이언트는 어댑터가 사용된다는 것을 알고 있습니다. 클라이언트는 어댑터를 통해 요청을 보내며, 어댑터가 요청을 변환하여 실제 작업을 수행합니다.
  • 퍼사드 패턴:
    • 클라이언트는 퍼사드의 내부 작동 방식에 대해 알 필요가 없습니다. 클라이언트는 단순히 퍼사드를 사용하여 복잡한 작업을 간편하게 수행할 수 있으며, 퍼사드가 내부적으로 어떻게 서브시스템을 제어하는지는 알지 못합니다.

5. 유사점과 차이점

  • 유사점:
    • 둘 다 구조적 패턴으로, 복잡한 시스템과의 상호작용을 단순화합니다.
    • 둘 다 클라이언트가 더 쉽게 특정 기능을 사용할 수 있도록 돕습니다.
  • 차이점:
    • 어댑터는 기존의 두 개의 호환되지 않는 인터페이스를 연결하는 반면, 퍼사드는 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공하는 데 중점을 둡니다.
    • 어댑터 패턴은 주로 호환성 문제를 해결하고, 퍼사드 패턴은 복잡성 문제를 해결합니다.

결론

어댑터 패턴과 퍼사드 패턴은 각각의 상황에 맞게 선택해야 합니다. 어댑터 패턴은 인터페이스의 불일치를 해결하는 데 사용하고, 퍼사드 패턴은 복잡한 시스템을 단순화하는 데 사용됩니다. 둘 다 코드의 재사용성과 유지 보수성을 높이지만, 사용 목적과 방식이 다르기 때문에 각 패턴의 장단점을 이해하고 적절히 활용하는 것이 중요합니다.

 

8. 최소 지식 원칙

최소 지식 원칙(Principle of Least Knowledge)에 따르면 객체 사이의 상호작용은 될 수 있으면 아주 가까운 '친구' 사이에만 허용하는 편이 좋습니다. 

진짜 절친에게만 이야기해야 한다.

 

이 원칙을 잘 따르면 여러 클래스가 복잡하게 얽혀 있어서, 시스템의 한 부분을 변경했을 때 다른 부분까지 줄줄이 고쳐야 하는 상황을 방지할 수 있습니다.


FE 에서 사용되는 어댑터 패턴

예제 1

https://velog.io/@superlipbalm/how-i-use-adapter-pattern-in-reactjs

 

(번역) 리액트에서 어댑터(Adapter) 패턴을 사용하는 방법

프론트엔드와 백엔드 사이의 개발은 별도로 진행됩니다. 따라서 데이터 구조가 일치하지 않는 것은 불가피합니다. 바로 이 지점에서 어댑터(Adapter) 패턴이라는 디자인 패턴을 적용할 수 있습니

velog.io

 

어댑터 패턴을 프론트엔드(Frontend)에 적용할 수 있는 여러 가지 예제가 있습니다. 프론트엔드 개발에서 어댑터 패턴은 기존 코드와의 호환성을 유지하거나, 다른 API 또는 라이브러리의 인터페이스를 통일하는 데 유용하게 사용됩니다. 다음은 프론트엔드에서 어댑터 패턴을 활용할 수 있는 대표적인 예입니다.

 

예제 2: 서로 다른 API 간의 호환성 문제 해결

가장 흔한 예 중 하나는 서로 다른 서드파티 API를 통합할 때 발생합니다. 예를 들어, 두 개의 서드파티 API가 동일한 기능(예: 데이터 가져오기)을 제공하지만, 각기 다른 인터페이스를 가지고 있다고 가정해 보겠습니다. 이 경우 어댑터 패턴을 사용해 두 API를 통합된 방식으로 사용할 수 있습니다.

시나리오

  • API 1: REST API를 사용해 데이터를 가져옴.
  • API 2: GraphQL을 사용해 데이터를 가져옴.

기존 인터페이스

// API 1: REST
function fetchDataFromRestApi(endpoint) {
    return fetch(endpoint).then(response => response.json());
}

// API 2: GraphQL
function fetchDataFromGraphQL(query) {
    return fetch('/graphql', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query }),
    }).then(response => response.json());
}

어댑터 패턴 적용

두 API를 통일된 인터페이스로 사용할 수 있도록 어댑터를 만듭니다.

// 어댑터 인터페이스
class DataAdapter {
    fetchData() {
        throw new Error('This method should be overridden.');
    }
}

// REST API 어댑터
class RestApiAdapter extends DataAdapter {
    constructor(endpoint) {
        super();
        this.endpoint = endpoint;
    }

    fetchData() {
        return fetchDataFromRestApi(this.endpoint);
    }
}

// GraphQL API 어댑터
class GraphQLAdapter extends DataAdapter {
    constructor(query) {
        super();
        this.query = query;
    }

    fetchData() {
        return fetchDataFromGraphQL(this.query);
    }
}

// 클라이언트 코드
function displayData(adapter) {
    adapter.fetchData().then(data => {
        console.log('Fetched Data:', data);
    });
}

// 사용 예시
const restAdapter = new RestApiAdapter('/api/data');
const graphqlAdapter = new GraphQLAdapter('{ data { id name } }');

displayData(restAdapter);
displayData(graphqlAdapter);

 

이 예제에서 RestApiAdapter와 GraphQLAdapter는 각각 DataAdapter라는 공통 인터페이스를 구현합니다. 이를 통해 클라이언트 코드(displayData)는 API의 종류에 상관없이 데이터를 가져올 수 있습니다.

 

1. 타겟 인터페이스 (Target Interface)

타겟 인터페이스는 클라이언트 코드가 기대하는 인터페이스입니다. 이 인터페이스를 통해 클라이언트는 특정 기능을 사용할 수 있으며, 이 인터페이스를 통해 여러 구현체를 동일한 방식으로 사용할 수 있습니다.

  • 타겟 인터페이스: DataAdapter 클래스
    • DataAdapter 클래스는 fetchData()라는 메서드를 정의하는 인터페이스로, REST API와 GraphQL API를 동일한 방식으로 사용할 수 있도록 합니다.
    • 클라이언트 코드(displayData 함수)는 이 인터페이스를 통해 데이터를 가져옵니다.

2. 어댑터 (Adapter)

어댑터는 타겟 인터페이스를 구현하고, 내부적으로 어댑티(Adaptee)의 인터페이스를 사용하여 요청을 변환하는 클래스입니다. 어댑터는 타겟 인터페이스와 어댑티 간의 호환성을 제공하여, 클라이언트 코드가 어댑티의 복잡한 인터페이스를 신경 쓰지 않고도 이를 사용할 수 있게 해줍니다.

  • 어댑터: RestApiAdapter 클래스와 GraphQLAdapter 클래스
    • RestApiAdapter는 REST API를 사용하여 데이터를 가져오는 어댑터입니다. 이 클래스는 타겟 인터페이스인 DataAdapter를 구현하고, 내부적으로 fetchDataFromRestApi 함수를 호출합니다.
    • GraphQLAdapter는 GraphQL API를 사용하여 데이터를 가져오는 어댑터입니다. 이 클래스 역시 타겟 인터페이스인 DataAdapter를 구현하고, 내부적으로 fetchDataFromGraphQL 함수를 호출합니다.

3. 어댑티 (Adaptee)

어댑티는 클라이언트가 직접 사용하기에는 인터페이스가 맞지 않지만, 어댑터를 통해 변환될 기존 클래스나 함수입니다. 어댑티는 어댑터에 의해 타겟 인터페이스에 맞게 변환되어 클라이언트에 제공됩니다.

  • 어댑티: fetchDataFromRestApi 함수와 fetchDataFromGraphQL 함수
    • fetchDataFromRestApi는 REST API를 통해 데이터를 가져오는 기존 함수입니다.
    • fetchDataFromGraphQL는 GraphQL API를 통해 데이터를 가져오는 기존 함수입니다.
    • 이 두 함수는 클라이언트 코드에서 직접 사용되기에는 인터페이스가 맞지 않기 때문에, 어댑터를 통해 타겟 인터페이스에 맞게 변환됩니다.

요약

  • 타겟 인터페이스 (DataAdapter): 클라이언트 코드가 기대하는 공통 인터페이스.
  • 어댑터 (RestApiAdapter, GraphQLAdapter): 타겟 인터페이스를 구현하고, 내부적으로 어댑티를 호출하여 요청을 처리하는 클래스.
  • 어댑티 (fetchDataFromRestApi, fetchDataFromGraphQL): 원래의 클래스나 함수로, 어댑터에 의해 타겟 인터페이스에 맞게 변환되는 객체.

이 구조를 통해 클라이언트 코드(displayData)는 어떤 API가 실제로 사용되는지 알 필요 없이, 일관된 인터페이스(fetchData)를 사용하여 데이터를 가져올 수 있습니다.

Comments