[디자인 패턴에 뛰어들기] 구조 패턴 #2 Bridge

devapploper·2024년 1월 26일
post-thumbnail

문제 상황

MVC (Model-View-Controller) 아키텍쳐로 개발하던 시절, UI 구현과 비즈니스 로직이 뷰컨트롤러에 몰리면서 Massive View Controller로 변해가는 모습은 아마 대부분의 iOS 개발자라면 경험해봤을 것이다.

클래스가 방대해지면 그 속에서 길을 잃기 쉽다. 뷰 코드와 비즈니스 로직이 뒤엉켜서 코드를 따라가는게 점점 어려워지며, 며칠만 코드를 안보면 금새 잊는다. 나중엔 작은 수정을 하고 싶을 때에도 수정 대비 많은 시간이 소모된다.

이때 뷰와 뷰모델로 나누면 약간은 해소된다는 것을 알고 있을 것이다. 물론 그 둘도 시간이 지나 또 massive해져서 같은 문제가 발생하기 전까지는 말이다. 여기서 흥미로운 사실은 뷰와 뷰모델로 나누는 것이 오늘 알아볼 브릿지 패턴과 매우 닮아있다는 점이다.

패턴 설명

브릿지는 방대한 하나의 클래스 혹은 연관된 클래스들을 두 계층, 즉 구현부와 추상부로 분리해서 각 계층의 독자적인 개발을 이어나갈 수 있게하게 하는 디자인 패턴이다.

구현부와 추상부가 무엇인지 이해하면 브릿지 패턴의 개념은 알아서 따라오니 간단히 살펴보자.

Abstraction

여기서 말하는 추상부 혹은 추상화(Abstraction)는 개체의 제어를 담당하는 상위 레벨 레이어를 가리킨다. 이 레이어는 직접 실제 작업(task)을 하지 않고, 실제 작업을 구현부에 위임하는 성격을 띈다.

간단한 예시로는 리모컨이 있다. 리모컨은 TV를 제어하고, 직접 일을 하기보다는 TV에게 일을 시킨다. 따라서 리모컨은 추상 레이어라고 할 수 있다.

Implementation

추상화를 보고나면 구현부 이해가 더욱 쉽다. 상위 레벨이 있으면 하위레벨이 있을 것이고, 하위 레벨이 바로 구현부다. 실제로 일이 일어나고 처리하는 영역이다. 상위 레벨로부터 일을 위임 받는다. 이 레이어는 플랫폼(platform), low level이라고도 불린다.

티비와 리모컨 예시에서 TV의 본체에 해당한다. 일을 직접하기 때문에 채널을 바꾸는 일이나, 볼륨을 켜고 줄이는 일이나, 전원을 켜고 끄는 일이나 모두 TV의 본체에서 일어난다. 그리고 리모컨으로부터 일을 위임 받는다. 따라서 TV는 구현부에 해당한다.

추상부와 구현부를 알아봤다. 그래서 추상부와 구현부는 이해가 되었는데 브릿지 패턴은 무엇이느냐. 하나의 클래스를 추상부과 구현부 둘로 나누면 브릿지 패턴이냐라고 묻는다면 반만 맞다.

하나의 클래스 혹은 연관된 클래스들을 추상부화 구현부 계층으로 나누고, 추상부에서 구현부를 레퍼런스하여 사용하는 것까지가 브릿지 패턴이라고 할 수 있다.

지금 보는 내용이 기억에 오래남길 바라며 UML 다이어그램으로도 한번 브릿지 패턴을 보자

추상부와 구현부 계층이 나뉘어져 있다. 추상부는 클래스로, 구현부는 인터페이스로 되어있다. 추상부는 구현부를 프로퍼티로 가지고 있으며, 그림엔 없지만 feature1()과 feature2() 메서드는 구현부에 접근해서 메서드를 호출한다.

장점

  • 한 개체를 두 계층으로 나누어 모듈화한 덕분에 수정사항의 영향범위가 명확하게 나뉜다.
  • 단일 책임 원칙에 부합한다.
    • 추상부에서는 상위 레벨의 로직에 집중하고 구현부에서는 하위 레벨의 로직에 집중할 수 있다.
  • 개방-폐쇄 원칙에 부합한다.
    • 확장에는 열려있고 수정에는 닫혀있도록, 추상부 혹은 구현부에 구현을 추가해도 다른 한쪽이 영향을 받지 않는다. 즉, 독자적인 (independent한) 개발 지속이 가능한 구조다

단점

  • 결합도가 매우 높은 코드의 경우 이를 분리하면서 결과적으로 복잡도를 증가시킬 수 있다.

예시 코드

class RemoteControl {
    private let device: Device
    init(device: Device) {
        self.device = device
    }
    
    func togglePower() {
        device.isEnabled() ? device.disable() : device.enable()
    }
    
    func volumeDown() {
        device.setVolume(device.getVolume() - 10)
    }
    
    func volumeUp() {
        device.setVolume(device.getVolume() + 10)
    }
    
    func channelDown() {
        device.setChannel(device.getChannel() - 1)
    }
    
    func channelUp() {
        device.setChannel(device.getChannel() + 1)
    }
}

// 구현부에 수정 없이도 추상부만 따로 확장이 가능하다. 구현부와 독립적으로 추상부를 확장할 수 있다.
class AdvancedRemoteControl: RemoteControl {
    func mute() {
        device.setVolume(0)
    }
}

// 보통 구현부는 원시적인 하위 레벨 작업을 인터페이스로 제공한다. 
// 추상부는 구현부의 하위 레벨 인터페이스를 토대로 상위 레벨 작업을 정의한다.
protocol Device {
    func isEnabled() -> Bool
    func enable()
    func disable()
    func getVolume() -> Int
    func setVolume(_ percent: Int)
    func getChannel() -> Int
    func setChannel(_ channel: Int)
}

class TV: Device {
   // ...
}

class Radio: Device {
   // ...
}

// Somewhere in client code:

let tv = TV()
let remote = RemoteControl(device: tv)
remote.togglePower()

let radio = Radio()
let advRemote = AdvancedRemoteControl(device: radio)
advRemote.mute()

잡담

브릿지, 즉 다리라고 해서 일반적인 다리에 대한 개념처럼, 분리된 무언가를 연결하는 것에 초점을 맞춘 디자인 패턴일 것이라 생각했다. 그런데 알고보니 오히려 분리하는 것에 초점을 맞춘 디자인 패턴인 것을 알았을 땐 약간 아이러니하다는 생각이 들었다.

profile
iOS, 알고리즘, 컴퓨터공학에 관련 포스트를 정리해봅니다

0개의 댓글