본문 바로가기
Concepts/Design Pattern

구조 패턴 - 가교(Bridge)

by ocwokocw 2021. 5. 1.

- 참조: GoF의 디자인 패턴

- 가교(Bridge) 패턴

가교(Bridge) 패턴은 다른 이름으로 핸들/구현부(Handle/Body) 라고도 불리우며, 구현부에서 추상적인 부분을 을 분리하여 독립적으로 다양성을 가질 수 있도록 한다.


- 시나리오

보통 하나의 추상적 개념에 대해 여러 가지 구현을 해야할 때, 대부분 상속을 통해 처리한다. 코드에서 추상적인 개념을 인터페이스로 정의하고, 클래스들이 이를 구현하게 처리하는 방식일것이다. 

 

상속은 잘 사용하면 좋은 기법이 되지만 구현부가 추상적 개념에 강하게 종속된다. 추상적 개념과 구현을 분리해서 재사용이 불가능해지거나 확장하기가 쉽지 않다.

 

이식성이 있는 Window를 추상적 개념으로 보고 이를 UI 툴킷을 써서 구현하는 예를 생각해보자. 이 Window는 X 윈도우 시스템에서 운영되는 응용프로그램에 사용되거나, IBM 프리젠테이션 매니저(PM) 에서 운영되는 응용프로그램에 적용될 수 있다. 여기에서 중요한 요소는 추상적인 개념 Window와 두 개의 플랫폼(X 윈도우 시스템, PM)을 추출할 수 있을것이다. 이런 상황을 인지하고 구현을 어떻게 할지 생각해보자.


- 구현과 문제점

위의 시나리오를 구현하기 위해 아마도 Window 추상 클래스를 정의하고, 이를 상속하여 X 윈도우나 PM에서 운영될 수 있도록 PMWindow, XWindow 클래스를 구현할것이다.

 

상속을 이용하여 처리하는것은 그럴듯 하지만 문제점이 있다. 다른 개념의 윈도우를 생성하거나 새로운 플랫폼이 생겨날 때 둘다 문제가 된다.

 

우선 새로운 개념의 Window인 IconWindow 가 있다고 가정해보자. IconWindow는 새로운 개념이지만 어쨌든 Window 이기 때문에 Window의 서브클래스일것이다. 우리는 이미 2개의 플랫폼을 갖고 있다. IconWindow가 X 윈도우와 PM 에서 모두 동작하려면 XIconWindow와 PMWindow의 서브클래스가 있어야 할 것이다. 이런 방식으로 구현하면 신규 Window와 신규 플랫폼이 늘어날때마다 경우의 수가 급격하게 늘어난다. (아래 다이어그램 참조)


- 가교(Bridge) 패턴의 적용

이 문제를 해결하기 위해 어떻게 해야할까? 우선 추상적 개념과 구현에 해당하는 클래스를 구별해야 한다. 위의 시나리오에서는 Window가 추상적 개념에 해당하며, XIconWindow와 PMWindow가 구현에 해당한다. 이 구현에 해당하는 클래스들의 최상위 클래스로 WindowImpl을 정의하고, Window에서 WindowImpl을 참조한다.

 

IconWindow나 TransientWindow 추상적 개념의 Window들은 Window 클래스를 상속하고, XWindow와 PMWindow 와 같은 구현에 해당하는 클래스들은 WindowImp 를 상속받는다. 이때 Window 클래스와 WindowImp 클래스의 관계를 가교(Bridge)라고 한다.

 

아래 다이어그램을 참조하여 위의 설명을 잘 이해해보길 바란다.

위의 다이어그램에서 참여자는 아래와 같다.

  • Abstraction(Window): 추상적 개념에 해당하는 인터페이스를 제공하며, 구현자를 참조한다.
  • RefinedAbstraction(IConWindow): 추상적 개념에 정의된 인터페이스를 확장한다.
  • Implementor(WindowImp): 구현 클래스에 대한 인터페이스를 제공한다. Abstraction 에 정의된 인터페이스에 정확하게 대응하지는 않아도 된다.
  • ConcreteImplementor(XWindowImp, PMWindowImp): Implementor 인터페이스를 실제 구현한다.

- Java 예제

위의 예제를 구현해보자. 우선 Window와 WindowImpl을 작성해보자. 두 클래스 모두 위의 다이어그램 정의에 따라 작성해주면 된다. 

 

