Bridge 패턴
9. Bridge 패턴
현실세계의 다리가 강 양쪽의 장소를 연결하는 역할을 하듯이 ‘Bridge 패턴’도 두 장소를 연결하는 역할을 한다. Bridge 패턴이 다리 역할을 하고 있는 두 곳은 기능의 클래스 계층과 구현의 클래스 계층 이다. 일단 두 클래스 계층이 무엇인지 부터 살펴보도록 하자.
클래스 계층의 두 가지 혁할
새로운 ‘기능’을 추가 하고 싶은 경우
어떤 클래스 Something이 있다고 가정한다. Something에 새로운 기능을 추가하고 싶을 때(새로운 메소드를 추가), 우리들은 Something의 하위 클래스(자식, 파생, 확장 클래스 등)로 SomethingGood 클래스를 만든다. 이때 작은 클래스 계층이 생성된다.
Something ↳ SomethingGood
이것은 기능을 추가하기 위해 만들어진 계층이다. - 상위 클래스는 기본적인 기능을 가지고 있다. - 하위 클래스에서 새로운 기능을 추가한다. 이 클래스 계층을 ‘기능의 클래스 계층’ 이라고 한다.
SomethingGood 클래스에 또 다른 새로운 기능을 추가하려면, SomethingGood 클래스의 하위 클래스로서 SomethingBetter 클래스를 만든다. 이것으로 기능의 클래스 계층이 더욱 깊어진다. Something ↳ SomethingGood ↳ SomethingBetter 새로운 기능을 추가하고 싶을 경우 클래스 계층 안에서 자신의 목적과 가까운 클래스를 찾아내 그 하위 클래스를 만들어, 목적한 기능을 추가한 새로운 클래스를 만든다는 것이 기능의 클래스 계층이다. Tip > 일반적으로 클래스 계층은 너무 깊지 않은 것이 좋다.
새로운 ‘구현’을 추가하고 싶은 경우
Template Method 패턴에서 추상 클래스의 역할에 대해서 배웠다. 추상 클래스가 일련의 메소드들을 추상 메소드로서 선언하고 인터페이스(API)를 규정한다. 그리고 하위 클래스 측에서 그 추상 메소드를 실제로 구현하고 상위 클래스는 추상 메소드로 인터페이스(API)를 규정하는 역할을 하고 하위 클래스는 그것을 구현하는 역할을 한다. 이와 같은 상위 클래스와 하위 클래스의 역할 분담에 의해 부품으로의 가치(교환 가능성)가 높은 클래스를 만들 수 있다. 여기에도 클래스 계층이 등장한다. 예를 들면, 상위클래스 AbstractClass의 추상 메소드를 구현하는 하위 클래스를 ConcreteClass로 하면, 다음과 같은 소규모의 클래스 계층이 만들어 진다. AbstractClass ↳ ConcreteClass 그러나 여기에서 사용되는 클래스 계층은 기능을 추가하기 위한 것도 새로운 메소드를 추가하기 위한 것도 아니다. 여기는 다음과 같은 역할 분담을 위해 클래스 계층이 사용된다. - 상위 클래스는 추상 메소드에 의해 인터페이스(API)를 규정한다. - 하위 클래스는 구상 메소드에 의해 그 인터페이스(API)를 구현한다. 이 클래스 계층을 ‘구현의 클래스 계층’ 이라고 한다. AbstractClass의 다른 구현을 만들 때, 하위 클래스를 AnotherConcerteClass로 하면 구현의 클래스 계층은 다시 변한다. 새로운 구현을 만들기 위해서는 AbstractClass의 하위 클래스를 만들어 추상 메소드를 구현하게 된다. 이것이 구현의 클래스 계층이다.
클래스 계층의 혼재와 클래스 계층의 분리
기능의 클래스 계층과 구현의 클래스 계층에 대해서 이해가 되었다면, 이제는 하위 클래스를 만들려고 생각할 때, 자신의 의도를 다음과 같이 확인할 필요가 있다. ‘나는 기능을 추가하려고 하는가? 구현을 수행하려고 하는가?’ 클래스 계층이 하나라면 기능의 클래스 계층과 구현의 클래스 계층이 하나의 계층구조 안에 혼재하게 된다. 이것은 클래스 계층을 복잡하게 해서 예측을 어렵게 할 우려가 있다. 자신이 하위 클래스를 만들려고 할 때 클래스 계층의 어디에 만들면 좋을지 헤매기 쉽다.
그래서 ‘기능의 클래스 계층’과 ‘구현의 클래스 계층’을 두 개의 독립된 클래스 계층으로 분리한다. 단순히 분리만 하면 흩어져 버리기 때문에 두 개의 클래스 계층 사이에 다리를 놓는일이 필요하다. 이때 사용하는 패턴이 바로 Bridge 패턴이다. 반드시 ‘두개의 클래스 계층’을 의식하면서 살펴보자.
예제 프로그램
Brigde 패턴을 사용한 예제 프로그램을 살펴보자. 여기서 만들 예제 프로그램은 ‘무언가를 표시하기’ 위한 것이다. 다소 추상적인 이야기지만 프로그램을 읽어나가다 보면 조금씩 구체화된다.
기능의 클래스 계층 : Display 클래스
Display 클래스는 추상적인 ‘무언가를 표시하는 것’이다. 이 클래스는 ‘기능의 클래스 계층’의 최상위에 있는 클래스다. impl 필드는 Display 클래스의 ‘구현’을 나타내는 인스턴스다. 생성자에는 구현을 나타내는 클래스의 인스턴스를 전달한다. 인수로 전달된 인스턴스는 impl 필드에 저장되어 이후에 처리할 때 사용한다. open, print, close 세 메소드는 Display 클래스가 제공하는 인터페이스(API)에서 표시를 실행하는 순서를 나타내고 있다. - open은 표시의 전처리 - print는 표시 그 자체 - close는 표시의 후처리 라는 역할을 맡는다. 이 세 메소드의 내용을 보면, 메소드들을 실현하기 위해 impl 필드의 구현 메소드를 이용하고 있다. 여기에서 Display의 인터페이스(API)가 DisplayImpl의 인터페이스(API)로 변환된다. display 메소드는 open, print, close라는 Display의 인터페이스(API)를 이용해서 ‘표시한다’라는 처리를 실현하고 있다.
package org.jellydiss.patternpractice.bridge; //'표시한다'는 클래스 (기능의 클래스 계층) public class Display { private DisplayImpl impl; public Display(DisplayImpl impl) { this.impl = impl; } public void open() { impl.rawOpen(); } public void print() { impl.rawPrint(); } public void close() { impl.rawClose(); } public final void display() { open(); print(); close(); } }
기능의 클래스 계층 : CountDisplay 클래스
’기능의 클래스 계층’을 살펴보자. Display 클래스에 기능을 추가한 것이 CountDisplay 클래스다. Display 클래스에는 ‘표시한다’는 기능밖에 없지만 CountDisplay 클래스에서는 ‘지정횟수만큼 표시한다’는 기능을 추가해 보자. 이것이 multiDisplay 메소드다. CountDisplay 클래스에서는 open, print, close라는 Display 클래스에서 상속받은 메소드를 사용해서 새로운 메소드를 사용해서 새로운 메소드를 추가하고 있다. 여기까지가 ‘기능의 클래스 계층’이다.
구현의 클래스 계층 : DisplayImpl 클래스
여기부터가 ‘구현의 클래스 계층’이다. DisplayImpl 클래스는 ‘구현의 클래스 계층’의 최상위에 위치한다. DisplayImpl 클래스는 추상 클래스이며 rawOpen, rawPrint, rawClose라는 세 가지 메소드를 가지고 있다. 이것은 Display 클래스의 open, print, close에 각각 대응하며 전처리, 표시, 후처리를 실행한다.
package org.jellydiss.patternpractice.bridge; //'표시한다'는 클래스(구현의 클래스 계층) public abstract class DisplayImpl { public abstract void rawOpen(); public abstract void rawPrint(); public abstract void rawClose(); }
구현의 클래스 계층 StringDisplayImpl 클래스
드디어 진정한 ‘구현’단계에 왔다. StringDisplayImpl 클래스는 문자열을 표시한다. 단지 표시만 하는 것이 아니라 DisplayImpl 클래스의 하위 클래스로서 rawOpen, rawPrint, rawClose 메소드를 사용해서 표시를 실행한다. DisplayImpl과 StringDisplayImpl 두 클래스가 ‘구현의 클래스 계층’에 해당한다.
package org.jellydiss.patternpractice.bridge; //'문자열을 사용해서 표시한다'는 클래스 public class StringDisplayImpl extends DisplayImpl { private String string; private int width; public StringDisplayImpl (String string) { this.string = string; this.width = string.getBytes().length; } public void rawOpen() { printLine(); } public void rawPrint() { System.out.println("|" + string + "|"); } public void rawClose(){ printLine(); } public void printLine() { System.out.print("+"); for (int i = 0; i < width; i++){ System.out.print("-"); } System.out.println("+"); } }
Main 클래스
Main 클래스에서는 앞서 기술한 네 개의 클래스를 조합해서 문자열을 표시한다. 변수 d1에는 Display클래스의 인스턴스를 대입하고 변수 d2와 d3에는 CountDisplay 클래스의 인스턴스를 대입한다. 둘 다 StringDisplayImpl 클래스의 인스턴스가 구현을 담당한다. d1, d2, d3은 모두 Display 클래스의 인스턴스의 일종이기 때문에 display메소드를 호출할 수 있고 d3은 multiDisplay 메소드도 호출할 수 있다.
package org.jellydiss.patternpractice.bridge; public class Main { public static void main(String[] args){ Display d1 = new Display(new StringDisplayImpl("Hello, Korea.")); Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World")); CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe.")); d1.display(); d2.display(); d3.display(); d3.multiDisplay(5); } }
Main 실행화면
Bridge 패턴의 등장인물
Abstaction(추상화)의 역할 예제의 Display 클래스에 해당하며, ‘기능의 클래스 계층’의 최상위 클래스다. Implementor 역할의 메소드를 사용해서 기본적인 기능만이 기술되어 있는 클래스다. 기본적인 기능만이 기술되어 있는 클래스다. 이 인스턴스는 Implementor 역할을 가지고 있다.
RefinedAbstraction(개선된 추상화)의 역할 예제의 CountDisplay 클래스에 해당하며, Abstraction 역할에 기능을 추가한 역할이다.
Implementor(구현자)의 역할 예제의 DisplayImpl 클래스에 해당하며, ‘구현의 클래스 계층’의 최상위 클래스다. Abstaction 역할의 인터페이스(API)를 구현하기 위한 메소드를 규정하는 역할이다.
Concrete Implementor(구체적인 구현자)의 역할 예제의 StringDisplayImpl 클래스에 해당하며, Implementor 역할의 인터페이스(API)를 구체적으로 구현하는 역할이다.
사고 넓히기
분리해 두면 확장이 편해진다.
Bridge 패턴의 특징은 ‘기능의 클래스 계층’과 ‘구현의 클래스 계층’을 분리하는 것이다. 이 두 개의 클래스 계층을 분리해 두면 각각의 클래스 계층을 독립적으로 확장할 수 있다. 기능을 추가하고 싶으면 기능의 클래스 계층에 클래스를 추가한다. 이때 구현의 클래스 계층은 전혀 수정할 필요가 없다. 새로 추가한 기능은 ‘모든 구현’에서 이용할 수 있다. 구현의 클래스 계층을 운영체제에 적용해서 생각해보자. 어떤 프로그램이 운영체제에 의존하는 부분이 있어서 윈도우판, 맥킨토시판, 유닉스판으로 분리된다고 가정하자. 운영체제에 의존하는 부분을 Bridge 패턴의 ‘구현의 클래스 계층’으로 표현한다. 즉 각 운영체제 공통의 인터페이스(API)를 정해서 Implementor 역할로 하고 CnocreteImplementor 역할로 윈도우판, 맥킨토시판, 유닉스판의 세 개의 클래스를 만든다. 이렇게 하면 ‘기능의 클래스 계층’ 쪽에 아무리 기능을 추가한다해도 세 개의 운영체제에 동시에 대응한다.
상속은 견고한 연결이고 위임은 느슨한 연결이다
‘상속’은 클래스를 확장하기 위해 편리한 방법이지만 클래스간의 연결을 강하게 고정시킨다. 소스코드 상에서
class SomethingGood extends Something { ... }
라고 쓰면 SomethingGood 클래스는 Something 클래스의 하위 클래스가 된다. 그리고 이 관계는 소스 코드를 고쳐 쓰지 않는 한 바꿀 수 없는 매우 견고한 연결이 된다. 프로그램의 필요에 따라서 클래스 간의 관계를 척척 바꾸고 싶을 때에 상속을 사용하는 것은 부적절하다. 교체할 때마다 소스 코드를 변경할 수 없기 때문이다. 이와 같은 경우에는 ‘상속’이 아니라 ‘위임’을 사용한다. 예제 프로그램에서는 Display 클래스 내에서 위임이 사용된다. Display 클래스의 impl 필드에는 구현되는 인스턴스가 저장되어 있어서 - open을 실행할 때에는 impl.rawOpen()을 호출한다. - print를 실행할 때에는 impl.rawPrint()를 호출한다 - close를 실행할 때에는 impl.rawClose()를 호출한다 라는 식으로 ‘떠넘기기’를 하고 있다. 이것이 위임이다. 상속은 견고한 연결이고 위임은 느슨한 연결이다. Display클래스의 인스턴스를 만드는 단계에서 인수로 전달되어 온 것과 연결되기 때문이다. 예제에서는 Main 클래스 내에서 Display나 CountDisplay의 인스턴스를 만들고 그때 StringDisplayImpl의 인스턴스를 인수에게 전달했다. 만약 StringDisplayImpl 클래스이외의 ConcreteIMplementor 역할이 있다고 가정하고 그 인스턴스를 Display나 countDisplay에게 전달하면 그것으로 구현이 확실히 교체된다. 이 교체를 수행할 때 수정한 것은 Main 클래스 뿐이다. Display나 DisplayImpl 등의 소스 코드는 전혀 수정할 필요가 없다. 복수의 클래스를 설계할 때는 이것을 이해해 둘 필요가 있다. 상속과 위임의 관계에 대해서는 Template Method 패턴에서도 등장했기에 참고하자. 9-1 이 장의 예제 프로그램에 클래스를 추가해서 ‘랜덤횟수를 표시한다’라는 처리를 실현해 보십시오. 이때 어느 클래스를 확장해야 하는지를 확인해 보십시오. 내가 생각한 답 : 솔직히 위 문제는 뭔가 번역에 이상이 있는 것 같지만, 내가 생각한 건 아래와 같이 구현하는 것이다.
package org.jellydiss.patternpractice.bridge; public class RandomDisplay extends Display{ public RandomDisplay(DisplayImpl impl) { super(impl); } public void randomDisplay(int times) { open(); for (int i = 0; i <(int)(Math.random() * times); i++){ print(); } close(); } }
책 해설 : 기존의 CountDisplay를 확장하여 사용한다.
package org.jellydiss.patternpractice.bridge; import java.util.Random; public class RandomDisplay extends CountDisplay{ private Random random = new Random(); public RandomDisplay(DisplayImpl impl) { super(impl); } public void randomDisplay(int times) { multiDisplay(random.nextInt(times)); } }
9-2 이 장의 예제프로그램에 클래스를 추가해서 ‘텍스트 파일의 내용을 표시한다’라는 처리를 실현해 보십시오. 이때 어느 클래스를 확장하는지를 확인해 보십시오. 내가 생각한 답 : Display(기능의 클래스 계층)을 확장한다. 책 해설 : 구현의 클래스 계층을 참조해야 한다. 자세한 내용은 책을 참고하자. 9-3 아래와 같은 모양을 표시하는 클래스를 이 장의 예제 프로그램에 추가한다고 가정합니다.
<> <*> <**> <***>
|- |##- |####- |######- |########- |##########-
이 모양들은 시작문자 - 장식문자가 여러번 - 마지막 문자와 행 바꾸기 를 1행으로 해서 이것이 여러행 반복되고 있다. 반복할 때마다 점점 장식 문자의 개수가 증가한다. 이같은 동작을 하는 클래스를 예제 프로그램에 추가하려고 할 때, 기능의 클래스 계층에 넣어야 할까? 아니면 구현의 클래스 계층에 넣어야 할까? 어떻게 하면 Brigde 패턴을 적용할 수 있을지 생각해보자. 내가 생각한 답 : 구현의 클래스 계층을 확장해야 한다고 생각한다. DisplayImpl 클래스를 확장하여 클라이언트 단에서 인스턴스를 해당하는 클래스로 지정하여 생성한다. 책 해설 : 추가할 내용을 우선 기능과 구현으로 분리한 위 한쪽은 기능의 클래스 계층을 입력하고, 다른 한쪽은 구현의 클래스 계층을 입력하면 Bridge 패턴과 정확히 일치한다. 흠… 내가 아직 Bridge 패턴에 대해 정확히 이해하지 못한 것 같다. 이후 포스팅 전에 다시한번 봐야 할것 같다.