[Design Pattern] Structural Pattern

2024. 11. 4. 12:53·ETC
728x90
반응형

디자인 패턴 시리즈의 마지막으로 구조 패턴(Structural Pattern)에 대해 알아보자. 구조 패턴이란 클래스와 객체를 더 큰 구조로 만들 수 있게 구성을 사용하는 패턴이다. 구조 패턴의 종류는 다음과 같다.

  • 프록시 패턴(Proxy Pattern)
  • 어댑터 패턴(Adapter Pattern)
  • 데코레이터 패턴(Decorator Pattern)
  • 파사드 패턴(Facade Pattern)
  • 경량 패턴(Flyweight Pattern)
  • 복합체 패턴(Composite Pattern)
  • 브릿지 패턴(Bridge Pattern)

프록시 패턴(Proxy Pattern)

: 원본 객체를 대리하여 대신 처리하게 함으로써 로직의 흐름을 제어하는 구조 패턴

  • 프록시(Proxy): 대리자
  • 클라이언트가 대상 객체를 직접 사용하는게 아니라 중간에 프록시(대리자)를 거쳐서 쓰는 패턴
  • 대상 객체의 메소드를 직접 실행하는 것이 아닌, 대상 객체에 접근하기 전에 프록시(Proxy) 객체의 메서드를 접근한 후 추가적인 로직을 처리한 뒤 접근하게 된다.
  • 프록시를 사용함으로써 다음과 같은 효과를 기대할 수 있다.
    • 보안(Security) : 프록시는 클라이언트가 작업을 수행할 수 있는 권한이 있는지 확인하고 검사 결과가 긍정적인 경우에만 요청을 대상으로 전달한다.
    • 캐싱(Caching) : 프록시가 내부 캐시를 유지하여 데이터가 캐시에 아직 존재하지 않는 경우에만 대상에서 작업이 실행되도록 한다.
    • 데이터 유효성 검사(Data validation) : 프록시가 입력을 대상으로 전달하기 전에 유효성을 검사한다.
    • 지연 초기화(Lazy initialization) : 대상의 생성 비용이 비싸다면 프록시는 그것을 필요로 할때까지 연기할 수 있다.
    • 로깅(Logging) : 프록시는 메소드 호출과 상대 매개 변수를 인터셉트하고 이를 기록한다.
    • 원격 객체(Remote objects) : 프록시는 원격 위치에 있는 객체를 가져와서 로컬처럼 보이게 할 수 있다.

프록시 패턴 구조

  • Subject: Proxy와 RealSubject를 하나로 묶는 인터페이스
    • 대상 객체와 프록시 역할을 동일하게 하는 추상 메소드 operation()를 정의한다.
    • 인터페이스가 있기 때문에 클라이언트는 Proxy 역할과 RealSubject 역할의 차이를 의식할 필요가 없다.
  • RealSubject: 원본 대상 객체
  • Proxy: 대상 객체(RealSubject)를 중계할 대리자 역할
    • 프록시는 대상 객체를 합성(composition)한다
    • 프록시는 대상 객체와 같은 이름의 메서드를 호출하며, 별도의 로직을 수행 할수 있다 (인터페이스 구현 메소드)
    • 프록시는 흐름제어만 할 뿐 결과값을 조작하거나 변경시키면 안 된다

프록시 패턴 종류

기본 프록시

interface ISubject {
    void action();
}

class RealSubject implements ISubject {
    public void action() {
        System.out.println("원본 객체 액션 !!");
    }
}
class Proxy implements ISubject {
    private RealSubject subject; // 대상 객체를 composition

    Proxy(RealSubject subject) {
        this.subject = subject;
    }

    public void action() {
        subject.action(); // 위임
        /* do something */
        System.out.println("프록시 객체 액션 !!");
    }
}

class Client {
    public static void main(String[] args) {
        ISubject sub = new Proxy(new RealSubject());
        sub.action();
    }
}

 

가상 프록시(Virutal Proxy)

  • 지연 초기화 방식
  • 가끔 필요하지만 항상 메모리에 적재되어 있는 무거운 서비스 객체가 있는 경우
  • 이 구현은 실제 객체의 생성에 많은 자원이 소모 되지만 사용 빈도는 낮을 때 쓰는 방식이다.
  • 서비스가 시작될 때 객체를 생성하는 대신에 객체 초기화가 실제로 필요한 시점에 초기화될수 있도록 지연할 수 있다.
class Proxy implements ISubject {
    private RealSubject subject; // 대상 객체를 composition

    Proxy() {
    }

    public void action() {
        // 프록시 객체는 실제 요청(action(메소드 호출)이 들어 왔을 때 실제 객체를 생성한다.
        if(subject == null){
            subject = new RealSubject();
        }
        subject.action(); // 위임
        /* do something */
        System.out.println("프록시 객체 액션 !!");
    }
}

class Client {
    public static void main(String[] args) {
        ISubject sub = new Proxy();
        sub.action();
    }
}

 

보호 프록시(Protection Proxy)

  • 프록시가 대상 객체에 대한 자원으로의 엑세스 제어(접근 권한)
  • 특정 클라이언트만 서비스 객체를 사용할 수 있도록 하는 경우
  • 프록시 객체를 통해 클라이언트의 자격 증명이 기준과 일치하는 경우에만 서비스 객체에 요청을 전달할 수 있게 한다.
class Proxy implements ISubject {
    private RealSubject subject; // 대상 객체를 composition
    boolean access; // 접근 권한

    Proxy(RealSubject subject, boolean access) {
        this.subject = subject;
        this.access = access;
    }

    public void action() {
        if(access) {
            subject.action(); // 위임
            /* do something */
            System.out.println("프록시 객체 액션 !!");
        }
    }
}

class Client {
    public static void main(String[] args) {
        ISubject sub = new Proxy(new RealSubject(), false);
        sub.action();
    }
}

 

