iOS & Swift

Swift 브릿지 패턴(Bridge Pattern), 서브클래스 폭발 막는 추상화 분리

Swift로 앱을 만들다 보면 클래스가 한없이 늘어나는 순간이 옵니다.

이석우iOS Developer6분 읽기
Swift 브릿지 패턴(Bridge Pattern), 서브클래스 폭발 막는 추상화 분리 대표 이미지

Swift로 앱을 만들다 보면 클래스가 한없이 늘어나는 순간이 옵니다.

버튼 하나 만들었을 뿐인데 라이트모드용, 다크모드용, iOS용, iPadOS용… 조합할수록 클래스 개수가 폭발하죠.

결론부터 말씀드릴게요. Swift 브릿지 패턴은 “무엇을 하는가(추상화)“와 “어떻게 하는가(구현)“를 별도의 클래스 계층으로 분리해, 두 축을 곱셈이 아니라 덧셈으로 관리하게 해주는 구조 패턴입니다.

이 글에서는 서브클래스 폭발이 왜 생기는지, 브릿지 패턴이 그걸 어떻게 막는지, 실제 Swift 코드까지 순서대로 짚어볼게요.

브릿지 패턴으로 바꾸니 클래스가 12개에서 7개로 줄었어요
브릿지 패턴으로 바꾸니 클래스가 12개에서 7개로 줄었어요

서브클래스 폭발은 왜 생길까요?

서브클래스 폭발은 서로 다른 두 가지 변화의 축을 상속 하나로만 표현할 때 생깁니다.

예를 들어 볼게요.

리모컨(Remote)이라는 클래스가 있고, 이 리모컨으로 TV와 라디오를 조작한다고 해봅시다.

여기에 “기본 리모컨”과 “고급 리모컨” 두 종류가 또 필요하다면요?

조합을 세보면 이렇게 됩니다.

  • 기본 리모컨 × TV
  • 기본 리모컨 × 라디오
  • 고급 리모컨 × TV
  • 고급 리모컨 × 라디오

벌써 4개예요.

여기서 기기에 “스피커”가 추가되면 6개, 리모컨에 “음성 리모컨”이 추가되면 9개로 불어납니다.

축이 2개일 때, 상속만 쓰면 클래스 수는 덧셈(m+n)이 아니라 곱셈(m×n)으로 늘어납니다.

이게 바로 서브클래스 폭발이에요. 축이 늘 때마다 관리 부담이 기하급수적으로 커지죠.


Swift 브릿지 패턴은 이걸 어떻게 막나요?

핵심은 딱 하나입니다.

변하는 두 축을 각각 독립된 계층으로 떼어내고 둘을 “참조(합성)“로 연결하는 거예요.

  • 추상화(Abstraction) 계층: 리모컨 종류 (기본/고급)
  • 구현(Implementation) 계층: 기기 종류 (TV/라디오)

리모컨은 기기를 상속하지 않습니다. 대신 기기 인터페이스를 프로퍼티로 “들고” 있어요.

먼저 구현 계층부터 프로토콜로 정의합니다. 기기가 할 줄 아는 최소 동작만 담아요.

// 구현 계층: 기기가 제공해야 할 최소 기능
protocol Device {
    var volume: Int { get set }
    func enable()
    func disable()
}

이제 추상화 계층인 리모컨은 이 Device를 참조만 합니다. 상속이 아니라 주입이에요.

// 추상화 계층: 기기를 '소유'하며 위임
class RemoteControl {
    let device: Device          // 브릿지 연결 지점
    init(device: Device) { self.device = device }
    func togglePower() { /* device.enable/disable 호출 */ }
}

device 프로퍼티가 바로 두 계층을 잇는 다리(Bridge)입니다.

리모컨은 기기를 상속 안 하고 들고만 있어요
리모컨은 기기를 상속 안 하고 들고만 있어요

실제로 확장해보면 어떻게 달라질까요?

제가 직접 코드를 늘려보면서 체감한 부분이에요.

고급 리모컨을 추가할 땐 RemoteControl을 상속해서 리모컨 계층에만 클래스를 하나 더 만듭니다.

// 추상화 확장: 기기 코드는 한 줄도 안 건드림
class AdvancedRemote: RemoteControl {
    func mute() {
        var d = device
        d.volume = 0        // 구현 계층에 위임
    }
}

반대로 새 기기(예: 스피커)를 추가할 땐 Device 프로토콜만 채택하면 끝이에요. 리모컨 코드는 그대로 재사용됩니다.

두 축이 완전히 독립돼서 한쪽을 늘려도 다른 쪽 조합을 새로 만들 필요가 없어요.

표로 비교하면 차이가 확 드러납니다.

구분 상속만 사용 브릿지 패턴
클래스 증가 방식 곱셈 (m×n) 덧셈 (m+n)
리모컨 3종 × 기기 4종 12개 7개
새 기기 추가 시 리모컨 종류만큼 추가 1개만 추가
런타임 교체 어려움 주입으로 자유롭게

리모컨 3종, 기기 4종 기준으로 12개가 7개로 줄어요. 축이 커질수록 격차는 더 벌어집니다.

곱셈으로 늘던 클래스가 다리 하나로 덧셈이 됩니다
곱셈으로 늘던 클래스가 다리 하나로 덧셈이 됩니다

언제 쓰고 언제 피해야 할까요?

모든 상황에 브릿지가 정답은 아니에요. 여기저기 남발하면 오히려 코드만 복잡해지기 쉽습니다.

이럴 때 잘 맞아요.

  1. 서로 독립적으로 변하는 축이 2개 이상 보일 때
  2. 런타임에 구현을 바꿔 끼우고 싶을 때 (예: 실제 API ↔ 목업)
  3. 플랫폼별·테마별 구현을 깔끔히 나누고 싶을 때

이럴 땐 굳이 안 써도 됩니다.

  • 축이 하나뿐이거나, 조합이 앞으로도 2~3개로 고정일 때
  • 구조가 단순한데 미리 계층부터 나눠 과설계가 될 때

Swift에서는 프로토콜과 의존성 주입이 워낙 자연스러워서, 사실 브릿지 패턴을 “이름만 몰랐지 이미 쓰고 있었네” 하는 경우도 많아요.


자주 묻는 질문 (Q&A)

Q. 어댑터 패턴이랑 뭐가 다른가요?

어댑터는 이미 만들어진 호환 안 되는 코드를 나중에 이어붙이는 목적이에요. 브릿지는 설계 단계에서 처음부터 두 축을 나눠두는 게 목적입니다.

Q. 꼭 protocol을 써야 하나요?

네, Swift에서는 구현 계층을 protocol로 정의하는 게 가장 자연스럽습니다. 추상 클래스 대신 프로토콜을 쓰면 값 타입(struct)도 구현체로 넣을 수 있어요.

Q. SwiftUI에서도 쓸 수 있나요?

가능합니다. 뷰가 데이터 소스 프로토콜을 주입받아 렌더링만 담당하게 하면, 그게 곧 브릿지 구조예요.

축이 두 개다 싶으면 이렇게 갈라놓는 게 편하더라고요
축이 두 개다 싶으면 이렇게 갈라놓는 게 편하더라고요

서브클래스가 자꾸 불어나서 스트레스받으셨다면, 오늘 딱 하나만 기억해 주세요.

“이 클래스가 변하는 이유가 두 가지구나” 싶을 때가 바로 두 축을 나눌 타이밍이에요.

작은 예제부터 손으로 옮겨보시면 생각보다 금방 손에 익습니다. 여러분의 코드가 한결 가벼워지길 응원할게요!

함께 보면 좋은 글