udemy 강좌(Java Design Patterns & SOLID Design Principles)를 정리한 내용이다.
브릿지는 무엇인가?
브릿지 디자인 패턴에 대해 알아보자. 구조적 디자인 패턴이며 어떤 일을 하는지 이해하는데 시간이 걸리는 디자인 패턴 중 하나이다. 왜냐하면 처음 들었을 때 불가능하다고 생각되는 것을 하는 것처럼 생각되기 때문이다.
- 구현부와 추상부는 일반적으로 상속관계에서 서로 결합되어 있다.
- 브릿지 패턴을 사용하여 서로 영향을 주지 않고 변경하는 방식으로 디커플링 할 수 있다.
- 두 개의 개별 상속 구조를 만들어서 작업할 수 있다. 구현부를 위한 것과 추상부를 위한 것
- 두 개의 구조를 연결하기 위해 합성(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());
}
}
구현 고려사항
- 하나의 구현부만 가지고 있다면 추상 구현부를 만들지 않을 수도 있다.
- 추상부는 생성자에 사용하고자 하는 구체 구현부를 정할 수 있고 다른 클래스로 위임을 할 수도 있다. 이전 예제에서는 추상부가 구체 구현부를 모르고 있고 결합도를 낮추는 효과가 있다.
디자인 고려사항
- 브릿지는 추상부와 구현부를 개별적으로 변경하게 함으로써 좀 더 나은 확장성을 보여주고 있다. 시스템을 모듈화하는데 개별적으로 빌드 및 패키징을 할 수 있다.
- 구현부에 추상 객체를 생성하는 데 추상 팩토리 패턴을 사용함으로써 추상부와 구체 구현부를 결합도를 낮출 수 있다.
위험요소
- 브릿지 디자인 패턴을 이해하고 구현하기가 꽤 복잡하다.
- 브릿지 디자인 패턴을 사용하기로 결정하기 전에 많은 고민을 해야 하고 잘 이해가 되는 디자인으로 만들어야 한다.
- 미리 많은 고민이 필요하다. 기존 코드에 브릿지를 추가하기는 어렵다. 진행중인 프로젝트에서 개발 기간 중 막바지에 브릿지를 추가하는 것은 꽤 많은 양의 재작업이 필요할 수도 있다.