로깅 프록시(Logging Proxy

  • 대상 객체에 대한 로깅을 추가하려는 경우
  • 프록시는 서비스 메서드를 실행하기 전달하기 전에 로깅을 하는 기능을 추가하여 재정의한다.
class Proxy implements ISubject {
    private RealSubject subject; // 대상 객체를 composition

    Proxy(RealSubject subject {
        this.subject = subject;
    }

    public void action() {
        System.out.println("로깅..................");

        subject.action(); // 위임
        /* do something */
        System.out.println("프록시 객체 액션 !!");

        System.out.println("로깅..................");
    }
}

class Client {
    public static void main(String[] args) {
        ISubject sub = new Proxy(new RealSubject());
        sub.action();
    }
}

 

원격 프록시(Remote Proxy)

  • 프록시 클래스는 로컬에 있고, 대상 객체는 원격 서버에 존재하는 경우
  • 프록시 객체는 네트워크를 통해 클라이언트의 요청을 전달하여 네트워크와 관련된 불필요한 작업들을 처리하고 결과값만 반환
  • 클라이언트 입장에선 프록시를 통해 객체를 이용하는 것이니 원격이든 로컬이든 신경 쓸 필요가 없으며, 프록시는 진짜 객체와 통신을 대리하게 된다.
  • 프록시를 스터브(Stub)라고도 부르며, 프록시로부터 전달된 명령을 이해하고 적합한 메서드를 호출해주는 역할을 하는 보조객체를 스켈레톤이라 한다.

캐싱 프록시(Caching Proxy)

  • 데이터가 큰 경우 캐싱하여 재사용을 유도
  • 클라이언트 요청의 결과를 캐시하고 이 캐시의 수명 주기를 관리
  • 웹 페이지를 캐싱해두는 HTTP Proxy가 대표적인 예시이다.

사용 시기

  • 접근 제어 혹은 부가 기능을 추가하고 싶은데, 기존의 특정 객체를 수정할 수 없는 상황일 때
  • 초기화 지연, 접근 제어, 로깅, 캐싱 등의 부가 기능을 기존 객체 동작에 수정 없이 가미하고 싶을 때

장점

  • 개방 폐쇄 원칙(OCP) 준수
    • 기존 대상 객체의 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
  • 단일 책임 원칙(SRP) 준수
    • 대상 객체는 자신의 기능에만 집중 하고, 그 이외 부가 기능을 제공하는 역할을 프록시 객체에 위임하여 다중 책임을 회피 할 수 있다.
  • 원래 하려던 기능을 수행하며 그외의 부가적인 작업(로깅, 인증, 네트워크 통신 등)을 수행하는데 유용하다
  • 클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리할 수 있다.
  • 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성에 문제 되지 않는다.

단점

  • 많은 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가한다.
    • 예를들어 여러 클래스에 로깅 기능을 가미 시키고 싶다면, 동일한 코드를 적용함에도 각각의 클래스에 해당되는 프록시 클래스를 만들어서 적용해야 되기 때문에 코드량이 많아지고 중복이 발생 된다.
    • 자바에서는 리플렉션에서 제공하는 동적 프록시(Dynamic Proxy) 기법을 이용해서 해결할 수 있다. (후술)
  • 프록시 클래스 자체에 들어가는 자원이 많다면 서비스로부터의 응답이 늦어질 수 있다

구현

이미지 뷰어 프로그램 예제

 

Subject

// 대상 객체와 프록시 객체를 묶는 인터페이스
interface IImage {
    void showImage(); // 이미지를 렌더링하기 위해 구현체가 구현해야 하는 메서드
}

// 대상 객체 (RealSubject)
class HighResolutionImage implements IImage {
    String img;

    HighResolutionImage(String path) {
        loadImage(path);
    }

    private void loadImage(String path) {
        // 이미지를 디스크에서 불러와 메모리에 적재 (작업 자체가 무겁고 많은 자원을 필요로함)
        try {
            Thread.sleep(1000);
            img = path;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s에 있는 이미지 로딩 완료\n", path);
    }

    @Override
    public void showImage() {
        // 이미지를 화면에 렌더링
        System.out.printf("%s 이미지 출력\n", img);
    }
}

Proxy

// 프록시 객체 (Proxy)
class ImageProxy implements IImage {
    private IImage proxyImage;
    private String path;

    ImageProxy(String path) {
        this.path = path;
    }

    @Override
    public void showImage() {
        // 고해상도 이미지 로딩하기
        proxyImage = new HighResolutionImage(path);
        proxyImage.showImage();
    }
}

Client

class ImageViewer {
    public static void main(String[] args) {
        IImage highResolutionImage1 = new ImageProxy("./img/고해상도이미지_1");
        IImage highResolutionImage2 = new ImageProxy("./img/고해상도이미지_2");
        IImage highResolutionImage3 = new ImageProxy("./img/고해상도이미지_3");

        highResolutionImage2.showImage();
    }
}

 

프록시 객체 내에서 경로 데이터를 지니고 있다가 사용자가 showImage를 호출하면 그때서야 대상 객체를 로드(lazyload)하여, 이미지를 메모리에 적재하고 대상 객체의 showIMage() 메서드를 위임 호출함으로써, 실제 메소드를 호출하는 시점에 메모리 적재가 이루어지기 때문에 불필요한 자원낭비가 발생하지 않는다.

 

이러한 Proxy 패턴은 스프링 AOP, JPA 등에서 정말 많이 사용된다.

Dynamic Proxy

자바 JDK에서는 별도로 프록시 객체 구현 기능을 지원한다. 이를 동적 프록시(Dynamic Proxy) 기법이라고 불리운다.
동적 프록시는 개발자가 직접 일일히 프록시 객체를 생성하는 것이 아닌, 애플리케이션 실행 도중 java.lang.reflect.Proxy 패키지에서 제공해주는 API를 이용하여 동적으로 프록시 인스턴스를 만들어 등록하는 방법으로서, 자바의 Reflection API 기법을 응용한 연장선의 개념이다.

 

별도의 프록시 클래스 정의없이 런타임으로 프록시 객체를 동적으로 생성해 이용할 수 있다는 장점이 있다.

 

Subject

// 대상 객체와 프록시를 묶는 인터페이스
interface Animal {
    void eat();
}

// 프록시를 적용할 타겟 객체
class Tiger implements Animal{
    @Override
    public void eat() {
        System.out.println("호랑이가 음식을 먹습니다.");
    }
}

 

Proxy & Client

public class Client {
    public static void main(String[] arguments) {

        // newProxyInstance() 메서드로 동적으로 프록시 객체를 생성할 수 있다.
        Animal tigerProxy = (Animal) Proxy.newProxyInstance(
                Animal.class.getClassLoader(), // 대상 객체의 인터페이스의 클래스로더
                new Class[]{Animal.class}, // 대상 객체의 인터페이스
                new InvocationHandler() { // 프록시 핸들러
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object target = new Tiger();

                        System.out.println("----eat 메서드 호출 전----");

                        Object result = method.invoke(target, args); // 타겟 메서드 호출

                        System.out.println("----eat 메서드 호출 후----");

                        return result;
                    }
                }
        );

        tigerProxy.eat();
    }
}

어댑터 패턴(Adapter Pattern)

: 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들을 함께 작동해주도록 변환 역할을 해주는 패턴이다.

 

예를 들어, 기존에 있는 시스템에 새로운 써드파티 라이브러리를 추가하고 싶거나, Legacy 인터페이스를 새로운 인터페이스로 교체하는 경우에 어댑터 패턴을 사용하면 코드의 재사용성을 높일 수 있다.

 

즉, 어댑터란 이미 구축되어 있는 것을 새로운 어떤것에 사용할때 양 쪽 간의 호환성을 유지해 주기 위해 사용하는 것으로서, 기존 시스템에서 새로운 업체에서 제공하는 기능을 사용하려고 할때 서로 간의 인터페이스를 어댑터로 일치시켜줌으로써 호환성 및 신규 기능 확장을 할수 있다고 보면 된다

어댑터가 Legacy 인터페이스를 감싸서 새로운 인터페이스로 변환하기 때문에 Wrapper 패턴이라고도 불리운다.

어댑터 패턴 구조

어댑터 패턴은 상속을 사용하느냐, 합성을 사용하느냐에 따라 두 가지 패턴 방법으로 나뉜다.

 

객체 어댑터(Object Adaptor)

  • 합성(Composition)된 맴버에게 위임을 이용한 어댑터 패턴 (추천 🌟)
  • 자기가 해야 할 일을 클래스 맴버 객체의 메소드에게 다시 시킴으로써 목적을 달성하는 것을 위임이라고 한다.
  • 합성을 활용했기 때문에 런타임 중에 Adaptee(Service)가 결정되어 유연하다.
  • 그러나 Adaptee(Service) 객체를 필드 변수로 저장해야 되기 때문에 공간 차지 비용이 든다

  • Adaptee(Service): 어댑터 대상 객체. 기존 시스템 / 외부 시스템 / 써드파티 라이브러리
  • Target(Client Interface): Adapter가 구현하는 인터페이스
  • Adapter: Client 와 Adaptee(Service) 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당.
    • Object Adaptor 방식에선 합성을 이용해 구현
    • Adaptee를 따로 클래스 멤버로 설정하고 위임을 통해 동작을 매치시킨다.
// Adaptee : 클라이언트에서 사용하고 싶은 기존의 서비스 (하지만 호환이 안되서 바로 사용 불가능)
class Service {

    void specificMethod(int specialData) {
        System.out.println("기존 서비스 기능 호출 + " + specialData);
    }
}

// Client Interface : 클라이언트가 접근해서 사용할 고수준의 어댑터 모듈
interface Target {
    void method(int data);
}

// Adapter : Adaptee 서비스를 클라이언트에서 사용하게 할 수 있도록 호환 처리 해주는 어댑터
class Adapter implements Target {
    Service adaptee; // composition으로 Service 객체를 클래스 필드로

    // 어댑터가 인스턴스화되면 호환시킬 기존 서비스를 설정
    Adapter(Service adaptee) {
        this.adaptee = adaptee;
    }

    // 어댑터의 메소드가 호출되면, Adaptee의 메소드를 호출하도록
    public void method(int data) {
        adaptee.specificMethod(data); // 위임
    }
}
class Client {
    public static void main(String[] args) {
        // 1. 어댑터 생성 (기존 서비스를 인자로 받아 호환 작업 처리)
        Target adapter = new Adapter(new Service());

        // 2. Client Interfac의 스펙에 따라 메소드를 실행하면 기존 서비스의 메소드가 실행된다.
        adapter.method(1);
    }
}

 

클래스 어댑터(Class Adapter)

  • 클래스 상속을 이용한 어댑터 패턴
  • Adaptee(Service)를 상속했기 때문에 따로 객체 구현없이 바로 코드 재사용이 가능하다.
  • 상속은 대표적으로 기존에 구현된 코드를 재사용하는 방식이지만, 자바에서는 다중 상속 불가 문제 때문에 전반적으로 권장하지는 않는 방법이다.

  • Adaptee(Service) : 어댑터 대상 객체. 기존 시스템 / 외부 시스템 / 써드파티 라이브러리
  • Target(Cient Interface) : Adapter가 구현하는 인터페이스
  • Adapter : Client 와 Adaptee(Service) 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당.
    • Class Adaptor 방식에선 상속을 이용해 구성한다.
    • Existing Class와 Adaptee(Service) 를 동시에 implements, extends 하여 구현한다.
// Adaptee : 클라이언트에서 사용하고 싶은 기존의 서비스 (하지만 호환이 안되서 바로 사용 불가능)
class Service {

    void specificMethod(int specialData) {
        System.out.println("기존 서비스 기능 호출 + " + specialData);
    }
}

// Client Interface : 클라이언트가 접근해서 사용할 고수준의 어댑터 모듈
interface Target {
    void method(int data);
}

// Adapter : Adaptee 서비스를 클라이언트에서 사용하게 할 수 있도록 호환 처리 해주는 어댑터
class Adapter extends Service implements Target {

    // 어댑터의 메소드가 호출되면, 부모 클래스 Adaptee의 메소드를 호출
    public void method(int data) {
        specificMethod(data);
    }
}
class Client {
    public static void main(String[] args) {
        // 1. 어댑터 생성
        Target adapter = new Adapter();

        // 2. 인터페이스의 스펙에 따라 메소드를 실행하면 기존 서비스의 메소드가 실행된다.
        adapter.method(1);
    }
}

사용 시기

  • 레거시 코드를 사용하고 싶지만 새로운 인터페이스가 레거시 코드와 호환되지 않을 때
  • 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때
  • 이미 만들어진 클래스를 새로운 인터페이스(API)에 맞게 개조할때
  • 소프트웨어의 구 버전과 신 버전을 공존시키고 싶을때

장점

  • 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있다 -> 단일 책임 원칙(SRP)
  • 기존 클래스 코드를 건들지 않고 클라이언트 인터페이스를 통해 어댑터와 작동 -> 개방 폐쇄 원칙(OCP)
  • 만일 추가로 필요한 메소드가 있다면 어댑터에 빠르게 만들 수 있다. 만약 버그가 발생해도 기존의 클래스에는 버그가 없으므로 Adapter 역할의 클래스를 중점적으로 조사하면 되고, 프로그램 검사도 쉬워진다.

단점

  • 코드의 복잡성이 증가하기에 트레이드 오프를 잘 생각하고 신중히 선택하자

구현

프로그램 엔진 교체하고 호환시키는 예제

 

Adaptee

// 기존의 인터페이스는 손대지 않는다
interface ISortEngine {
    void setList(); // 정렬할 리스트
    void sort(); // 정렬 알고리즘
    void reverseSort(); // 역순 정렬 알고리즘
    void printSortListPretty(); // 정렬된 리스트를 예쁘게 출력
}

class A_SortEngine implements ISortEngine {
    public void setList() {}
    public void sort() {}
    public void reverseSort() {}
    public void printSortListPretty() {}
}

class B_SortEngine {
    public void setList() {} // 정렬할 리스트
    public void sorting(boolean isReverse) {} // 정렬 / 역순 정렬 알고리즘 (파라미터로 순서 결정)
}

 

Adaptor

// 객체 어댑터를 구성한다.
class SortEngineAdaptor implements ISortEngine {
    // (두 엔진을 composition 하여 이용)
    A_SortEngine Aengine;
    B_SortEngine Bengine;

    SortEngineAdaptor(A_SortEngine Aengine, B_SortEngine Bengine) {
        this.Aengine = Aengine;
        this.Bengine = Bengine;
    }

    @Override
    public void setList() {
        Bengine.setList();
    }

    @Override
    public void sort() {
        Bengine.sorting(false); // 메서드 시그니처가 달라고 위임을 통해 호환 작업
    }

    @Override
    public void reverseSort() {
        Bengine.sorting(true); // 메서드 시그니처가 달라고 위임을 통해 호환 작업
    }

    @Override
    public void printSortListPretty() {
        Aengine.printSortListPretty(); // B 엔진에 없는 기능을 A 엔진으로 실행
    }
}

 

Client

// Client 역할을 하는 클래스 : Sort 엔진 객체를 받아 실행
class SortingMachine {
    ISortEngine engine;

    void setEngine(ISortEngine engine) { this.engine = engine; }

    void sortingRun() {
        engine.setList();

        engine.sort();
        engine.printSortListPretty();

        engine.reverseSort();
        engine.printSortListPretty();
    }

    public static void main(String[] args) {
        // 클라이언트의 머신에 원본 엔진 대신 어댑터를 할당한다.
        ISortEngine adaptor = new SortEngineAdaptor(new A_SortEngine(), new B_SortEngine());
        SortingMachine machine = new SortingMachine();
        machine.setEngine(adaptor);

        machine.sortingRun();
    }
}

 

기존 엔진이었던 A 코드의 코드는 건들지 않은채, B 엔진으로 손쉽게 교체할 수 있고, 여전히 A와의 호환성도 유지되는 것을 볼 수 있다.

데코레이터 패턴(Decorator Pattern)

: 대상 객체에 대한 기능 확장이나 변경이 필요할때 객체의 결합을 통해 서브클래싱 대신 쓸수 있는 유연한 대안 구조 패턴

 

데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에서 동적으로 생성할 수 있다. 데코레이터할 대상 객체를 새로운 행동들을 포함한 특수 장식자 객체에 넣어서 행동들을 해당 장식자 객체마다 연결시켜, 서브클래스로 구성할때 보다 훨씬 유연하게 기능을 확장 할 수 있다. 그리고 기능을 구현하는 클래스들을 분리함으로써 수정이 용이해진다.

데코레이터 패턴 구조

  • Component: 원본 객체와 장식된 객체 모두를 묶는 역할을 하는 인터페이스
  • ConcreteComponent: 원본 객체(데코레이팅 할 객체)
  • Decorator: 장식자 추상 클래스
    • Component 인터페이스를 구현한다.
    • 원본 객체를 합성(composition)한 wrappee 필드와 인터페이스의 구현 메서드를 가지고 있다.
  • ConcreteDecorator: 구체적인 장식자 클래스
    • 부모 클래스가 감사고 있는 하나의 Component를 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.

Component

// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface IComponent {
    void operation();
}

// 장식될 원본 객체
class ConcreteComponent implements IComponent {
    public void operation() {
    }
}

Decorator

// 장식자 추상 클래스
abstract class Decorator implements IComponent {
    IComponent wrappee; // 원본 객체를 composition

    Decorator(IComponent component) {
        this.wrappee = component;
    }

    public void operation() {
        wrappee.operation(); // 위임
    }
}

// 장식자 클래스
class ComponentDecorator1 extends Decorator {

    ComponentDecorator1(IComponent component) {
        super(component);
    }

    public void operation() {
        super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
        extraOperation(); // 장식 클래스만의 메소드를 실행한다.
    }

    void extraOperation() {
    }
}

class ComponentDecorator2 extends Decorator {

    ComponentDecorator2(IComponent component) {
        super(component);
    }

    public void operation() {
        super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
        extraOperation(); // 장식 클래스만의 메소드를 실행한다.
    }

    void extraOperation() {
    }
}

Client

public class Client {
    public static void main(String[] args) {
        // 1. 원본 객체 생성
        IComponent obj = new ConcreteComponent();

        // 2. 장식 1 하기
        IComponent deco1 = new ComponentDecorator1(obj);
        deco1.operation(); // 장식된 객체의 장식된 기능 실행

        // 3. 장식 2 하기
        IComponent deco2 = new ComponentDecorator2(obj);
        deco2.operation(); // 장식된 객체의 장식된 기능 실행

        // 4. 장식 1 + 2 하기
        IComponent deco3 = new ComponentDecorator1(new ComponentDecorator2(obj));
    }
}

사용 시기

  • 객체 책임과 행동이 동적으로 상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
  • 객체의 결합을 통해 기능이 생성될 수 있는 경우
  • 객체를 사용하는 코드를 손상시키지 않고 런타임에 객체에 추가 동작을 할당할 수 있어야 하는 경우
  • 상속을 통해 서브클래싱으로 객체의 동작을 확장하는 것이 어색하거나 불가능 할 때

장점

  • 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장 가능
  • 객체를 여러 데코레이터로 래핑하여 여러 동작 결합 가능
  • 런타임에 동적으로 기능 변경 가능
  • 각 장식자 클래스마다 고유의 책임 -> 단일 책임 원칙(SRP)
  • 클라이언트 코드 수정없이 기능 확장이 필요하면 장식자 클래스를 추가 -> 개방 폐쇄 원칙(OCP)
  • 구현체가 아닌 인터페이스를 바라봄 -> 의존 역전 원칙(DIP)

단점

  • 만일 장식자 일부를 제거하고 싶다면, Wrapper 스택에서 특정 wrapper를 제거하는 것은 어렵다.
  • 데코레이터를 조합하는 초기 생성코드가 보기 안좋을 수 있다. new A(new B(new C(new D())))
  • 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정되는데, 만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어렵다.

구현

총에 악세서리를 부착하는 예제

 

Component

// Component: 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface Weapon {
    void aim_and_fire();
}

