브릿지 패턴
구현에서 추상을 분리하여, 이들이 독립적으로 다양성을 가질 수 있도록 만들어준다.
큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴이다.
구조
- Abstraction - 추상적 개념에 대한 인터페이스를 제공하고 객체 구현자에 대한 참조자를 관리한다.
- RefinedAbstraction - 추상적 개념에 정의된 인터페이스를 확장한다.
- Implementor - 구현 클래스에 대한 인터페이스를 제공한다. 실질적인 구현을 제공한 서브클래스들에 공통적인 연산의 시그니처만을 정의한다. 일반적으로 Implementor 인터페이스는 기본적인 구현 연산을 구행하고, Abstraction은 더 추상화된 서비스 관점의 인터페이스를 제공한다.
- ConcreteImplementor - Implementor인터페이스를 구현한 서브클래스로, 실질적인 구현 내용을 담고 있다.
기본 인터페이스
// Implementor: 구현 클래스에 대한 인터페이스
interface IImplementor
{
void OperationImpl();
}
class ConcreteImplementorA : IImplementor
{
public void OperationImpl()
{
// 구현 내용
}
}
class ConcreteImplementorB : IImplementor
{
public void OperationImpl()
{
// 다른 구현 내용
}
}
// Abstraction: 추상적 개념에 대한 인터페이스를 제공하고 객체 구현자에 대한 참조자를 관리
abstract class Abstraction
{
protected IImplementor Implementor;
public Abstraction(IImplementor implementor)
{
Implementor = implementor;
}
public abstract void Operation();
}
// RefinedAbstraction: 추상적 개념에 정의된 인터페이스를 확장
class RefinedAbstraction : Abstraction
{
public RefinedAbstraction(IImplementor implementor) : base(implementor) { }
public override void Operation()
{
// 다른 동작 수행
Implementor.OperationImpl();
// 다른 동작 수행
}
}
// 클라이언트 코드
class Client
{
static void Main()
{
IImplementor implementorA = new ConcreteImplementorA();
IImplementor implementorB = new ConcreteImplementorB();
Abstraction abstractionA = new RefinedAbstraction(implementorA);
Abstraction abstractionB = new RefinedAbstraction(implementorB);
abstractionA.Operation();
abstractionB.Operation();
}
}
왜 사용해야 할까?
객체 지향에서는 상속을 통해 기존의 코드를 재사용 또는 확장할 수 있다. 그림판 프로그램의 예시를 들어, 그림판에서 도형을 그릴 수 있는 IShape 인터페이스와 색을 표현하는 IColor 인터페이스를 통해 그림을 그린다고 가정해보자.
interface IShape
{
void Draw();
}
interface IColor
{
void ApplyColor();
}
도형 중 사각형, 원을 그리게 하기 위해 IShape를 상속받는 서브클래스 Rectangle, Circle을 정의했다.
색의 경우 빨강, 파랑을 표현하기 위해 IColor를 상속받는 서브클래스 Red, Blue를 정의했다.
class Red : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying red color");
}
}
class Blue : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying blue color");
}
}
class Circle : IShape
{
public void Draw()
{
Console.Write("Drawing Circle - ");
}
}
class Rectangle : IShape
{
public void Draw()
{
Console.Write("Drawing Rectangle - ");
}
}
자, 이제 우리는 사각형, 원을 그릴 수 있고, 빨강, 파랑을 색칠할 수 있다. 그렇다면, 빨간색 원, 파란색 사각형을 그리려면 어떻게 해야할까?
가장 쉬운 방법은 빨간색 원, 파란색 사각형을 그리는 구체화 클래스를 구현하는 것이다. 각각의 도형, 각각의 색에 조합에 맞춰 도형을 그리도록 구현하는 것이다.
class GraphicLibrary
{
public void DrawRedCircle()
{
Console.WriteLine("Drawing red circle");
// 빨간 원을 그리는 구체적인 구현
}
public void DrawBlueCircle()
{
Console.WriteLine("Drawing blue circle");
// 파란 원을 그리는 구체적인 구현
}
public void DrawRedRectangle()
{
Console.WriteLine("Drawing red rectangle");
// 빨간 사각형을 그리는 구체적인 구현
}
public void DrawBlueRectangle()
{
Console.WriteLine("Drawing blue rectangle");
// 파란 사각형을 그리는 구체적인 구현
}
}
하지만 이런 방식은 도형의 종류와 색상이 강하게 결합되어 있다. 만약 새로운 도형이나 다른 색상이 추가되면, 해당하는 메서드를 계속 추가해야 한다. 이로 인해 클래스가 계속해서 불필요하게 커져 유지보수가 어려워진다.
그렇다고 두 구체화 클래스를 상속받을 수도 없다. 객체지향에서는 다중 상속 시 변수의 모호성이 발생하는 것과 다중 상속으로 발생하는 메소드의 무거움 때문에 다중 상속을 막아놓고 있다. C++의 경우 다중 상속이 가능하지만, 구현해야 하는 클래스 또한 기하급수적으로 늘어나기 때문에 근본적인 해결책이 될 수 없다.
브릿지 패턴은 이러한 문제의 솔루션을 제공한다. 브릿치 패턴은 구현부(implementation)와 추상층(abstraction)을 분리하여 각각 독립적으로 변형할 수 있게 한다. 이는 두 개의 독립된 계층을 형성하고, 이 두 계층을 연결하는 브릿지 역할을 하는 객체를 통해 통신한다. 이로써 두 계층의 변형이 서로 영향을 주지 않으면서도 협력할 수 있게 된다. 브릿지 패턴을 구현하는 과정은 아래와 같다.
1) Implementor Interface (구현자 인터페이스): 추상화에서 정의한 인터페이스를 구현하는 클래스의 공통 메서드를 선언한다.
// Implementor: 구현 클래스에 대한 인터페이스
interface IColor
{
void ApplyColor();
}
2) Concrete Implementor Classes (구현자 구현 클래스): Implementor 인터페이스를 구현한 실제 클래스를 작성한다.
class Red : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying Red Color");
}
}
class Blue : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying Blue Color");
}
}
3) Abstraction Class (추상화 클래스): 구현자 인터페이스에 대한 참조자를 포함하고, 추상 메서드를 선언한다.
// Abstraction: 추상적 개념에 대한 인터페이스를 제공하고 객체 구현자에 대한 참조자를 관리
abstract class Shape
{
protected IColor color;
public Shape(IColor color)
{
this.color = color;
}
public abstract void Draw();
}
4) RefinedAbstraction Classes (확장된 추상화 클래스): 추상화를 확장하여 구체적인 기능을 추가한다.
class Circle : Shape
{
public Circle(IColor color) : base(color) { }
public override void Draw()
{
Console.Write("Drawing Circle - ");
color.ApplyColor();
}
}
class Rectangle : Shape
{
public Rectangle(IColor color) : base(color) { }
public override void Draw()
{
Console.Write("Drawing Rectangle - ");
color.ApplyColor();
}
}
Shape와 IColor를 추상화와 구현으로 나눔으로써 두 클래스간의 결합도가 낮아졌고, 두 클래스들을 각각 독립적으로 재사용할 수 있기 때문에 다양한 조합을 통해 추가적인 클래스 정의 없이 새로운 기능을 쉽게 추가할 수 있게 되었다.
//클라이언트 코드
class Client
{
static void Main()
{
IColor redColor = new Red();
IColor blueColor = new Blue();
//원하는 조합을 통해 새로운 기능들을 사용할 수 있다!
Shape redCircle = new Circle(redColor);
Shape blueRectangle = new Rectangle(blueColor);
redCircle.Draw(); //출력 : Drawing Circle - Applying red color
blueRectangle.Draw(); //출력 : Drawing Rectangle - Applying blue color
}
}
장점
- 새로운 추상화나 구현을 추가하더라도 다른 측면에 영향을 미치지 않고 시스템을 확장할 수 있다.
- 추상화와 구현이 서로 독립적으로 발전할 수 있기 때문에, 코드의 이해와 유지보수가 쉬워지고 클래스 간의 종속성이 감소한다.
- 추상화와 구현을 분리함으로써 두 부분을 독립적으로 재사용할 수 있다.
단점
- 브릿지 패턴은 구조적인 유연성을 제공하나, 동시에 클래스의 수가 증가하여 전반적인 시스템의 복잡성이 증가할 수 있다. 특히 작은 규모의 프로젝트나 간단한 상황에서는 이 추가적인 추상화 계층이 불필요하게 느껴질 수 있다.
추상 팩토리와 브릿지 패턴의 조합
추상 팩토리와 브릿지 패턴을 조합하여 사용하면, 복잡한 객체들을 생성하고 조합할 때 유용한 구조를 만들 수 있다. 이 두 디자인 패턴은 서로 다른 측면에서 시스템을 분리하고 확장 가능하게 만들어주기 때문에, 특히 대규모의 시스템에서 유용하게 적용될 수 있다.
1) 추상팩토리 구현
// Abstract Factory
interface IShapeFactory
{
IShape CreateShape();
IColor CreateColor();
}
// Concrete Factories
class CircleFactory : IShapeFactory
{
public IShape CreateShape()
{
return new Circle();
}
public IColor CreateColor()
{
return new Red();
}
}
class RectangleFactory : IShapeFactory
{
public IShape CreateShape()
{
return new Rectangle();
}
public IColor CreateColor()
{
return new Blue();
}
}
2) 브릿지 구현
// Bridge
interface IColor
{
void ApplyColor();
}
class Red : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying Red Color");
}
}
class Blue : IColor
{
public void ApplyColor()
{
Console.WriteLine("Applying Blue Color");
}
}
// Abstraction
abstract class Shape
{
protected IColor color;
public Shape(IColor color)
{
this.color = color;
}
public abstract void Draw();
}
// RefinedAbstraction
class Circle : Shape
{
public Circle(IColor color) : base(color) { }
public override void Draw()
{
Console.Write("Drawing Circle - ");
color.ApplyColor();
}
}
class Rectangle : Shape
{
public Rectangle(IColor color) : base(color) { }
public override void Draw()
{
Console.Write("Drawing Rectangle - ");
color.ApplyColor();
}
}
3) 두 패턴의 조합 사용
class Client
{
static void Main()
{
// Abstract Factory 사용
IShapeFactory circleFactory = new CircleFactory();
IShape circle = circleFactory.CreateShape();
IColor red = circleFactory.CreateColor();
circle.Draw(); //출력 :Drawing Circle - Applying Red Color
IShapeFactory rectangleFactory = new RectangleFactory();
IShape rectangle = rectangleFactory.CreateShape();
IColor blue = rectangleFactory.CreateColor();
rectangle.Draw(); //출력 : Drawing Rectangle - Applying Blue Color
}
}
이렇게 조합된 구조에서는 객체의 종류와 구현이 각각 추상화되어 있어, 새로운 도형이나 다른 색상을 추가하더라도 기존 코드를 수정하지 않고도 확장이 가능하다. 이는 시스템의 유연성과 확장성을 높일 수 있는 강력한 패턴의 조합이다.
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
09. [Design Pattern] 데코레이터(Decorator) 패턴 (1) | 2023.12.10 |
---|---|
08. [Design Pattern] 컴포지트(Composite) 패턴 (1) | 2023.11.25 |
06. [Design Pattern] 어댑터(Adapter) 패턴 (0) | 2023.11.21 |
05. [Design Pattern] 싱글톤(Singleton) 패턴 (0) | 2023.11.16 |
04. [Design Pattern] 프로토타입(Prototype) 패턴 (0) | 2023.11.07 |