도서 요약 / / 2023. 2. 15. 21:07

디자인 패턴 - 브릿지(bridge) 패턴

udemy 강좌(Java Design Patterns & SOLID Design Principles)를 정리한 내용이다.

https://www.udemy.com/course/design-patterns-in-java-concepts-hands-on-projects/learn/lecture/9604610?start=0#overview


브릿지는 무엇인가?

브릿지 디자인 패턴에 대해 알아보자. 구조적 디자인 패턴이며 어떤 일을 하는지 이해하는데 시간이 걸리는 디자인 패턴 중 하나이다. 왜냐하면 처음 들었을 때 불가능하다고 생각되는 것을 하는 것처럼 생각되기 때문이다.

  • 구현부와 추상부는 일반적으로 상속관계에서 서로 결합되어 있다.
  • 브릿지 패턴을 사용하여 서로 영향을 주지 않고 변경하는 방식으로 디커플링 할 수 있다.
  • 두 개의 개별 상속 구조를 만들어서 작업할 수 있다. 구현부를 위한 것과 추상부를 위한 것
  • 두 개의 구조를 연결하기 위해 합성(Composition)을 사용한다.

UML

브릿지(bridge)라는 이름은 추상부와 구현부 사이에 존재하는 특별한 관계에서 나온 말이다. 구현부는 추상부 내부에 존재한다. 여기에 두 개의 상속 구조가 있다. 왼쪽에 추상부 구조가 있고 오른쪽에는 구현부 구조가 있다. 그리고 이 두개가 서로 합성관계로 연결되어 있고 이것을 브릿지라고 부르는 것이다.

브릿지 구현

  • 클라이언트에서 필요한 추상부를 정의하면서 시작
    • 일반적인 기본 동작을 결정하고 그것을 추상부에 정의한다.
    • 정제된(refined) 추상부에 더 많은 기능을 정의할 수도 있고 더 추가적인 동작을 제공할 수 있다.
    • 그리고 나서 구현부를 정의한다. 구현부 메소드는 추상부와 일치할 필요가 없다. 하지만 추상부는 구현부 메소드를 사용하여 작업을 수행할 수 있다.
    • 그리고 하나 이상의 구현을 제공하는 구현부를 작성한다.
  • 추상부는 구체적인 구현부 인스턴스와 구성하여 만들어진다.

예제: UML

여기 브릿지 디자인 패턴을 사용하는 예제가 있다.

클라이언트 코드가 있고 이 클라이언트는 first-in/first-out 알고리즘을 제공하는 컬렉션이 필요하다. 그래서 여기에 offer(), poll()을 제공하는 FifoCollection이라는 클래스가 있다. 그리고 offer()는 기본적으로 객체를 받고 컬렉션에 추가한 다음 first-in/first-out 원칙으로 리턴한다.

그리고 정제된(refined) 추상부가 있다. Queue가 있고 FIFO를 구현하고 offer(), poll()에 대한 구현부를 가지고 있다. 그래서 클라이언트는 오른쪽에 Queue 클래스를 사용할 것이다.

오른쪽에는 LinkedList라는 구현부 클래스가 있다. Queue의 첫 항목에 추가하는 add와 같은 메소드가 정의되어 있고 첫 항목를 삭제하고 끝 항목에 추가, 삭제한다. 이 부분이 구현부이고 offer()와 직접적으로 연결되지 않고 자신만의 메소드가 정의되어 있다. LinkedList에 어떤 offer() 메소드도 가지고 있지 않다.

하지만 offer() 메소드에 대한 구현부를 이 메소드를 이용해서 어떻게 제공할 수 있을까? 이것이 브릿지가 제공하는 것이다.

하단에 LinkedList에 대한 두 개의 구현부가 있는 것을 볼 수 있다. ArrayLinkedList, SinglyLinkedList

중간에 브릿지가 있다는 것을 알 수 있다. FIFO 컬렉션 인스턴스는 하나의 인스턴스를 가지거나 LinkedList 클래스의 객체를 가질 것이다.

이것이 브릿지 디자인 패턴이 동작하는 방식이다.

추상부에 대해 이야기할 때 특정 상속구조에 특정 클래스를 추가할 수 있거나 기존 FIFO 컬렉션을 수정할 수 있다.