// ConcreteComponent: 장식될 원본 객체
class BaseWeapon implements Weapon {

    @Override
    public void aim_and_fire() {
        System.out.println("총알 발사");
    }
}

Decorator

// 장식자 추상 클래스
abstract class WeaponAccessory implements Weapon {
    private Weapon rifle;

    WeaponAccessory(Weapon rifle) { this.rifle = rifle; }

    @Override
    public void aim_and_fire() {
        rifle.aim_and_fire(); // 위임
    }
}

// 장식자 클래스 (유탄발사기)
class Generade extends WeaponAccessory {

    Generade(Weapon rifle) { super(rifle); }

    @Override
    public void aim_and_fire() {
        super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
        generade_fire();
    }

    public void generade_fire() {
        System.out.println("유탄 발사");
    }
}

// 장식자 클래스 (조준경)
class Scoped extends WeaponAccessory {

    Scoped(Weapon rifle) { super(rifle); }

    @Override
    public void aim_and_fire() {
        aiming();
        super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
    }

    public void aiming() {
        System.out.println("조준 중..");
    }
}

// 장식자 클래스 (개머리판)
class Buttstock extends WeaponAccessory {

    Buttstock(Weapon rifle) { super(rifle); }

    @Override
    public void aim_and_fire() {
        holding();
        super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
    }

