Bridge
Bridge Pattern이란?
브리지 패턴Bridge Pattern은 소프트웨어 설계에서 구현부와 추상부를 분리하여 독립적으로 확장할 수 있도록 하는 구조적 디자인 패턴입니다. 이 패턴은 클래스 계층의 확장성을 높이고, 기능의 확장과 구현의 확장을 독립적으로 처리할 수 있게 해줍니다. 이는 시스템의 복잡성을 줄이고, 유지보수를 쉽게 하며, 다양한 구현체와 기능을 유연하게 결합할 수 있는 장점을 제공합니다. 브리지 패턴이라는 이름은 패턴이 “추상부와 구현부를 연결하는 다리bridge” 역할을 하기 때문에 붙여졌습니다. 이 패턴에서 “브리지"는 두 가지 주요 요소를 분리하면서도 연결하는 중요한 역할을 합니다.
Bridge Pattern의 구조
- Abstraction → Implementor
Abstraction
은Implementor
의 인스턴스를 참조하고 이를 통해 작업을 위임합니다.- Abstraction은 Implementor의
Operation()
메소드를 호출하여 실제 작업을 수행합니다.
- RefinedAbstraction → Abstraction
RefinedAbstraction
은Abstraction
을 상속하여 구체적인 기능을 제공합니다.
- ConcreteImplementorA / ConcreteImplementorB → Implementor
- 각각
Implementor
인터페이스를 구현하여, 특정 구현을 제공합니다.
- 각각
Bridge Pattern의 구성 요소
브리지 패턴은 다음과 같은 구성 요소로 이루어집니다:
추상부Abstraction
기능의 기본 인터페이스를 정의합니다. 추상부는 구현부에 대한 참조를 가지고 있으며, 기능을 정의하는 메서드를 포함합니다.
확장 추상부Refined Abstraction
추상부를 확장하여 더 구체적인 기능을 제공합니다. 이 클래스는 추상부의 인터페이스를 통해 구현부와 상호작용합니다.
구현부Implementor
추상부와 독립적으로 변형 가능한 구현을 정의하는 인터페이스입니다. 일반적으로 기본적인 연산을 정의하며, 추상부에서 이 인터페이스를 통해 기능을 호출합니다.
구체적인 구현부Concrete Implementor
구현부의 인터페이스를 실제로 구현하는 클래스입니다. 이 클래스는 플랫폼에 종속적인 코드나 특정 구현을 포함할 수 있습니다.
Bridge Pattern 적용
Bridge Pattern의 필요성
브리지 패턴은 일반적으로 두 가지 상황에서 필요합니다:
복잡한 클래스 계층의 단순화
기능의 다양성과 구현의 다양성을 모두 고려할 때, 클래스 계층이 매우 복잡해질 수 있습니다. 예를 들어, 여러 기능을 가진 다양한 디바이스를 구현해야 한다면, 모든 조합을 고려해야 하기 때문에 클래스 계층이 급격히 복잡해집니다.
플랫폼 독립성 확보
특정 기능을 여러 플랫폼에서 구현해야 하는 경우, 기능과 플랫폼을 분리하여 독립적으로 확장할 수 있게 합니다. 이를 통해 하나의 기능을 여러 플랫폼에서 쉽게 구현할 수 있습니다.
잘못된 클래스 계층 방식
브리지 패턴을 적용하지 않은 경우, 다양한 기능과 구현을 조합하는 클래스 계층이 매우 복잡해질 수 있습니다. 예를 들어, 도서관 관리 시스템에서 여러 종류의 도서에 대해 다양한 포맷을 제공하는 경우를 생각해봅시다.
// 다양한 도서와 포맷을 클래스로 구현 (비효율적)
public class PrintBook
{
public void Read()
{
Console.WriteLine("Reading a printed book.");
}
}
public class EBook
{
public void Read()
{
Console.WriteLine("Reading an e-book.");
}
}
public class AudioBook
{
public void Listen()
{
Console.WriteLine("Listening to an audiobook.");
}
}
클래스 폭발 문제
다양한 도서 형식과 포맷을 각각의 클래스로 표현하려면, 모든 조합을 커버해야 하므로 클래스의 수가 급격히 증가합니다.
유연성 부족
새로운 도서 형식이나 포맷을 추가할 때마다 기존 클래스를 수정하거나 새로운 클래스를 생성해야 합니다.
Bridge Pattern 적용 예시
브리지 패턴을 사용하면 기능과 구현을 분리하여, 서로 독립적으로 확장할 수 있습니다.
// 구현부 인터페이스
public interface IFormat
{
void Display(string title);
}
// 구체적인 구현부 클래스
public class PrintFormat : IFormat
{
public void Display(string title)
=> Console.WriteLine($"Displaying the '{title}'(printed).");
}
public class EBookFormat : IFormat
{
public void Display(string title)
=> Console.WriteLine($"Displaying the '{title}'(e-book).");
}
public class AudioBookFormat : IFormat
{
public void Display(string title)
=> Console.WriteLine($"Playing the '{title}'(audiobook).");
}
// 추상부 클래스
public abstract class Book
{
protected IFormat format;
protected Book(IFormat format)
{
this.format = format;
}
public abstract void Show();
}
// 확장 추상부 클래스
public class Novel : Book
{
private string _title;
public Novel(string title, IFormat format) : base(format)
{
_title = title;
}
public override void Show() => format.Display(_title);
}
public class Program
{
public static void Main(string[] args)
{
Book novelInPrint = new Novel("Design Patterns", new PrintFormat());
novelInPrint.Show();
Book novelAsEbook = new Novel("Design Patterns", new EBookFormat());
novelAsEbook.Show();
Book novelAsAudiobook = new Novel("Design Patterns", new AudioBookFormat());
novelAsAudiobook.Show();
}
}
IFormat 인터페이스
구현부의 인터페이스로, 책의 포맷에 대한 기본적인 Display
메서드를 정의합니다.
PrintFormat, EBookFormat, AudioBookFormat 클래스
IFormat 인터페이스를 구현한 구체적인 구현부입니다. 각 클래스는 책의 포맷에 따른 구체적인 기능을 제공합니다.
Book 클래스
추상부로서, IFormat 인터페이스를 참조하여 책의 내용을 표시하는 기능을 제공합니다.
Novel 클래스
추상부를 확장한 클래스로, 구체적인 책의 형식에 따라 포맷을 선택하여 출력합니다.
Bridge Pattern 장단점
장점
확장성 증가
기능과 구현을 분리하여, 각각 독립적으로 확장할 수 있습니다. 새로운 기능이나 구현이 추가될 때 기존 코드를 수정할 필요가 없습니다.
복잡성 감소
기능과 구현의 조합으로 인한 클래스 폭발 문제를 방지합니다. 브리지 패턴을 사용하면 클래스 계층이 더 간결해집니다.
플랫폼 독립성
브리지 패턴은 플랫폼 독립적인 인터페이스를 제공하여, 다양한 플랫폼에서 동일한 기능을 구현할 수 있게 해줍니다.
단점
구조의 복잡성
브리지 패턴을 사용하면 코드 구조가 다소 복잡해질 수 있습니다. 특히, 구현부와 추상부를 명확히 구분해야 하기 때문에 설계 단계에서 신중한 접근이 필요합니다.
초기 설계 비용 증가
브리지 패턴을 적용하려면 초기 설계 단계에서 기능과 구현을 명확히 구분해야 하므로, 초기 설계 비용이 증가할 수 있습니다. 이로 인해 단순한 문제에 적용할 경우 과도한 설계가 될 수 있습니다.
단점 해결 방안
초기 설계 단계에서 명확한 요구 사항 분석
초기 설계 단계에서 요구 사항을 명확히 분석하고, 기능과 구현을 독립적으로 관리해야 하는 필요성이 높은 경우에만 브리지 패턴을 적용합니다. 이를 통해 구조적 복잡성을 최소화할 수 있습니다. 기능과 구현의 확장 가능성이 크지 않다면, 브리지 패턴을 과도하게 적용하지 않는 것이 좋습니다. 단순한 경우에는 기존의 상속 구조를 사용하는 것이 더 효율적일 수 있습니다.
추상화와 구현의 명확한 분리
추상화와 구현을 명확히 분리하여, 각 클래스가 맡은 역할을 명확히 정의합니다. 이를 통해 코드의 가독성을 높이고, 유지보수성을 향상시킬 수 있습니다.
객체지향 원칙과의 관계
캡슐화
브리지 패턴은 구현부와 추상부를 분리하여, 각 요소의 내부 구현을 캡슐화합니다. 클라이언트는 추상부의 인터페이스를 통해 구현부와 상호작용하며, 구현부의 세부 사항은 숨겨집니다.
Bridge와 다형성
브리지 패턴은 다형성을 적극 활용하여, 동일한 인터페이스를 통해 다양한 구현을 사용할 수 있도록 합니다.
Bridge와 단일 책임 원칙
브리지 패턴은 기능과 구현을 분리하여 각각의 책임을 명확히 합니다. 추상부는 기능적인 책임을, 구현부는 구체적인 구현 책임을 가집니다.
Bridge와 개방_폐쇄 원칙
브리지 패턴은 기존 코드의 변경 없이 새로운 기능과 구현을 추가할 수 있도록 해줍니다. 새로운 기능이나 구현을 추가할 때, 기존 코드를 수정하지 않고도 확장할 수 있으므로 OCP를 잘 준수합니다.
맺음말
브리지 패턴은 구현과 추상화를 분리하여, 시스템의 확장성과 유지보수성을 높이는 데 매우 유용한 패턴입니다. 특히, 기능과 구현이 독립적으로 변화할 가능성이 큰 경우 이 패턴을 적용하면 클래스 계층의 복잡성을 줄이고, 코드의 유연성을 극대화할 수 있습니다. 다만, 단순한 문제에 이 패턴을 적용하면 오히려 설계가 복잡해질 수 있으므로, 상황에 맞게 신중하게 적용해야 합니다.
심화학습
Bridge와 Multi Bridge 적용
예시: 다중 브리지를 사용한 도서 형식과 출력 형식, 그리고 권한 관리
도서 관리 시스템에서 도서 형식(전자책, 종이책)과 출력 형식(텍스트 출력, 이미지 출력)을 분리할 뿐만 아니라, 사용자 권한에 따라 접근 가능한 형식도 제어해야 한다고 가정해보겠습니다. 이를 위해, 브리지 패턴을 사용하여 출력 형식과 권한 관리를 분리하고 독립적으로 확장할 수 있습니다.
// 출력 형식 인터페이스
public interface IOutputFormat
{
void Print(string content);
}
// 권한 관리 인터페이스
public interface IAccessControl
{
bool HasAccess();
}
// 구체적인 출력 형식: 텍스트 출력
public class TextOutput : IOutputFormat
{
public void Print(string content)
=> Console.WriteLine($"Printing as text: {content}");
}
// 구체적인 출력 형식: 이미지 출력
public class ImageOutput : IOutputFormat
{
public void Print(string content)
=> Console.WriteLine($"Printing as image: [Image of {content}]");
}
// 구체적인 권한 관리: 관리자 권한
public class AdminAccess : IAccessControl
{
public bool HasAccess() => return true; // 관리자 권한은 항상 접근 가능
}
// 구체적인 권한 관리: 일반 사용자 권한
public class UserAccess : IAccessControl
{
public bool HasAccess() => return false; // 일반 사용자는 접근 불가능
}
// 도서 형식 추상 클래스
public abstract class BookFormat
{
protected IOutputFormat outputFormat;
protected IAccessControl accessControl;
protected BookFormat(IOutputFormat outputFormat, IAccessControl accessControl)
{
this.outputFormat = outputFormat;
this.accessControl = accessControl;
}
public void PrintBook()
{
if (accessControl.HasAccess())
{
outputFormat.Print(GetBookDetails());
}
else
{
Console.WriteLine("Access denied.");
}
}
protected abstract string GetBookDetails();
}
// 구체적인 도서 형식: 전자책
public class EBook : BookFormat
{
private string _title;
public EBook(string title, IOutputFormat outputFormat, IAccessControl accessControl)
: base(outputFormat, accessControl)
{
_title = title;
}
protected override string GetBookDetails() => $"E-Book: {_title}";
}
// 구체적인 도서 형식: 종이책
public class PaperBook : BookFormat
{
private string _title;
public PaperBook(string title, IOutputFormat outputFormat, IAccessControl accessControl)
: base(outputFormat, accessControl)
{
_title = title;
}
protected override string GetBookDetails() => $"Paper Book: {_title}";
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 관리자 권한으로 전자책 텍스트 출력
IOutputFormat textOutput = new TextOutput();
IAccessControl adminAccess = new AdminAccess();
BookFormat ebookTextAdmin = new EBook("Design Patterns", textOutput, adminAccess);
ebookTextAdmin.PrintBook();
// 일반 사용자 권한으로 종이책 이미지 출력
IOutputFormat imageOutput = new ImageOutput();
IAccessControl userAccess = new UserAccess();
BookFormat paperBookImageUser = new PaperBook("Clean Code", imageOutput, userAccess);
paperBookImageUser.PrintBook();
}
}
- IAccessControl 인터페이스는 도서 접근 권한을 제어합니다.
- BookFormat 클래스는 권한이 있는 경우에만 책을 출력할 수 있도록 제어합니다.
- EBook 및 PaperBook 클래스는 각각 전자책과 종이책을 나타냅니다
- 추상 클래스
BookFormat
은IOutputFormat
인터페이스에 의존하며, 도서의 구체적인 출력 형식에 대한 구현을 외부로 위임합니다. 이 구조 덕분에 출력 형식을 새로 추가하거나 변경할 때BookFormat
클래스 자체를 수정할 필요가 없습니다. 이 구조를 통해 도서 형식, 출력 형식, 그리고 권한 관리를 독립적으로 확장할 수 있으며, 이러한 기능들이 서로 간섭하지 않도록 잘 분리할 수 있습니다.
Bridge의 유연성 극대화 방법
예시: 다양한 사용자 인터페이스와 도서 형식의 결합
도서 관리 시스템에서 여러 종류의 사용자 인터페이스(예: 웹, 모바일 앱, 터치스크린 키오스크)와 도서 형식(전자책, 오디오북, 종이책)을 결합해야 한다면, 브리지 패턴을 사용하여 UI와 도서 형식을 독립적으로 관리할 수 있습니다.
// 사용자 인터페이스 인터페이스
public interface IUserInterface
{
void Display(string content);
}
// 구체적인 UI 구현: 웹 UI
public class WebInterface : IUserInterface
{
public void Display(string content)
=> Console.WriteLine($"Displaying on Web: {content}");
}
// 구체적인 UI 구현: 모바일 앱 UI
public class MobileInterface : IUserInterface
{
public void Display(string content)
=> Console.WriteLine($"Displaying on Mobile: {content}");
}
// 도서 형식 추상 클래스
public abstract class BookFormat
{
protected IUserInterface user;
protected BookFormat(IUserInterface user)
{
this.user = user;
}
public abstract void ShowBook();
}
// 구체적인 도서 형식: 전자책
public class EBook : BookFormat
{
private string _title;
public EBook(string title, IUserInterface user) : base(user)
{
_title = title;
}
public override void ShowBook() => user.Display($"E-Book: {_title}");
}
// 구체적인 도서 형식: 오디오북
public class AudioBook : BookFormat
{
private string _title;
public AudioBook(string title, IUserInterface user) : base(user)
{
_title = title;
}
public override void ShowBook() => user.Display($"Audio Book: {_title}");
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 웹 UI에서 전자책 보여주기
IUserInterface webUI = new WebInterface();
BookFormat ebookWeb = new EBook("Effective Java", webUI);
ebookWeb.ShowBook();
// 모바일 UI에서 오디오북 보여주기
IUserInterface mobileUI = new MobileInterface();
BookFormat audioBookMobile = new AudioBook("The Art of Computer Programming", mobileUI);
audioBookMobile.ShowBook();
}
}
- IUserInterface 인터페이스는 도서를 표시할 사용자 인터페이스를 정의합니다.
- EBook 및 AudioBook 클래스는 각각 전자책과 오디오북을 나타냅니다.
- WebInterface 및 MobileInterface 클래스는 각각 웹과 모바일 앱에서 도서를 표시합니다. 이 예시는 새로운 도서 형식이나 UI 유형이 추가될 때마다 기존 코드를 수정하지 않고 새로운 기능을 쉽게 추가할 수 있도록 합니다. 브리지 패턴을 사용하여 도서 형식과 사용자 인터페이스 간의 결합도를 낮추고, 시스템의 유연성을 극대화할 수 있습니다.
조건부 Adapter와 Bridge
예시: 특정 형식의 도서만 허용하는 조건부 어댑터
도서 관리 시스템에서 특정 사용자(예: 어린이)에게는 특정 형식의 도서(예: 만화책)만 표시하고 다른 형식은 숨기는 기능을 구현할 수 있습니다. 이 기능은 어댑터 패턴을 사용하여 구현할 수 있으며, 브리지 패턴과 결합하여 더욱 유연하게 관리할 수 있습니다.
// 어댑터 인터페이스
public interface IBookAdapter
{
bool CanDisplay();
void DisplayBook();
}
// 구체적인 어댑터: 어린이 사용자에게 적합한 도서 필터링
public class ChildrenBookAdapter : IBookAdapter
{
private readonly BookFormat _book;
private readonly string _allowedType = "Comic";
public ChildrenBookAdapter(BookFormat book)
{
_book = book;
}
public bool CanDisplay()
{
// 특정 형식의 도서만 허용
return _book.GetType().Name == _allowedType;
}
public void DisplayBook()
{
if (CanDisplay())
{
_book.ShowBook();
}
else
{
Console.WriteLine("This book is not suitable for children.");
}
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
IUserInterface mobileUI = new MobileInterface();
// 만화책 보여주기 (어린이에게 허용됨)
BookFormat comicBook = new EBook("Spider-Man", mobileUI);
IBookAdapter childrenAdapter = new ChildrenBookAdapter(comicBook);
childrenAdapter.DisplayBook();
// 소설책 보여주기 (어린이에게 비허용)
BookFormat novelBook = new AudioBook("The Catcher in the Rye", mobileUI);
IBookAdapter novelAdapter = new ChildrenBookAdapter(novelBook);
novelAdapter.DisplayBook();
}
}
설명:
- IBookAdapter 인터페이스는 특정 도서가 표시 가능한지 여부를 결정하고, 표시하는 메서드를 정의합니다.
- ChildrenBookAdapter 클래스는 만화책만 어린이에게 표시할 수 있도록 필터링합니다.
- CanDisplay() 메서드는 조건부로 도서를 표시할지 여부를 결정합니다. 이러한 구조는 브리지 패턴과 어댑터 패턴을 결합하여 특정 사용자 요구사항에 맞게 도서 형식을 필터링하는 유연한 방식입니다.
Bridge와 Factory
팩토리 패턴을 사용하여 도서 형식(전자책, 종이책)을 생성하고, 브리지 패턴을 사용해 도서 형식과 출력 형식을 결합하여 확장 가능한 구조를 구축합니다.
// 출력 형식 인터페이스
public interface IOutputFormat
{
void Print(string content);
}
// 구체적인 출력 형식: 텍스트 출력
public class TextOutput : IOutputFormat
{
public void Print(string content)
{
Console.WriteLine($"Printing as text: {content}");
}
}
// 구체적인 출력 형식: 이미지 출력
public class ImageOutput : IOutputFormat
{
public void Print(string content)
{
Console.WriteLine($"Printing as image: [Image of {content}]");
}
}
// 도서 형식 추상 클래스
public abstract class BookFormat
{
protected IOutputFormat outputFormat;
protected BookFormat(IOutputFormat outputFormat)
{
this.outputFormat = outputFormat;
}
public abstract void PrintBook();
}
// 구체적인 도서 형식: 전자책
public class EBook : BookFormat
{
private string _title;
public EBook(string title, IOutputFormat outputFormat) : base(outputFormat)
{
_title = title;
}
public override void PrintBook()
=> outputFormat.Print($"E-Book: {_title}");
}
// 구체적인 도서 형식: 종이책
public class PaperBook : BookFormat
{
private string _title;
public PaperBook(string title, IOutputFormat outputFormat) : base(outputFormat)
{
_title = title;
}
public override void PrintBook()
=> outputFormat.Print($"Paper Book: {_title}");
}
// 팩토리 클래스: 도서 형식 생성
public class BookFactory
{
public static BookFormat CreateBook(string type, string title, IOutputFormat outputFormat)
{
if (type == "EBook")
{
return new EBook(title, outputFormat);
}
else if (type == "PaperBook")
{
return new PaperBook(title, outputFormat);
}
throw new ArgumentException("Invalid book type");
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 팩토리 패턴을 통해 도서 형식 생성
IOutputFormat textOutput = new TextOutput();
BookFormat ebook = BookFactory.CreateBook("EBook", "Clean Code", textOutput);
ebook.PrintBook();
// 팩토리 패턴을 통해 종이책 생성 및 이미지 출력 형식 적용
IOutputFormat imageOutput = new ImageOutput();
BookFormat paperBook = BookFactory.CreateBook("PaperBook", "The Pragmatic Programmer", imageOutput);
paperBook.PrintBook();
}
}
- 팩토리 패턴은 클라이언트 코드에서 도서 형식을 생성하는 책임을 캡슐화합니다.
BookFactory
는 클라이언트가 전자책 또는 종이책을 생성할 수 있게 합니다. - 브리지 패턴은 팩토리에서 생성된 도서 형식과 출력 형식을 결합하여, 서로 독립적으로 변경 및 확장할 수 있게 만듭니다.
- 팩토리를 통해 도서 형식을 쉽게 확장할 수 있으며, 브리지 패턴을 통해 출력 형식도 독립적으로 확장할 수 있습니다.