[Swift 디자인 패턴] Bridge Pattern (브리지) - 디자인 패턴 공부 8
안녕하세요 Pingu입니다.🐧
지난 글에서는 구조 패턴 중 Adapter Pattern(어댑터)에 대해 알아봤는데요, 이번 글에서는 또 다른 구조 패턴 중 하나인 Bridge Pattern(브리지)에 대해 알아보도록 하겠습니다.
브리지 패턴이란?
브리지 패턴은 큰 클래스나 밀접하게 관련된 클래스 집합을 구현 계층과 추상 계층으로 분할할 수 있는 디자인 패턴입니다.
추상화를 구현하는 일반적인 방법은 상속을 사용하는 것입니다. 추상 클래스는 추상화에 대한 인터페이스를 정의하고 이를 상속한 서브 클래스들은 이를 구현하게 되는데요, 이러한 방법은 서브 클래스가 영구적으로 추상 클래스를 바인딩하므로 추상화와 구현을 독립적으로 사용하기 어렵습니다.
그럼 추상화와 구현이라는 용어를 이해해보겠습니다.
추상화는 interface라고도 하며 이는 실제로 작업을 수행하지는 않습니다. 실제 작업은 구현 계층에서 할 수 있도록 위임해야 하며 이는 프로그래밍 언어에서 추상 클래스를 말하는 것은 아닙니다. 반대로 구현은 실제 작업을 수행하는 부분이라고 할 수 있습니다.
- Abstraction
- 추상화의 인터페이스를 정의합니다.
- Implementor 타입의 객체에 대한 참조를 유지합니다.
- Refined Abstraction
- Abstraction에서 정의한 인터페이스를 확장합니다.
- Implementor
- 구현 클래스의 인터페이스를 정의합니다. 여기서 정의한 인터페이스는 Abstraction의 인터페이스와 일치할 필요는 없고 다를 수 있습니다.
- Concrete Implementor
- Implementor 인터페이스의 구체적으로 구현합니다.
브리지 패턴은 언제 쓰나요?
Shape라는 클래스가 있고 Shape의 하위 클래스로 Circle, Square가 있는 예를 들어볼게요. 현재 클래스 계층구조를 확장하여 Color를 갖는 Shape 클래스를 만들고 싶다고 가정해보겠습니다. 파란색 Circle, Square를 만들고 싶다고 할 때 각각 BlueCircle, BlueSquare 클래스를 만들게 되면 어떻게 될까요? 매번 색을 하나씩 추가할 때마다 너무 많은 서브 클래스가 생겨나게 됩니다.
이러한 문제는 Shape와 Color 클래스의 개념을 독립적으로 확장하려고 했기 때문에 발생한 일인데요, 브리지 패턴은 이러한 문제를 객체 구성으로 전환하여 해결합니다. Color 클래스 객체와 Shape 클래스 객체를 나누어 생각하는 거예요. 만약 Color 클래스 객체로 Blue, Red를 만들었다면 Shape 클래스의 객체가 이를 참조하여 사용하게 할 수 있으며 이러한 참조가 Bridge 역할을 하게 됩니다.
브리지 패턴의 결과
- 종속성이 없는 인터페이스와 구현부를 가질 수 있습니다. 추상화의 구현은 런타임에서 구성할 수 있기 때문에 객체가 런타임에서 구현부를 변경할 수도 있습니다. 이렇게 추상화와 구현을 분리하면 구현에 대한 컴파일 시간 종속성도 제거되며 구현 클래스를 변경하기 위해 Abstraction 클래스와 해당 클라이언트를 다시 컴파일할 필요가 없어집니다.
- 코드의 확장성이 좋아집니다.
- 클라이언트로부터 구현의 세부 사항을 숨길 수 있습니다. 구현 클래스의 객체 공유 및 참조 카운트 메커니즘과 같은 세부 정보로부터 클라이언트를 보호할 수 있습니다.
예제
그럼 Swift로 브리지 패턴을 간단하게 구현해보도록 하겠습니다.
사운드 버튼이 있고 여러 가지 전자기기에서 사용할 수 있도록 해주는 것을 브리지 패턴으로 만들어보겠습니다.
만약 전자기기가 TV, Radio, SmartPhone이라고 할 때 별다른 패턴을 사용하지 않고 만든다면 매번 어떤 전자기기인지 확인해주는 조건문이 필요하게 되어 코드가 지저분해 보일 거예요. 이를 브리지 패턴으로 해결해보겠습니다.
브리지 패턴으로 해결하기 위해 Abstraction은 사운드 버튼, Implementor는 전자기기별 API로 나누어 생각하겠습니다. 이렇게 하면 동일한 사운드 버튼이 여러 개의 전자기기에서 동작하도록 만들 수 있습니다. 또한 Implementor 클래스를 수정하지 않고도 Abstraction 클래스를 수정할 수 있게 됩니다.
그럼 Abstraction에 해당하는 SoundButton과 Implementor에 해당하는 SoundButtonImplementor 인터페이스를 먼저 만들어 보겠습니다.
// Abstraction
class SoundButton {
// Bridge
var soundButtonImplementor: SoundButtonImplementor?
init(implementor: SoundButtonImplementor) {
self.soundButtonImplementor = implementor
}
func up() {
self.soundButtonImplementor?.volumeUp()
}
func down() {
self.soundButtonImplementor?.volumeDown()
}
func set(percent: Float) {
self.soundButtonImplementor?.setVolume(percent: percent)
}
}
// Implementor
protocol SoundButtonImplementor {
var volume: Float { get set }
func volumeUp()
func volumeDown()
func setVolume(percent: Float)
}
이렇게 만든 것을 가지고 Tv, Radio에서 사용 가능한 SoundButton을 만들면 됩니다.
// Concrete Implementor
class TvSoundButton: SoundButtonImplementor {
var volume: Float = 0
func volumeUp() {
if volume >= 1.0 {
print("TV 볼륨이 최대값입니다.")
} else {
volume = min(volume + 0.1, 1.0)
print("TV 볼륨 \(volume)으로 올림")
}
}
func volumeDown() {
if volume <= 0.0 {
print("TV 볼륨이 최저값입니다.")
} else {
volume = max(volume - 0.1, 0.0)
print("TV 볼륨 \(volume)으로 내림")
}
}
func setVolume(percent: Float) {
if percent < 0.0 || percent > 1.0 {
print("잘못된 TV 볼륨 값")
} else {
volume = percent
print("TV 볼륨 \(volume)으로 세팅")
}
}
}
// Concrete Implementor
class RadioSoundButton: SoundButtonImplementor {
var volume: Float = 0
func volumeUp() {
if volume >= 1.0 {
print("Radio 볼륨이 최대값입니다.")
} else {
volume = min(volume + 0.1, 1.0)
print("Radio 볼륨 \(volume)으로 올림")
}
}
func volumeDown() {
if volume <= 0.0 {
print("Radio 볼륨이 최저값입니다.")
} else {
volume = max(volume - 0.1, 0.0)
print("Radio 볼륨 \(volume)으로 내림")
}
}
func setVolume(percent: Float) {
if percent < 0.0 || percent > 1.0 {
print("잘못된 Radio 볼륨 값")
} else {
volume = percent
print("Radio 볼륨 \(volume)으로 세팅")
}
}
}
이렇게 만든 SoundButton들은 아래와 같이 사용할 수 있습니다.
근데 이제 이렇게 만든 SoundButton에 mute기능을 추가하고 싶습니다.
이렇게 추가 기능을 확장할 때 만들어야 하는 것이 Refined Abstraction입니다.
// Refined Abstraction
class AdvancedSoundButton: SoundButton {
func mute() {
self.soundButtonImplementor?.setVolume(percent: 0.0)
}
}
위와 같이 추가 기능을 구현부에 직접 만드는 것이 아닌 Abstraction에 만드는 거예요.
이렇게 되면 실제 기능을 추가할 때 모든 구현부에 적용하지 않고 위와 같이 Abstraction에만 추가하면 됩니다.
물론 Swift에서는 extension을 활용해도 될 것 같아요.
// Refined Abstraction
extension SoundButton {
func mute() {
self.soundButtonImplementor?.setVolume(percent: 0.0)
}
}
그렇게 되면 위와 같이 mute 기능이 추가된 soundButton을 사용할 수 있게 됩니다.
이렇게 구조 패턴 중 하나인 브리지 패턴을 알아보고 간단하게 직접 구현도 해봤습니다.
혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다.
전체 코드는 여기에서 볼 수 있습니다.
감사합니다.