    public void holding() {
        System.out.println("견착 완료");
    }
}

Client

public class Client {
    public static void main(String[] args) {

        // 1. 유탄발사기가 달린 총
        Weapon generade_rifle = new Generade(new BaseWeapon());
        generade_rifle.aim_and_fire();

        // 2. 개머리판을 장착하고 스코프를 달은 총
        Weapon buttstock_scoped_rifle = new Buttstock(new Scoped(new BaseWeapon()));
        buttstock_scoped_rifle.aim_and_fire();

        // 3. 유탄발사기 + 개머리판 + 스코프가 달린 총
        Weapon buttstock_scoped_generade_rifle = new Buttstock(new Scoped(new Generade(new BaseWeapon())));
        buttstock_scoped_generade_rifle.aim_and_fire();
    }
}

 

주의할 점은, 어느 장식자를 먼저 감싸느냐에 따라 그에 대한 행동 패턴이 완전히 달라지게 된다!

파사드 패턴(Facade Pattern)

: 사용하기 복잡한 클래스 라이브러리에 대해 사용하기 편하게 간편한 인터페이스(API)를 구성하기 위한 구조 패턴

 

예를들어 라이브러리의 각 클래스와 메서드들이 어떤 목적의 동작인지 이해하기 어려워 바로 가져다 쓰기에는 난이도가 높을때, 이에 대한 적절한 네이밍과 정리를 통해 사용자로 하여금 쉽게 라이브러리를 다룰수 있도록 인터페이스를 만드는데, 우리가 교제를 보고 필기노트에 재정리를 하듯이 클래스를 재정리하는 행위로 보면 된다.

 

