브리지 패턴
의도
브리지는 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴입니다.
문제
추상화? 구현? 어렵게 들리시나요? 진정하세요. 그리고 간단한 예시를 한번 살펴봅시다.
Circle
(원) 및 Square
(직사각형)라는 한 쌍의 자식 클래스들이 있는 기하학적 Shape
(모양) 클래스가 있다고 가정해 봅시다. 이 클래스 계층 구조를 확장하여 색상을 도입하기 위해 Red
(빨간색) 및 Blue
(파란색) 모양들의 자식 클래스들을 만들 계획입니다. 그러나 이미 두 개의 자식 클래스가 있으므로 BlueCircle
(파란색 원) 및 RedSquare
(빨간색 직사각형)와 같은 네 가지의 클래스 조합을 만들어야 합니다.
새로운 모양 유형들과 색상 유형들을 추가할 때마다 계층 구조는 기하급수적으로 성장합니다. 예를 들어, 삼각형 모양을 추가하려면 각 색상별로 하나씩 두 개의 자식 클래스들을 도입해야 합니다. 그리고 그 후에 또 새 색상을 추가하려면 각 모양 유형별로 하나씩 세 개의 자식 클래스를 만들어야 합니다. 유형들이 많아지면 많아질수록 코드는 점점 복잡해집니다.
해결책
이 문제는 모양과 색상의 두 가지 독립적인 차원에서 모양 클래스들을 확장하려고 하기 때문에 발생합니다. 이것은 클래스 상속과 관련된 매우 일반적인 문제입니다.
브리지 패턴은 상속에서 객체 합성으로 전환하여 이 문제를 해결하려고 시도합니다. 이것이 의미하는 바는 차원 중 하나를 별도의 클래스 계층구조로 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조하도록 한다는 것입니다.
이 접근 방식을 따르면, 색상 관련 코드를 Red
및 Blue
라는 두 개의 자식 클래스들이 있는 자체 클래스로 추출할 수 있습니다. 그런 다음 Shape
클래스는 색상 객체들 중 하나를 가리키는 참조 필드를 받습니다. 이제 모양은 연결된 색상 객체에 모든 색상 관련 작업을 위임할 수 있습니다. 이 참조는 Shape
및 Color
클래스들 사이의 브리지(다리) 역할을 할 것입니다. 이제부터 새 색상들을 추가할 때 모양 계층구조를 변경할 필요가 없으며 그 반대의 경우도 마찬가지입니다.
추상화와 구현
GoF의 디자인 패턴 은 브리지 패턴 정의의 일부로 추상화 및 구현이라는 용어들을 소개합니다. 저는 위 용어들이 너무 학문적이라 그로 인해 패턴이 실제보다 더 복잡하게 들린다고 생각합니다. 그러면 GoF의 책의 난해한 용어들 뒤에 숨겨진 의미를 모양과 색상이 있는 간단한 예를 통해 해독해 보겠습니다.
추상화(인터페이스라고도 함)는 일부 개체(entity)에 대한 상위 수준의 제어 레이어입니다. 이 레이어는 자체적으로 실제 작업을 수행해서는 안 되며, 작업들을 구현 레이어(플랫폼이라고도 함)에 위임해야 합니다.
참고로 지금 우리가 이야기하는 것은 당신이 선호하는 프로그래밍 언어의 인터페이스들이나 추상 클래스들이 아닙니다. 이것들은 같은 것이 아닙니다.
실제 앱을 예로 들면 추상화는 그래픽 사용자 인터페이스이며 구현은 그래픽 사용자 인터페이스 레이어가 사용자와 상호작용하여 그 결과로 호출하는 배경 운영 체제 코드(API)입니다.
일반적으로 이러한 앱은 두 가지 독립적인 방향으로 확장할 수 있습니다.
- 다른 여러 가지의 그래픽 사용자 인터페이스를 가진다 (예: 일반 고객 또는 관리자용으로 맞춘 인터페이스들).
- 여러 다른 API들을 지원한다 (예: 맥, 리눅스 및 윈도우에서 앱을 실행할 수 있는 API들).
최악의 경우 이 앱은 수백 개의 조건문들이 코드 전체에 다양한 API와 다양한 유형의 그래픽 사용자 인터페이스들을 연결한 거대한 스파게티 코드 그릇처럼 형성됩니다.
당신은 특정 인터페이스-플랫폼 조합들과 관련된 코드를 별도의 클래스들로 추출하여 이 복잡함에 질서를 부여할 수 있으나, 곧 이러한 클래스들이 많이 있다는 것을 알게 될 것입니다. 새로운 그래픽 사용자 인터페이스를 추가하거나 다른 API를 지원하려면 점점 더 많은 클래스를 생성해야 하므로 클래스 계층구조는 기하급수적으로 성장할 것입니다.
브리지 패턴으로 이 문제를 해결해 봅시다. 브리지 패턴은 클래스들을 두 개의 계층구조로 분리하라고 제안합니다:
- 추상화: 앱의 그래픽 사용자 인터페이스 레이어.
- 구현: 운영 체제의 API.
추상화 객체는 앱의 드러나는 모습을 제어하고 연결된 구현 객체에 실제 작업들을 위임합니다. 서로 다른 구현들은 공통 인터페이스를 따르는 한 상호 호환이 가능하며, 이에 따라 같은 그래픽 사용자 인터페이스는 리눅스와 윈도우에 동시에 작동할 수 있습니다.
따라서 당신은 API 관련 클래스들을 건드리지 않고 그래픽 사용자 인터페이스 클래스들을 변경할 수 있습니다. 그리고 다른 운영 체제에 대한 지원을 추가하려면 구현 계층구조 내에 자식 클래스를 생성하기만 하면 됩니다.
구조
-
추상화는 상위 수준의 제어 논리를 제공하며, 구현 객체에 의존해 실제 하위 수준 작업들을 수행합니다.
-
구현은 모든 구상 구현들에 공통적인 인터페이스를 선언하며, 추상화는 여기에 선언된 메서드들을 통해서만 구현 객체와 소통할 수 있습니다.
추상화는 구현과 같은 메서드들을 나열할 수 있지만 보통은 구현이 선언한 다양한 원시 작업들에 의존하는 몇 가지 복잡한 행동들을 선언합니다.
-
구상 구현들에는 플랫폼별 맞춤형 코드가 포함됩니다.
-
정제된 추상화들은 제어 논리의 변형들을 제공합니다. 그들은 그들의 부모처럼 일반 구현 인터페이스를 통해 다른 구현들과 작업합니다.
-
일반적으로 클라이언트는 추상화와 작업하는데만 관심이 있습니다. 그러나 추상화 객체를 구현 객체들 중 하나와 연결하는 것도 클라이언트의 역할입니다.
의사코드
이 예시는 브리지 패턴이 기기와 리모컨을 관리하는 앱의 모놀리식 코드를 나누는 데 어떻게 도움이 되는지 보여줍니다. Device
클래스들은 구현의 역할을 하는 반면, Remote
클래스들은 추상화 역할을 합니다.
기초 리모컨 클래스는 이 클래스를 장치 객체와 연결하는 참조 필드를 선언합니다. 모든 리모컨은 일반 장치 인터페이스를 통해 장치들과 작동하므로 같은 리모컨이 여러 장치 유형을 지원할 수 있습니다.
장치 클래스들과 독립적으로 리모컨 클래스들을 개발할 수 있으며, 필요한 것은 새로운 리모컨 자식 클래스를 만드는 것뿐입니다. 예를 들어 기초 리모컨에는 버튼이 두 개뿐일 수 있지만, 추가 터치스크린과 추가 배터리 같은 기능들도 가지도록 확장할 수 있습니다.
클라이언트 코드는 Remote
의 생성자를 통해 원하는 유형의 리모컨을 특정 장치 객체와 연결합니다.
적용
브리지 패턴은 당신이 어떤 기능의 여러 변형을 가진 모놀리식 클래스를 나누고 정돈하려 할 때 사용하세요. (예: 클래스가 다양한 데이터베이스 서버들과 작동할 수 있는 경우).
클래스가 성장할수록 그 작동 방식을 파악하기가 더 어려워지고 해당 클래스를 변경하는 데 더더욱 오랜 시간이 걸립니다. 클래스 기능의 여러 변형 중 하나를 변경하려면 클래스 전체에 걸쳐 여러 가지 변경을 수행해야 할 수 있으며, 이를 수행 중 개발자들은 종종 실수하거나 일부 중요한 부작용들을 해결하지 않기도 합니다.
브리지 패턴을 사용하면 모놀리식 클래스를 여러 클래스 계층구조로 나눌 수 있습니다. 그런 다음 각 계층구조의 클래스들을 다른 계층구조들에 있는 클래스들과는 독립적으로 변경할 수 있습니다. 이 접근 방식은 코드의 유지관리를 단순화하고 기존 코드가 손상될 위험을 최소화합니다.
이 패턴은 여러 직교(독립) 차원에서 클래스를 확장해야 할 때 사용하세요.
브리지 패턴은 각 차원에 대해 별도의 클래스 계층구조를 추출할 것을 제안합니다. 원래 클래스는 모든 작업을 자체적으로 수행하는 대신 추출된 계층구조들에 속한 객체들에 관련 작업들을 위임합니다.
브리지 패턴은 런타임(실행시간)에 구현을 전환할 수 있어야 할 때에 사용하세요.
선택 사항이지만 브리지 패턴을 사용하면 추상화 내부의 구현 객체를 바꿀 수 있으며, 그렇게 하려면 필드에 새 값을 할당하기만 하면 됩니다.
위 항목은 많은 사람이 브리지와 전략 패턴을 혼동하는 주된 이유입니다. 패턴은 클래스의 구조를 설계하는 특정 방법 그 이상의 것이라는 것을 기억하세요. 패턴은 제기되고 있는 문제 및 의도에 대하여도 소통할 수 있습니다.
구현방법
-
클래스에서 직교 차원들을 식별하세요. 이러한 독립적인 개념들은 추상화/플랫폼, 도메인/인프라, 프런트엔드/백엔드 또는 인터페이스/구현 등일 수 있습니다.
-
클라이언트가 필요로 하는 작업들을 확인한 후 기초 추상 클래스에서 정의하세요.
-
모든 플랫폼들에 제공되어야 하는 작업들을 결정하세요. 그 후 추상화에 필요한 작업들을 일반 구현 인터페이스에서 선언하세요.
-
도메인의 모든 플랫폼에 대해 구상 구현 클래스들을 생성하되 이 클래스들 모두가 구현 인터페이스를 따르도록 하세요.
-
추상화 클래스 내에서 구현 유형에 대한 참조 필드를 추가하세요. 추상화는 대부분 작업들을 위 필드에서 참조되는 구현 객체에 위임합니다.
-
상위 수준 논리의 변형들이 여러 개 있는 경우 기초 추상화 클래스를 확장하여 각 변형에 대해 정제된 추상화들을 만드세요.
-
클라이언트 코드는 구현 객체를 추상화의 생성자에 전달하여 이 객체를 그 생성자에 연관시켜야 합니다. 그 후에 클라이언트는 구현을 잊어버린 후 추상화 객체와만 작업할 수 있습니다.
장단점
- 플랫폼 독립적인 클래스들과 앱들을 만들 수 있습니다.
- 클라이언트 코드는 상위 수준의 추상화를 통해 작동하며, 플랫폼 세부 정보에 노출되지 않습니다.
- 개방/폐쇄 원칙. 새로운 추상화들과 구현들을 상호 독립적으로 도입할 수 있습니다.
- 단일 책임 원칙. 추상화의 상위 수준 논리와 구현의 플랫폼 세부 정보에 집중할 수 있습니다.
- 결합도가 높은 클래스에 패턴을 적용하여 코드를 더 복잡하게 만들 수 있습니다.
다른 패턴과의 관계
-
브리지는 일반적으로 사전에 설계되며, 앱의 다양한 부분을 독립적으로 개발할 수 있도록 합니다. 반면에 어댑터는 일반적으로 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 합니다.
-
브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있습니다. 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 합니다. 하지만 이 패턴들은 모두 다른 문제들을 해결합니다. 패턴은 특정 방식으로 코드의 구조를 짜는 레시피에 불과하지 않습니다. 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문입니다.
-
당신은 추상 팩토리를 브리지와 함께 사용할 수 있습니다. 이 조합은 브리지에 의해 정의된 어떤 추상화들이 특정 구현들과만 작동할 수 있을 때 유용합니다. 이런 경우에 추상 팩토리는 이러한 관계들을 캡슐화하고 클라이언트 코드에서부터 복잡성을 숨길 수 있습니다.
-
빌더를 브리지와 조합할 수 있습니다. 디렉터 클래스는 추상화의 역할을 하고 다양한 빌더들은 구현의 역할을 합니다.