WindowImpl 을 반환하는 책임은 Window의 서브클래스가 아닌 Window 객체에서 책임진다. WindowImpl은 Platform에 의존하기 때문에 Window 서브클래스들(IconWindow, TransientWindow) 에 따라 달라지는게 아니라고 생각하여 Window 에서 반환되게 하였다.

 

drawText와 drawRect는 추상 Window의 개념에 따라 외부에서도 사용되어야 하지만 WindowImpl은 IConWindow나 TransientWindow 에서만 사용되기 때문에 노출할 필요가 없어 접근 제어자를 protected 로 설정하였다.

 

public abstract class WindowImpl {

	abstract void devDrawText();
	abstract void devDrawLine();
}

public abstract class Window {

	private WindowImpl windowImpl = null;
	
	protected WindowImpl getWindowImpl() {
		
		if(windowImpl == null) {
			windowImpl = WindowFactory.getCurrentWindowImpl();
		}
		
		return windowImpl;
	}

	public abstract void drawText();
	public abstract void drawRect();
}

 

Platform에 따라 정의되는 WindowImpl의 구현체인 XWindow와 PMWindow 를 작성해보자. Test시 어떤 Platform 에서 호출되었는지만 알아볼 수 있게 간단하게 작성하였다.

 

public class XWindow extends WindowImpl{

	@Override
	void devDrawText() {
		System.out.println("devDrawText on XWindow platform");
	}

	@Override
	void devDrawLine() {
		System.out.println("devDrawLine on XWindow platform");
	}
}

public class PMWindow extends WindowImpl{

	@Override
	void devDrawText() {
		System.out.println("devDrawText on PMWindow platform");
	}

	@Override
	void devDrawLine() {
		System.out.println("devDrawLine on PMWindow platform");
	}
}

 

추상 Window의 구현체인 IConWindow와 TransientWindow 도 작성해보자. 추상 Window에서 WindowImpl를 반환하는 getWindowImpl 메소드를 이용하여 해당 PlatformWindow 객체를 얻는다.

 

public class IConWindow extends Window {

	@Override
	public void drawText() {
		
		System.out.println("drawText in IConWindow");
		
		WindowImpl windowImpl = getWindowImpl();
		windowImpl.devDrawText();
	}

	@Override
	public void drawRect() {
		
		System.out.println("drawRect in IConWindow");
		
		WindowImpl windowImpl = getWindowImpl();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
	}

	public void drawBorder() {
		System.out.println("drawBorder in IConWindow");
	}
}

public class TransientWindow extends Window {

	@Override
	public void drawText() {
		
		System.out.println("drawText in TransientWindow");
		
		WindowImpl windowImpl = getWindowImpl();
		windowImpl.devDrawText();
	}

	@Override
	public void drawRect() {
		
		System.out.println("drawRect in TransientWindow");
		
		WindowImpl windowImpl = getWindowImpl();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
		windowImpl.devDrawLine();
	}

	public void drawCloseBox() {
		System.out.println("drawCloseBox in TransientWindow");
	}
}

 

마지막으로 getWindowImpl 메소드에서 의존하고 있는 WindowFactory를 작성한다. 해당 함수안에서 상황에 따라 WindowImpl을 반환하는 코드를 작성해도 되지만, WindowImpl은 Platform에 따라 달라지므로 Factory 를 하나 두어 관리하게 하였다. 

 

public enum WindowFactory {

	XWindow {
		protected WindowImpl createWindowImpl() {
			return new XWindow();
		}
	}, 
	PMWindow {
		protected WindowImpl createWindowImpl() {
			return new PMWindow();
		}
	};
	
	abstract protected WindowImpl createWindowImpl();
	
	private static WindowFactory systemFactoryType = null;

	public static void setSystemFactoryType(WindowFactory systemFactoryType) {
		WindowFactory.systemFactoryType = systemFactoryType;
	}
	
	public static WindowImpl getCurrentWindowImpl() {
		if(systemFactoryType == null) {
			throw new RuntimeException("The systemFactoryType is not configured.");
		}
		
		return systemFactoryType.createWindowImpl();
	}
}

 

Factory 가 추가된 버전의 다이어그램은 아래와 같다.

댓글