이처럼 파사드(Facade) 패턴은 복잡하게 얽혀 있는 것을 정리해서 사용하기 편한 인터페이스를 사용자에게 제공하는 패턴이다. 사용자는 복잡한 시스템을 알 필요없이 시스템의 외부에 대해서 단순한 인터페이스를 이용하기만 하면 된다.

파사드 패턴 구조

  • Facade: 서브 시스템 기능ㅇ을 편리하게 사용할 수 있도록 하기 위해 여러 시스템과 상호작용하는 복잡한 로직을 재정리해서 높은 레벨의 인터페이스를 구성한다.
    • 서비 시스템에 대한 창구 역할
    • 클라이언트와 서브 시스템 간의 연결을 느슨하게 한다.
  • Additional Facade: 파사드 클래스는 반드시 한개만 존재해야 한다는 규칙같은건 없다. 연관 되지 않은 기능이 있다면 얼마든지 파사드 2세로 분리한다. 이 퍼사드 2세는 다른 파사드에서 사용할 수도 있고 클라이언트에서 직접 접근할 수도 있다.
  • SubSystem: 수십 가지 라이브러리 혹은 클래스들

파사드 패턴은 다른 디자인 패턴과 다르게 클래스 구조가 정형화되지 않은 패턴이다. 그냥 파사드 클래스를 만들어 적절히 기능 집약화만 해주면 그게 디자인 패턴이 되는 것이다.

사용 시기

  • 시스템이 너무 복잡할때
  • 그래서 간단한 인터페이스를 통해 복잡한 시스템을 접근하도록 하고 싶을때
  • 시스템을 사용하고 있는 외부와 결합도가 너무 높을 때 의존성 낮추기 위할때

장점

  • 하위 시스템의 복잡성에서 코드를 분리하여, 외부에서 시스템을 사용하기 쉬워진다.
  • 하위 시스템 간의 의존 관계가 많을 경우 이를 감소시키고 의존성을 한 곳으로 모을 수 있다.
  • 복잡한 코드를 감춤으로써, 클라이언트가 시스템의 코드를 모르더라도 Facade 클래스만 이해하고 사용 가능하다.

단점

  • 파사드가 앱의 모든 클래스에 결합된 God 객체가 될 수 있다
  • 파사드 클래스 자체가 서브시스템에 대한 의존성을 가지게 되어 의존성을 완전히는 피할 수는 없다.
  • 어찌되었건 추가적인 코드가 늘어나는 것이기 때문에 유지보수 측면에서 공수가 더 많이 들게 된다.
  • 따라서 추상화 하고자하는 시스템이 얼마나 복잡한지 퍼사드 패턴을 통해서 얻게 되는 이점과 추가적인 유지보수 비용을 비교해보며 결정하여야 한다.

구현

복잡한 DBMS 시스템을 간편하게 재구성하는 예제

 

데이터베이스를 조회해서 데이터를 가공하기 까지 다음과 같은 규칙이 존재한다고 하자.

  • dbms를 바로 조회하기전에
  • 과거에 조회된 데이터인지를 캐시에서 먼저 조사를 하고
  • 캐시에 데이터가 있다면 이 캐시에 데이터를 가공하고 출력
  • 캐시에 데이터가 없다면 DBMS를 통해서 조회를 하고
  • 조회된 데이터를 가공하고 출력함과 동시에 캐시에 저장한다.

Facade

class Facade {
    private DBMS dbms = new DBMS();
    private Cache cache = new Cache();

    public void insert() {
        dbms.put("홍길동", new Row("홍길동", "1890-02-14", "honggildong@naver.com"));
        dbms.put("임꺽정", new Row("임꺽정", "1820-11-02", "imgguckjong@naver.com"));
        dbms.put("주몽", new Row("주몽", "710-08-27", "jumong@naver.com"));
    }

    public void run(String name) {
        Row row = cache.get(name);

        // 1. 만약 캐시에 없다면
        if (row == null){
            row = dbms.query(name); // DB에 해당 데이터를 조회해서 row에 저장하고
            if(row != null) {
                cache.put(row); // 캐시에 저장
            }
        }

        // 2. dbms.query(name)에서 조회된 값이 있으면
        if(row != null) {
            Message message = new Message(row);

            System.out.println(message.makeName());
            System.out.println(message.makeBirthday());
            System.out.println(message.makeEmail());
        }
        // 3. 조회된 값이 없으면
        else {
            System.out.println(name + " 가 데이터베이스에 존재하지 않습니다.");
        }
    }
}

Client

class Client {
    public static void main(String[] args) {

        // 1. 퍼사드 객체 생성
        Facade facade = new Facade();

        // 2. db 값 insert
        facade.insert();

        // 3. 퍼사드로 데이터베이스 & 캐싱 & 메세징 로직을 한번에 조회
        String name = "홍길동";
        facade.run(name);
    }
}

경량 패턴(Flyweight Pattern)

: 재사용 가능한 객체 인스턴스를 공유시켜 메모리 사용량을 최소화하는 구조 패턴

 

간단히 말하면 캐시(Cache) 개념을 코드로 패턴화한 것으로, 자주 변화는 속성(extrinsit)과 변하지 않는 속성(intrinsit)을 분리하고 변하지 않는 속성을 캐시하여 재사용해 메모리 사용을 줄이는 방식