Java 구현

// 여기가 추상부이다.
// FIFO 컬렉션을 나타낸다.
public interface FifoCollection<T> {

    // 엘리먼트를 추가
    void offer(T element);

    // 컬렉션에서 첫 엘리먼트를 삭제하고 리턴한다.
    T poll();
}
// 여기가 구현부이다.
// 구현부는 추상 구조와 관련없이 자신만의 구조를 정의한다.
public interface LinkedList<T> {

    void addFirst(T element);

    T removeFirst();

    void addLast(T element);

    T removeLast();

    int getSize();
}
// 정제된 추상부이다.
public class Queue<T> implements FifoCollection<T> {

    private LinkedList<T> list;

    public Queue(LinkedList<T> list) {
        this.list = list;
    }

    @Override
    public void offer(T element) {
        list.addLast(element);
    }

    @Override
    public T poll() {
        return list.removeFirst();
    }
}
// 구체 구현부이다. 
//이 구현부는 node를 사용하는 전통적인 LinkedList이다.
public class SinglyLinkedList<T> implements LinkedList<T> {

    private class Node {
        private Object data;
        private Node next;
        private Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    private int size;
    private Node top;
    private Node last;

    @Override
    public void addFirst(T element) {
        if (top == null) {
            last = top = new Node(element, null);
        }
        size++;
    }

    @Override
    public T removeFirst() {
        if (top == null) {
            return null;
        }
        T temp = (T) top.data;
        if (top.next != null) {
            top = top.next;
        } else {
            top = null;
            last = null;
        }
        size--;
        return temp;
    }

    @Override
    public void addLast(T element) {
        if(last == null) {
            last = top = new Node(element, null);
        } else {
            last.next = new Node(element, null);
            last = last.next;
        }
        size++;
    }

    @Override
    public T removeLast() {
        if(last == null) {
            return null;
        }
        if(top == last) {
            @SuppressWarnings("unchecked")
            T temp = (T)top.data;
            top = last = null;
            size--;
            return temp;
        }
        //since we don't have a back pointer
        Node temp = top;
        while(temp.next != last) {
            temp = temp.next;
        }
        @SuppressWarnings("unchecked")
        T result = (T)last.data;
        last = temp;
        last.next = null;
        size--;
        return result;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        String result = "[";
        Node temp = top;
        while(temp!=null) {
            result += temp.data + (temp.next == null?"":", ");
            temp = temp.next;
        }
        result += "]";
        return result;
    }
}
public class Client {

    public static void main(String[] args) {
        FifoCollection<Integer> collection = new Queue<>(new SinglyLinkedList<>());
        collection.offer(10);
        collection.offer(40);
        collection.offer(99);

        System.out.println(collection.poll());
        System.out.println(collection.poll());
        System.out.println(collection.poll());
        //
        System.out.println(collection.poll());
    }
}

구현 고려사항

  • 하나의 구현부만 가지고 있다면 추상 구현부를 만들지 않을 수도 있다.
  • 추상부는 생성자에 사용하고자 하는 구체 구현부를 정할 수 있고 다른 클래스로 위임을 할 수도 있다. 이전 예제에서는 추상부가 구체 구현부를 모르고 있고 결합도를 낮추는 효과가 있다.

디자인 고려사항

  • 브릿지는 추상부와 구현부를 개별적으로 변경하게 함으로써 좀 더 나은 확장성을 보여주고 있다. 시스템을 모듈화하는데 개별적으로 빌드 및 패키징을 할 수 있다.
  • 구현부에 추상 객체를 생성하는 데 추상 팩토리 패턴을 사용함으로써 추상부와 구체 구현부를 결합도를 낮출 수 있다.

위험요소

  • 브릿지 디자인 패턴을 이해하고 구현하기가 꽤 복잡하다.
  • 브릿지 디자인 패턴을 사용하기로 결정하기 전에 많은 고민을 해야 하고 잘 이해가 되는 디자인으로 만들어야 한다.
  • 미리 많은 고민이 필요하다. 기존 코드에 브릿지를 추가하기는 어렵다. 진행중인 프로젝트에서 개발 기간 중 막바지에 브릿지를 추가하는 것은 꽤 많은 양의 재작업이 필요할 수도 있다.



반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유