브릿지 패턴과 일반적인 추상/구현 클래스 패턴의 차이점이 궁금해요~
글쓴이: greathero / 작성시간: 토, 2013/03/23 - 1:59오전
여기서 제가 말하는 일반적인 추상/구현 클래스 패턴은
class Timer { public: virtual ~Timer(); virtual void onTick(int tickPeriod) = 0; }; class TimerA: public Timer { public: ... virtual void onTick(int tickPeriod); void foo(); ... }; class TimerB: public Timer { public: ... virtual void onTick(int tickPeriod); void qoo(); ... };
이런식으로 하나의 abstract class(비가상 함수도 포함할 수 있는)가 있고 n개의 derived class가 있는것을 말합니다.
브릿지 패턴(impl 패턴)이라는게 보여서 글을 좀 읽어봤는데...
브릿지 패턴이 위에서 말한 추상/구현 클래스 패턴이랑 다를게 없어보이거든요 제눈엔ㅠㅠ
왜 굳이 구현 클래스에 impl이라는 이름을 붙여서 브릿지 패턴이라는게 존재하는지 궁금합니다.
두개가 똑같은건가요? 아니면 분명한 차이점이 있나요?
전 똑같은거 같은데...
혹시라도 두개에 분명한 차이점이 있다면 설명좀 해주세요~
Forums:
제가 그려본 그림입니다
Abstraction 의 Operations 는 Implementor 의 OperationImp 들을 사용해 구현되고
RefinedAbstraction 의 Operations 는 Abstraction 의 Operation 들을 사용해 구현됩니다.
이렇게 함으로써 RefinedAbstraction 과 Implementor (ConcreteImplementor)가 분리되게 됩니다.
RefinedAbstraction 에서 Implementor (ConcreteImplementor)로의 의존성이 없는 건 이걸 나타냅니다.
헤드퍼스트 디자인패턴에서 설명하기로는
장점
* 구현을 인터페이스에 완전히 결합시키지 않았기 때문에 구현과 추상화된 부분을 분리시킬 수 있다.
* 추상화된 부분과 실제 구현 부분을 독립적으로 확장 할 수 있다.
* 추상화된 부분을 구현한 구상 클래스를 바꿔도 클라이언트 쪽에는 영향을 끼치지 않는다.
사용법 및 단점
* 여러 플랫폼에서 사용해야 할 그래픽스 및 윈도우 처리 시스템에서 유용하게 쓰인다.
* 인터페이스와 실제 구현부를 서로 다른 방식으로 변경해야 하는 경우에 유용하게 쓰인다.
* 디자인이 복잡해진다는 단점이 있다.
구현뿐만 아니라 추상화된 부분까지 변경시켜야 하는 경우에는 브리지 패턴을 씁니다.
Thanks for being one of those who care for people and mankind.
I'd like to be one of those as well.
References
http://www.lepus.org.uk/ref/companion/Bridge.xml
http://en.wikipedia.org/wiki/Bridge_pattern
http://www.vincehuston.org/dp/bridge.html
Thanks for being one of those who care for people and mankind.
I'd like to be one of those as well.
추상/구현 클래스 패턴이 무언가요?
패턴을 칭할 때는 알려진 이름들을 사용합니다.
"추상/구현 클래스 패턴"은 혹시 자작하신 명명인가요?
올려주신 코드만 보면 마치 브리지 패턴의 제 그림에서 우측편의 구현 계층(Implementations)에 해당할 것 같은데요?? 브리지 패턴이 되려면 좌측편의 인터페이스 계층(Interfaces)이 있어야 할 것 같습니다.
이 점이 둘간의 차이점이 될 것 같습니다.
저도 책에서만 봤지 실제 구현할 때 사용해보지 못했습니다. 사용해보신 분의 실제적이 답변이 있다면 좋겠군요..
"여러 플랫폼에서 사용해야 할 그래픽스 및 윈도우 처리 시스템에서 유용하게 쓰인다."
라고 했듯이 브리지 패턴을 써야하는 부분은 이렇게 멀티 플랫폼 지원을 위한 경우가 많은 것으로 알고 있습니다.
Thanks for being one of those who care for people and mankind.
I'd like to be one of those as well.
넵 답변 감사합니다 ㅎㅎ
아랫분이 실질적인 예제와 함께 답변을 달아주셨네요 ㅎㅎ
브릿지 패턴 요약(?)
1.
예시하신 Timer, TimerA, TimerB의 경우, TimerA와 TimerB는 Timer로부터 상속을 받고는 있지만 가상 함수가 아닌 각각의 public 함수들, 즉 TimerA::foo(), TimerB::goo()이 있는데요, 이는 다음을 의미합니다.
- TimerA, TimerB 의 객체를 직접 생성해서 foo() 나 goo() 를 쓰기도 하고
- onTick() 만 사용할 경우, Timer* 변수로 TimerA 객체나 TimerB 객체의 포인터를 저장해서 사용하기도 한다.
(경우에 따라 Timer* 변수를 TimerA* 또는 TimerB* 변수로 dynamic_cast 해서 foo, goo를 사용할 수도 있긴 하겠지만 바람직한 방식은 아니죠.)
이런 경우라면 브릿지 패턴을 굳이 써야 할 필요를 못 느낄 수 있겠네요...
2.
하지만 브릿지 패턴은, 말씀하신 대로 "인터페이스와 구현을 분리"하는 게 가장 큰 특징입니다. 이로 인한 장점은,
(1) 해당 클래스를 사용하는 코드에서, 구현부분을 감춤으로써 구현 부분 관련된 소스 코드에 대한 의존성을 줄일 수 있고,
(2) 구현에 대한 정보를 감출 수 있다
는 겁니다.
2.1
예시 코드를 조금 바꿔서 설명해 볼까요...
TimerA::foo()가 public이 아니라 private 이고, void TimerA::onTick() 에서 foo()를 사용한다고, 즉 foo()는 외부에서 사용하는 용도가 아니라 onTick() 의 helper 함수인 거라 가정해 보죠. 그런데 어찌 하다 보니, TimerA::onTick() 에 점점 많은 기능이 들어가게 되어 foo2(), foo3(), ... 이런 새로운 helper 함수들이 추가되어야 한다면 어떻게 될까요?
TimerA의 헤더 파일이 바뀔 때마다 이 헤더 파일들을 include 하는 소스 파일들을 다시 빌드해야 합니다. public 함수의 인터페이스가 바뀌는 경우라면, 사용하는 쪽이 영향받는 거야 당연한 것이지만, private 영역 부분은 사용하는 쪽에서는 몰라도 되는 것들인데 이것의 변경 때문에 다시 빌드하는 것은 좀 짜증나는 일입니다. TimerA가 아주 많은 곳에 include 되어 있다면 더더욱 그렇겠죠?
브릿지 패턴은 이럴 때 그 진가를 발휘합니다. TimerAImp 라는 TimerA 전용 구현 클래스를 별도로 만들고, TimerA 의 모든 구현 함수들을 TimerAImp 로 옮깁니다. 그리고 TimerA 에는 TimerAImp* _imp 변수 하나와 public 함수들만 남겨 둡니다. 그리고나서 모든 TimerA 함수들은 _imp 변수의 동일한 이름의 함수를 호출하는 것으로 바꾸는 거죠.
일단 구현 관련된 부분이 별도의 클래스(imp)로 분리되고 나면, 구현과 관련된 변경 사향으로 인한 영향은 TimerAImp.cpp 와 TimerA.cpp 에만 국한됩니다. 구현부를 아무리 지지고 볶아도 TimerA 의 public 인터페이스가 바뀌지 않는 이상, TimerA.hpp 를 include 하는 다른 소스 파일에는 영향을 주지 않습니다.
2.2
다음으로, 내가 TimerA를 소스가 아닌 라이브러리로 형태로 제공해야 하는데, 구현과 관련된 부분을 최대한 감추고 싶은 경우를 생각해 볼까요? 라이브러리를 binary로 제공한다 해도, 헤더 파일은 같이 제공해야 하겠죠.
만약 브릿지 패턴으로 구현부가 분리되어 있지 않고, foo(), foo2(), … 이런 함수들이 TimerA에서 선언되어 있으면, 헤더 파일로부터, 뭔가 foo 관련된게 많다는 걸 알 수 있겠죠? 이를 테면 3DES 암호화를 사용하게 되어 encodeBy3Des() 뭐 이런 멤버 함수를 추가하면, 이 헤더 파일을 본 것만으로도 3DES로 encryption 을 하나 보네? 짐작할 수 있을 겁니다. C++에서 이런 부분까지 모두 감추고 싶다면, 브릿지 패턴 말고는 방법이 없습니다.
3.
사실 브릿지 패턴은 가상함수를 이용한 다형성과 상관이 없고요, 구현부를 인터페이스에서 분리하는 게 핵심입니다. 그런데 구현부의 변경 사항, 예를 들어 어떤 기능이 플랫폼별로 달라져야 하는 경우, 이 부분을 고립화시켜 변경이 생겼을 때 영향받는 부분을 최소화 하는 경우에 사용하다 보면,
- 변경되는 부분들을 구현부 클래스에다 가상 함수를 만들어 처리하고,
- 인터페이스 클래스에서는 imp 객체에 이를 위임하는 구조로 구현
되는 경우가 대부분입니다. 그래서 결과만 놓고 보면 그냥 가상함수 쓰는 것과의 차이가 없어 보일 수 있죠...
3.1
Timer 클래스의 경우 브릿지 패턴을 적용해야 한다면, 다음과 같을 겁니다.
위 코드의 Timer에는 가상함수가 없습니다. 모든 public 멤버 함수는 그냥 자신이 가지고 imp 객체한테 위임하는 껍데기에 불과합니다. 실제 달라져야 하는 하는 부분은 TimerImp 클래스의 파생 클래스에서 구현합니다.
3.2
그리고 브릿지 패턴을 쓰게 되면, Timer 객체에 어떤 imp 객체를 할당하는 지를 결정하는 게 중요해 지는데, 보통은 이를 처리하는 별도의 생성 패턴을 사용하게 됩니다. 3.1 의 code에서는 가장 흔하고 쉽게 쓸 수 있는 팩토리 메쏘드를 사용했는데요, TimerImp::CreateImp() static 함수는 생성해야 하는 timer의 타입을 인자로 받아서 해당하는 timer 객체를 생성해서 반환합니다. 그리고 이 타입은 timer를 생성자에서 인자로 받아 TimerImp::CreateImp()에게 넘겨 주도록 했고요...
상황에 따라서, timer 타입을 생성자에서 받는 게 아니라, 조건부 컴파일 플래그나 시스템 환경 변수에 따라 결정되도록 하는 식으로 응용해도 되겠죠.
4.
브릿지 패턴의 단점은,
(1) 모든 함수 호출이 imp 객체로 위임하기 때문에 함수 호출이 한번 더 된다는 성능상의 문제(?),
(2) imp class 를 분리해 내고 어떤 imp 객체를 생성해서 할당해야 하는 지를 정하는 등과 같이 설계와 구현에 손이 좀 더 간다는 점,
(3) 그래서 디버깅 할 때 현재 어떤 imp 객체가 생성되어서 할당되어 있는 지를 파악해야 한다는 점 정도입니다.
하지만 구현부의 변경 사항을 고립(isolation)시켜서 얻는 이득이 그런 단점들보다 훨씬 크다고 판단되는 경우, 예컨대 어떤 클래스가 광범위한 코드에 include 되어 사용되는데 빈번하게 변경될 가능성이 클 때나, 헤더 파일을 감추어야 하는 경우라면 브릿지 패턴을 적용하는 게 좋겠죠...
정말 상세한 답변 감사합니다 ㅎㅎ
덕분에 브릿지 패턴에 대해 이해를 확실히 했습니다!
5년이 지나 브리지패턴을 좀 더 공부하고 싶어서
5년이 지나 브리지패턴을 좀 더 공부하고 싶어서 검색해보고 이 글을 다시 봤습니다.
5년 전에 제가 수박 겉핡기 식으로 요점만 정리해두었었는데, 이 글을 다시 보니 수박 속살까지 이해할 수 있네요.
감사합니다.
Thanks for being one of those who care for people and mankind.
I'd like to be one of those as well.
8년이 지나 브리지패턴을 회사 프로젝트에 사용하려고
8년이 지나 브리지패턴을 회사 프로젝트에 사용하려고 검색해보고 이 글을 다시 봤습니다.
수박의 속살까지 알았었다 생각했던 제가 8년뒤에 또 보니 또 처음 보는 것 같습니다.
글을 정말 잘 쓰신 것 같습니다. 작은 책을 읽는 느낌이 듧니다.
훌륭한 답글 감사합니다.
Thanks for being one of those who care for people and mankind.
I'd like to be one of those as well.
댓글 달기