경량 패턴 구조

  • Flyweight: 경량 객체를 묶는 인터페이스
  • ConcreteFlyweight: 공유 가능하여 재사용되는 객체 (intrinsic state)
  • UnsahredConcreteFlyweight: 공유 불가능한 객체 (extrinsic state)
  • FlyweightFactory: 경량 객체를 만드는 공장 역할과 캐시 역할을 겸비하는 Flyweight 객체 관리 클래스
    • GetFlyweight() 메서드는 팩토리 메서드 역할을 한다고 보면 된다.
    • 만일 객체가 메모리에 존재하면 그대로 가져와 반환하고, 없다면 새로 생성해 반환한다

사용 시기

  • 어플리케이션에 의해 생성되는 객체의 수가 많아 저장 비용이 높아질 때
  • 생성된 객체가 오래도록 메모리에 상주하며 사용되는 횟수가 많을때
  • 공통적인 인스턴스를 많이 생성하는 로직이 포함된 경우
  • 임베디드와 같이 메모리를 최소한으로 사용해야하는 경우에 활용

장점

  • 애플리케이션에서 사용하는 메모리 감소
  • 프로그램 속도를 개선
    • new로 인스턴스화를 하면 데이터가 생성되고 메모리에 적재 되는 미량의 시간이 걸리게 된다.
    • 객체를 공유하면 인스턴스를 가져오기만 하면 되기 때문에 메모리 뿐만 아니라 속도도 향상시킬 수 있게 되는 것이다.

단점

  • 코드 복잡도 증가

구현

마인크래프트 나무 심기 예제

 

나무 객체에 필요한 데이터

  • 나무 종류
  • 메시 폴리곤 (mesh) -> 크기가 크고 변하지 않는 데이터 => intrinsic state
  • 나무껍질 텍스쳐 (texture) -> 크기가 크고 변하지 않는 데이터 => intrinsic state
  • 잎사귀 텍스쳐 (texture) -> 크기가 크고 변하지 않는 데이터 => intrinsic state
  • 위치 매개변수

ConcreteFlyweight

// ConcreteFlyweight - 플라이웨이트 객체는 불변성을 가져야한다. 변경되면 모든 것에 영향을 주기 때문이다.
final class TreeModel {
    // 메시, 텍스쳐 총 사이즈
    long objSize = 90; // 90MB

    String type; // 나무 종류
    Object mesh; // 메쉬
    Object texture; // 나무 껍질 + 잎사귀 텍스쳐

    public TreeModel(String type, Object mesh, Object texture) {
        this.type = type;
        this.mesh = mesh;
        this.texture = texture;

        // 나무 객체를 생성하여 메모리에 적재했으니 메모리 사용 크기 증가
        Memory.size += this.objSize;
    }
}

UnsharedConcreteFlyweight

// UnsahredConcreteFlyweight
class Tree {
    // 죄표값과 나무 모델 참조 객체 크기를 합친 사이즈
    long objSize = 10; // 10MB

    // 위치 변수
    double position_x;
    double position_y;

    // 나무 모델
    TreeModel model;

    public Tree(TreeModel model, double position_x, double position_y) {
        this.model = model;
        this.position_x = position_x;
        this.position_y = position_y;

        // 나무 객체를 생성하였으니 메모리 사용 크기 증가
        Memory.size +=  this.objSize;
    }
}

FlyweightFactory

// FlyweightFactory
class TreeModelFactory {
    // Flyweight Pool - TreeModel 객체들을 Map으로 등록하여 캐싱
    private static final Map<String, TreeModel> cache = new HashMap<>(); // static final 이라 Thread-Safe 함

    // static factory method
    public static TreeModel getInstance(String key) {
        // 만약 캐시 되어 있다면
        if(cache.containsKey(key)) {
            return cache.get(key); // 그대로 가져와 반환
        } else {
            // 캐시 되어있지 않으면 나무 모델 객체를 새로 생성하고 반환
            TreeModel model = new TreeModel(
                    key,
                    new Object(),
                    new Object()
            );
            System.out.println("-- 나무 모델 객체 새로 생성 완료 --");

            // 캐시에 적재
            cache.put(key, model);

            return model;
        }
    }
}

Client

// Client
class Terrain {
    // 지형 타일 크기
    static final int CANVAS_SIZE = 10000;

    // 나무를 렌더릴
    public void render(String type, double position_x, double position_y) {
        // 1. 캐시 되어 있는 나무 모델 객체 가져오기
        TreeModel model = TreeModelFactory.getInstance(type);

        // 2. 재사용한 나무 모델 객체와 변화하는 속성인 좌표값으로 나무 생성
        Tree tree = new Tree(model, position_x, position_y);

        System.out.println("x:" + tree.position_x + " y:" + tree.position_y + " 위치에 " + type + " 나무 생성 완료");
    }
}

public static void main(String[] args) {
    // 지형 생성
    Terrain terrain = new Terrain();

    // 지형에 Oak 나무 5 그루 생성
    for (int i = 0; i < 5; i++) {
        terrain.render(
                "Oak", // type
                Math.random() * Terrain.CANVAS_SIZE, // position_x
                Math.random() * Terrain.CANVAS_SIZE // position_y
        );
    }

    // 지형에 Acacia 나무 5 그루 생성
    for (int i = 0; i < 5; i++) {
        terrain.render(
                "Acacia", // type
                Math.random() * Terrain.CANVAS_SIZE, // position_x
                Math.random() * Terrain.CANVAS_SIZE // position_y
        );
    }

    // 지형에 Jungle 나무 5 그루 생성
    for (int i = 0; i < 5; i++) {
        terrain.render(
                "Jungle", // type
                Math.random() * Terrain.CANVAS_SIZE, // position_x
                Math.random() * Terrain.CANVAS_SIZE // position_y
        );
    }

    // 총 메모리 사용률 출력
    Memory.print();
}

매번 나무를 생성할 때마다, 모든 데이터를 새롭게 적재하는 것보다 위처럼 경량 패턴을 통해 변하지 않는 데이터는 공유시켜 메모리 사용량을 줄일 수 있다!

Garbage Collection(GC) 주의사항

위에서 구현한 TreeModelFactory에서는 HashMap을 이용해 TreeModel의 인스턴스를 캐싱하여 관리하고 있다. 이와 같이 '인스턴스를 관리' 하는 기능을 자바 프로그래밍에서 구현하여 사용할 때에는 반드시 '관리되고 있는 인스턴스는 GC(Garbage Collection) 처리되지 않는다' 라는 점을 주의해야 한다.

 

