Design Pattern | 브리지 패턴(Bridge Pattern)
브리지 패턴(Bridge Pattern)은 GoF 디자인 패턴 중 하나로, 기능과 구현을 분리하게 독립적으로 확장할 수 있도록 돕는 구조적 디자인 패턴입니다.
이 패턴은 객체지향 설계에서 클래스 폭발 문제를 해결하기 위해 사용되며, 두 개 이상의 독립적인 자원을 클래스 계층 구조로 분리해 각각의 계층을 따로 개발하고 확장할 수 있게 합니다.
1. 브리지 패턴의 정의와 의도
브리지 패턴은 두 계층(추상화와 구현)을 연결하는 다리와 같습니다.
즉, 클라이언트는 상위 계층인 추상화와 작업하고, 추상화는 하위 계층인 구현에 작업을 위임합니다.
이 패턴의 목적은 하위 계층(구현부)과 상위 계층(추상화)을 분리하여 독립적인 확장을 가능하게 만드는 것입니다.
2. 브리지 패턴을 사용하는 의도
이 패턴을 사용함으로써 기능이 복잡해지는 상황에서도 각 기능을 독립적으로 추가하거나 확장할 수 있으며,
코드 유지보수의 용이성도 증가합니다.
또한, 클래스 폭발 문제를 해결하는 데 효과적입니다.
클래스 폭발(Class Explosion)
객체지향 프로그래밍에서 특정 기능을 확장하거나 새로운 기능을 추가할 때,
너무 많은 클래스가 생성되어 코드 관리가 복잡해지는 상황을 의미합니다.
이 문제는 특히 여러 차원에서 클래스 계층을 확장해야 하는 경우에 발생합니다.
예를 들어, 특정 클래스가 여러 종류의 기능을 제공해야 하고, 각각의 기능이 서로 다른 조합을 가질 때, 모든 조합을 처리하기 위해 새로운 클래스를 계속 추가해야 합니다.
예시를 통한 설명
가장 흔한 예로, 도형(Shape)과 색상(Color)을 다루는 프로그램이 있다고 가정합니다.
이 프로그램에서 Circle(원)과 Square(사각형)라는 두 가지 도형 유형이 있고,
각 도형을 빨간색(Red)과 파란색(Blue)으로 표현할 수 있다고 해봅니다.
처음에는 Circle, Square 두개의 클래스만 존재하지만, 색상을 추가하려 하면 각 도형에 대한 새로운 클래스를 만들어야 합니다.
RedCircle, BlueCircle, RedSquare, BlueSquare...
이렇게 클래스 조합이 기하급수적으로 증가하면서 관리해야 할 클래스의 수가 폭발적으로 늘어나는 것이 바로 클래스 폭발 문제입니다.
해결책 : 구현과 추상화의 분리
브리지 패턴은 상속 대신 객체 합성을 사용해 이 문제를 해결할 수 있습니다.
즉, 추상화(ex. 도형)는 구현(ex. 색상)을 참조하고, 기능의 확장과 구현의 확장을 독립적으로 진행할 수 있게 됩니다.
추상화는 하위 구현 계층에 모든 작업을 위임하기 때문에, 서로 독립적인 기능 계층과 구현 계층이 생기게 됩니다.
3. 브리지 패턴의 구조
추상화(Abstraction)
추상화는 시스템의 상위 수준 제어 논리를 정의하는 부분입니다. 추상화는 실제로는 구현 계층에서 처리되는 작업들을 직접 수행하지 않지만, 이 작업들을 조율하고 관리하는 역할을 합니다.
역할
추상화는 클라이언트가 시스템과 상호작용하는 인터페이스 역할을 합니다.
클라이언트는 추상화 객체를 통해 시스템의 기능을 사용할 수 있으며, 이때 추상화는 실제 작업을 수행하는 구현 객체에게 작업을 위임합니다.
구성
추상화는 보통 하나의 인터페이스나 추상 클래스로 정의됩니다. 이 추상 클래스는 구현 객체에 대한 참조를 보유하고 있으며, 이 참조를 통해 구현 계층과 소통합니다.
abstract class RemoteControl {
protected Device device;
public RemoteControl(Device device) {
this.device = device;
}
public abstract void togglePower();
public abstract void volumeUp();
public abstract void volumeDown();
}
위 예시에서 RemoteControl 클래스는 추상화로, 상위 수준의 제어 논리를 제공하며, Device라는 구현 객체에 의존해 실제 작업을 수행합니다.
구현(Implementation)
구현은 추상화 계층이 작업을 위임하는 실제 실행 계층입니다.
이 계층은 모든 구체적인 작업을 수행하는 역할을 하며, 다양한 구현체가 동일한 인터페이스를 따르게 되어 있습니다.
역할
구현 계층은 추상화 계층에서 전달된 명령을 실제로 수행합니다.
이 계층은 각기 다른 구체적인 작업 방법을 정의하며, 플랫폼에 맞춘 특정 로직을 포함할 수 있습니다.
구성
구현 계층은 보통 인터페이스 또는 추상 클래스로 정의되며, 이 인터페이스를 구현하는 여러 구체적인 클래스들이 존재합니다.
이러한 구체적인 클래스들은 특정 플랫폼이나 환경에 맞춘 코드를 포함할 수 있습니다.
interface Device {
void enable();
void disable();
int getVolume();
void setVolume(int percent);
}
class Tv implements Device {
// Tv-specific implementation
public void enable() { /* Tv-specific code */ }
public void disable() { /* Tv-specific code */ }
public int getVolume() { /* Tv-specific code */ }
public void setVolume(int percent) { /* Tv-specific code */ }
}
class Radio implements Device {
// Radio-specific implementation
public void enable() { /* Radio-specific code */ }
public void disable() { /* Radio-specific code */ }
public int getVolume() { /* Radio-specific code */ }
public void setVolume(int percent) { /* Radio-specific code */ }
}
위 코드에서 Device 인터페이스는 구현 계층의 역할을 하며,
Tv와 Radio는 각각 Device 인터페이스를 구현한 구체적인 구현 클래스입니다.
추상화와 구현의 관계
추상화와 구현은 서로 독립적으로 확장될 수 있습니다. 추상화 객체는 자신이 가진 구현 객체와 협력하여 작업을 수행하지만, 각각의 계층은 서로 영향을 받지 않고 독립적으로 변경될 수 있습니다.
예를 들어, 새로운 구현체를 추가해도 추상화 계층에는 영향을 미치지 않으며, 새로운 추상화를 추가해도 기존의 구현체를 변경할 필요가 없습니다.
class AdvancedRemoteControl extends RemoteControl {
public AdvancedRemoteControl(Device device) {
super(device);
}
public void mute() {
device.setVolume(0);
}
}
AdvancedRemoteControl은 RemoteControl의 하위 클래스이며,
mute라는 새로운 기능을 추가했습니다. 이 클래스는 Device 인터페이스를 구현한 어떠한 객체와도 함께 작동할 수 있습니다.
클라이언트의 역할
클라이언트는 추상화 계층과 작업을 수행하지만, 실제로는 구현 객체를 추상화 객체에 연결하는 역할도 수행합니다.
클라이언트는 특정 구현 객체를 선택하여 추상화 객체에 전달하고, 이를 통해 시스템의 기능을 사용하는 방식입니다.
public class Client {
public static void main(String[] args) {
Device tv = new Tv();
RemoteControl remote = new AdvancedRemoteControl(tv);
remote.togglePower();
remote.volumeUp();
remote.mute();
}
}
클라이언트는 Tv라는 구체적인 구현체를 AdvancedRemoteControl에 전달하고, 이를 통해 기능을 실행합니다.
Abstraction: 기능을 수행하는 인터페이스로, Implementor 객체에 대한 참조를 가집니다.
Implementor: 추상화된 기능을 수행하는 메서드를 선언합니다.
ConcreteImplementor: 실제 구현을 제공하는 클래스입니다.
RefinedAbstraction: Abstraction의 구체화된 버전으로, 더 복잡한 기능을 제공할 수 있습니다.
4. 브리지 패턴의 구현 - java
문제 : 도형과 색상 조합
예시로 도형과 색상을 결합하는 문제를 해결하려합니다.
Shape 클래스는 Color 클래스와 결합하여 다양한 색상의 도형을 만들 수 있어야 합니다.
1) Color 인터페이스 (구현 계층)
// Implementor 역할
interface Color {
String applyColor();
}
2) ConcreteImplementor (구체적 구현 클래스들)
class Red implements Color {
@Override
public String applyColor() {
return "Red";
}
}
class Blue implements Color {
@Override
public String applyColor() {
return "Blue";
}
}
3) Shape 추상 클래스 (추상화 계층)
// Abstraction 역할
abstract class Shape {
protected Color color;
protected Shape(Color color) {
this.color = color;
}
abstract void draw();
}
4) RefinedAbstraction (구체적 추상화)
class Circle extends Shape {
public Circle(Color color) {
super(color);
}
@Override
void draw() {
System.out.println("Drawing Circle in " + color.applyColor() + " color.");
}
}
class Square extends Shape {
public Square(Color color) {
super(color);
}
@Override
void draw() {
System.out.println("Drawing Square in " + color.applyColor() + " color.");
}
}
5) 클라이언트 코드
public class BridgePatternExample {
public static void main(String[] args) {
Shape redCircle = new Circle(new Red());
Shape blueSquare = new Square(new Blue());
redCircle.draw(); // Drawing Circle in Red color.
blueSquare.draw(); // Drawing Square in Blue color.
}
}
Shape 클래스는 Color 객체를 참조하고, 그 참조된 객체의 메서드를 호출하여 색상을 적용합니다.
Circle과 Square는 Shape의 구체적 구현체로, 색상과 결합하여 각각의 도형을 생성합니다.
새로운 도형이나 색상이 추가될 때 기존 클래스 계층을 건드리지 않고도 확장할 수 있습니다.
5. 브리지 패턴의 장단점
장점
- 확장성 : 추상화와 구현을 독립적으로 확장할 수 있어 매우 유연한 구조를 제공합니다.
- 유지보수 용이성 : 구현부와 추상화부를 분리해 변경이 필요한 부분만 수정할 수 있으므로 유지보수가 용이합니다.
- 플랫폼 독립성 : 클라이언트는 구현 세부 사항에 독립적으로 상위 수준의 추상화와만 작업합니다.
단점
- 복잡성 증가: 클래스 계층을 분리하기 때문에 코드가 처음에는 다소 복잡해질 수 있습니다.
- 설계 비용 : 처음 설계할 때 추상화와 구현의 명확한 분리를 요구하므로 설계 비용이 다소 높을 수 있습니다.
6. 브리지 패턴의 실제 사용 사례
- GUI 라이브러리 : 서로 다른 운영체제에서 같은 인터페이스를 사용하고자 할 때 GUI 계층과 운영체제 API 계층을 분리합니다.
- 데이터베이스 : 다양한 DB엔진을 지원하는 애플리케이션에서 각 엔진의 구현을 분리하고 상위 계층에서는 동일한 인터페이스를 사용합니다.
- 그래픽 시스템 : 다양한 형식의 이미지를 출력하는 시스템에서 추상화된 인터페이스를 통해 여러 구현체를 지원할 수 있습니다.
출처