즉, 나무를 모두 렌더링을 완료하여 더 이상 나무를 생성할 일이 없다라면, 반드시 TreeModelFactory에 잔존해있는 Flyweight Pool 을 비워줄 필요가 있다. 그래야 인스턴스에 대한 참조를 잃은 TreeModel 인스턴스들이 GC에 의해 메모리 청소가 되게 된다. 그렇지 않으면 더이상 나무를 생성할 일이 없는데도 TreeModel 데이터가 메모리에 쓸데없이 잔존하게 된다. 

복합체 패턴(Composite Pattern)

: 복합 객체(Composite) 와 단일 객체(Leaf)를 동일한 컴포넌트로 취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록 하는 구조 패턴

  • 전체-부분의 관계를 갖는 객체들 사이의 관계를 트리 계층 구조로 정의해야 할때 유용 ex) 파일 시스템 구조
  • 그릇과 내용물을 동일시해서 재귀적인 구조를 만들기 위한 디자인 패턴

복합체 패턴 구조

  • Component: Leaf와 Compsite를 묶는 공통적인 상위 인터페이스
  • Composite: 복합 객체로서, Leaf 역할이나 Composite 역할을 넣어 관리하는 역할
    • Component 구현체들을 내부 리스트로 관리
    • Component 인터페이스의 구현 메서드인 operation은 복합 객체에서 호출되면 재귀 하여, 추가 단일 객체를 저장한 하위 복합 객체를 순회
  • Leaf: 단일 객체로서, 단순하게 내용물을 표시하는 역할을 한다.
    • Component 인터페이스의 구현 메서드인 operation은 단일 객체에서 호출되면 적절한 값만 반환한다

복합체 패턴의 핵심은 Composite와 Leaf가 동시에 구현하는 operation() 인터페이스 추상 메서드를 정의하고, Composite 객체의 operation() 메서드는 자기 자신을 호출하는 재귀 형태로 구현하는 것

 

Component

interface Component {
    void operation();
}

Leaf

class Leaf implements Component {

    @Override
    public void operation() {
        System.out.println(this + " 호출");
    }
}

Composite

class Composite implements Component {

    // Leaf 와 Composite 객체 모두를 저장하여 관리하는 내부 리스트
    List<Component> components = new ArrayList<>();

    public void add(Component c) {
        components.add(c); // 리스트 추가
    }

    public void remove(Component c) {
        components.remove(c); // 리스트 삭제
    }

    @Override
    public void operation() {
        System.out.println(this + " 호출");

        // 내부 리스트를 순회하여, 단일 Leaf이면 값을 출력하고,
        // 또다른 서브 복합 객체이면, 다시 그 내부를 순회하는 재귀 함수 동작이 된다.
        for (Component component : components) {
            component.operation(); // 자기 자신을 호출(재귀)
        }
    }

    public List<Component> getChild() {
        return components;
    }
}

Client

class Client {
    public static void main(String[] args) {
        // 1. 최상위 복합체 생성
        Composite composite1 = new Composite();

        // 2. 최상위 복합체에 저장할 Leaf와 또다른 서브 복합체 생성
        Leaf leaf1 = new Leaf();
        Composite composite2 = new Composite();

        // 3. 최상위 복합체에 개체들을 등록
        composite1.add(leaf1);
        composite1.add(composite2);

        // 4. 서브 복합체에 저장할 Leaf 생성
        Leaf leaf2 = new Leaf();
        Leaf leaf3 = new Leaf();
        Leaf leaf4 = new Leaf();

        // 5. 서브 복합체에 개체들을 등록
        composite2.add(leaf2);
        composite2.add(leaf3);
        composite2.add(leaf4);

        // 6. 최상위 복합체의 모든 자식 노드들을 출력
        composite1.operation();
    }
}

사용 시기

  • 데이터를 다룰때 계층적 트리 표현을 다루어야 할때
  • 복잡하고 난해한 단일 / 복합 객체 관계를 간편히 단순화하여 균일하게 처리하고 싶을때

장점

  • 단일체와 복합체를 동일하게 여기기 때문에 묶어서 연산하거나 관리할 때 편리하다.
  • 다형성 재귀를 통해 복잡한 트리 구조를 보다 편리하게 구성할 수 있다.
  • 수평적, 수직적 모든 방향으로 객체를 확장할 수 있다.
  • 새로운 Leaf 클래스를 추가하더라도 클라이언트는 추상화된 인터페이스만을 바라본다 -> 개방 폐쇄 원칙(OCP) (단일 부분의 확장이 용이)

단점

  • 디버깅의 어려움: 재귀 호출 특징 상 트리의 깊이(depth)가 깊어질수록 디버깅 어려움
  • 설계가 지나치게 범용성을 갖기 때문에 새로운 요소를 추가할 때 복합 객체에서 구성 요소에 제약을 갖기 힘들다.
  • 예를들어, 계층형 구조에서 leaf 객체와 composite 객체들을 모두 동일한 인터페이스로 다루어야하는데, 이 공통 인터페이스 설계가 까다로울 수 있다.
    • 복합 객체가 가지는 부분 객체의 종류를 제한할 필요가 있을 때
    • 수평적 방향으로만 확장이 가능하도록 Leaf를 제한하는 Composite를 만들때

구현

파일 디렉토리 시스템 구현

 

Component

// Component
interface Node {
    // 계층 트리 출력
    void print();
    void print(String str);

    // 파일/폴더 용량 얻기
    int getSize();
}

Composite

// Composite
class Folder implements Node {
    private String name; // 폴더 이름

    private ArrayList<Node> list;

    public Folder(String name) {
        this.name = name;
        list = new ArrayList<>();
    }

    // 리스트에 폴더, 파일 추가
    public void add(Node node) {
        list.add(node);
    }

    // 공백 indent 표현 처리를 위한 print 메서드 오버로딩
    public void print() {
        this.print("");
    }

    public void print(String str) {
        int size = getSize(); // 폴더가 담고 있는 모든 파일에 대한 용량 합산

        System.out.println(str + "\uD83D\uDCC2" + name + " (" + size + "kb)");

        for (Node node : list) {
            // Folder 일 경우 재귀 동작
            node.print(str + "    "); // 인자로 공백문자를 할당하여 indent 처리
        }
    }

    // 각 파일의 용량(kb) 구하기
    public int getSize() {
        int sum = 0;
        for (Node node : list) {
            sum += node.getSize(); // print 로직과 똑같이 재귀 동작
        }
        return sum;
    }
}

Leaf

// Leaf
class File implements Node {
    private String name; // 파일 이름
    private int size; // 파일 사이즈

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public void print() {
        this.print("");
    }

    public void print(String str) {
        System.out.println(str + "\uD83D\uDCDC" + name + " (" + size + "kb)");
    }

    public int getSize() {
        return size;
    }
}

Client

class Client {
    public static void main(String[] args) {

        Folder root = new Folder("root");

        File file1 = new File("file1", 10);
        Folder sub1 = new Folder("sub1");
        Folder sub2 = new Folder("sub2");

        root.add(sub1);
        root.add(file1);
        root.add(sub2);

        File file11 = new File("file11", 10);
        File file12 = new File("file12", 10);

        sub1.add(file11);
        sub1.add(file12);

        File file21 = new File("file21", 10);

        sub2.add(file21);

        // 전체 dir 출력
        root.print();
        /**
        root(40kb)
            sub1 (20kb)
                file11 (10kb)
                file12 (10kb)
            file1 (10kb)
            sub2 (10kb)
                file21 (10kb)
        **/
    }
}

브릿지 패턴(Bridge Pattern)

: 기능의 계층과 구현의 계층을 분리하여 서로 독립적으로 확장할 수 있게 만드는 구조 패턴. 즉, 추상화와 구현을 분리하여 변경이 서로 영향을 주지 않도록 하는 패턴

브릿지 패턴 구조

  • Abstraction: 기능의 추상화를 나타내는 추상 클래스
    • Implementor를 참조하고, 구현 클래스의 메서드를 호출하여 기능을 수행
  • RefinedAbstraction: 추상화를 구체화한 클래스
    • Abstraction을 확장하여 추가적인 기능을 구현 가능
  • Implementor: 구현에 대한 인터페이스
    • 구체적인 구현 클래스들이 따라야 하는 메서드를 정의
  • ConcreteImplementor: 실제 구현을 담당하는 클래스.
    • Implementor 인터페이스를 구현하고, Abstraction의 기능을 실제로 수행

사용 시기

  • 기능과 구현을 분리하여 독립적으로 확장해야 하는 경우
  • 하나의 기능이 여러 방식으로 구현될 수 있는 경우 (예: 다양한 플랫폼에서 작동하는 GUI)
  • 런타임에 구현을 변경할 수 있도록 시스템을 설계하고자 하는 경우

장점

  • 기능과 구현의 분리: 기능 계층과 구현 계층이 독립적으로 확장 가능해지므로, 코드의 유연성과 유지보수성이 향상
  • 확장성: 기능과 구현을 독립적으로 확장할 수 있어 새로운 기능과 구현이 추가될 때 수정 범위가 감소
  • 런타임 구현 변경 가능: 런타임에 구현체를 교체할 수 있어 다양한 방식으로 객체의 동작을 제어

단점

  • 구조가 복잡해짐: 클래스가 많아지기 때문에 구조가 복잡해짐
  • 간접 참조로 인한 성능 저하: 추상화된 인터페이스와 구현이 간접 참조를 통해 연결되므로, 성능이 조금 저하될 수 있음

구현

TV와 리모컨을 분리하여 각각 독립적으로 확장 가능하게 만든 예제

 

Implementor

// Implementor 인터페이스: 구현 부분에 대한 인터페이스 정의
interface TV {
    void on();
    void off();
    void setChannel(int channel);
}

// ConcreteImplementor 1: LGTV 클래스
class LGTV implements TV {
    @Override
    public void on() {
        System.out.println("LGTV is on.");
    }

    @Override
    public void off() {
        System.out.println("LGTV is off.");
    }

    @Override
    public void setChannel(int channel) {
        System.out.println("LGTV: Channel set to " + channel);
    }
}

// ConcreteImplementor 2: SonyTV 클래스
class SonyTV implements TV {
    @Override
    public void on() {
        System.out.println("SonyTV is on.");
    }

    @Override
    public void off() {
        System.out.println("SonyTV is off.");
    }

    @Override
    public void setChannel(int channel) {
        System.out.println("SonyTV: Channel set to " + channel);
    }
}

Abstraction

// Abstraction 클래스: 리모컨
abstract class RemoteControl {
    protected TV tv;

    public RemoteControl(TV tv) {
        this.tv = tv;
    }

    public abstract void turnOn();
    public abstract void turnOff();
    public abstract void changeChannel(int channel);
}

// RefinedAbstraction 클래스: 기본 리모컨
class BasicRemoteControl extends RemoteControl {
    public BasicRemoteControl(TV tv) {
        super(tv);
    }

    @Override
    public void turnOn() {
        tv.on();
    }

    @Override
    public void turnOff() {
        tv.off();
    }

    @Override
    public void changeChannel(int channel) {
        tv.setChannel(channel);
    }
}

Client

// 클라이언트 코드
public class BridgePatternDemo {
    public static void main(String[] args) {
        TV lgTv = new LGTV();
        TV sonyTv = new SonyTV();

        RemoteControl lgRemote = new BasicRemoteControl(lgTv);
        RemoteControl sonyRemote = new BasicRemoteControl(sonyTv);

        lgRemote.turnOn();
        lgRemote.changeChannel(10);
        lgRemote.turnOff();

        sonyRemote.turnOn();
        sonyRemote.changeChannel(5);
        sonyRemote.turnOff();
    }
}

/**
LGTV is on.
LGTV: Channel set to 10
LGTV is off.
SonyTV is on.
SonyTV: Channel set to 5
SonyTV is off.
**/

 

클라이언트 코드에서 TV와 리모컨이 분리되어 있어 다양한 TV와 리모컨 조합을 쉽게 만들 수 있다. 이와 같이 브릿지 패턴은 기능과 구현을 독립적으로 확장 가능한 구조를 제공하여 유지보수성과 유연성을 높인다.

728x90
반응형

'ETC' 카테고리의 다른 글

[오브젝트 - 기초편] 영화 예매 도메인 - 절차적인 설계로 구현하기  (3) 2024.12.30
[오브젝트 - 기초편] 영화 예매 도메인 예제  (3) 2024.12.30
[Design Pattern] Behavioral Pattern(행동 패턴) - 2  (3) 2024.11.03
[Design Pattern] Behavioral Pattern(행동 패턴) - 1  (1) 2024.11.02
[Design Pattern] Creational Pattern  (1) 2024.11.01
'ETC' 카테고리의 다른 글
  • [오브젝트 - 기초편] 영화 예매 도메인 - 절차적인 설계로 구현하기
  • [오브젝트 - 기초편] 영화 예매 도메인 예제
  • [Design Pattern] Behavioral Pattern(행동 패턴) - 2
  • [Design Pattern] Behavioral Pattern(행동 패턴) - 1
mxruhxn
mxruhxn
소소하게 개발 공부 기록하기
    반응형
    250x250
  • mxruhxn
    maruhxn
    mxruhxn
  • 전체
    오늘
    어제
    • 분류 전체보기 (150)
      • Java (21)
      • Spring (4)
      • Database (13)
      • Operating Syste.. (1)
      • Computer Archit.. (0)
      • Network (24)
      • Data Structure (6)
      • Algorithm (11)
      • Data Infra (7)
      • DevOps (12)
      • ETC (27)
      • Project (21)
      • Book (1)
      • Look Back (1)
  • 블로그 메뉴

    • 링크

      • Github
    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    mxruhxn
    [Design Pattern] Structural Pattern
    상단으로

    티스토리툴바