Let us go protocol

Swift Protocol 적재적소에 사용하기

프로토콜 적재적소에 사용하기라는 주제로 let us: Go! 2017 Summer에서 발표된 내용입니다.


소개

여러 가지 주제 중에 프로토콜을 정한 이유는 WWDC 2015의 Introduction to Protocol에 말한 Swift는 첫 번째 Protocol 언어라는 강연에 영감을 받아서입니다. 당시 인턴이었던 저는 이 영상에 감명을 받아 모든 것을 프로토콜로 전환한 경험이 있습니다.

프로토콜이란?

Swift는 인터페이스와 비슷한 프로토콜을 사용합니다. 프로토콜은 함수/정수를 제공한다는 약속입니다. 예제와 함께 살펴볼까요?

class Pokemon {}
class FirePokemon: Pokemon {}
class Charizard: FirePokemon {}
  protocol 🚀 { }
  protocol 🔥 { }
  protocol 🐉 { }
  struct Charizard: 🚀, 🔥, 🐉 { }

정통적인 객체 지향이라면 Pokemon 클래스들을 각각 만들고 Charizard는 FirePokemon을 상속받을 겁니다. 하지만 기획이 바뀌어서 Charizard를 다른 포켓몬으로 바꾸려면 복잡하겠죠? 이때 프로토콜을 사용하면 간결하고 쓰기 쉬운 구조로 만들 수 있습니다.

이런 개발 뉴스를 더 만나보세요

protocol URLStringConvertable {
  var urlString: String { get }
  // ...
  
  Func sendRequest(urlString: URLStringConvertable, method: () -> ()) {
    let string = urlString.urlString
  }
}

위 코드에서 프로토콜을 사용하면서 얻는 이득은 뭘까요? 그냥 함수에 urlString 문자를 주는 것과 상관이 없어 보이지 않으신가요?

protocol NetworkAdaptable {
  func adapt(urlRequest: urlRequest) throws -> urlRequest
}

이 예제에서는 함수가 단지 하나밖에 없는데 왜 굳이 프로토콜을 만들었을까요?

Swift이니까 일단 프로토콜부터 만들어 보겠다고 시작하는 튜토리얼들이 많습니다. Swift에서 프로토콜을 사용하면 무조건 Swift스러운 코딩일까요? 예를 들어 UI를 만드는 간단한 라이브러리를 한번 만들어보겠습니다.

protocol HeaderViewProtocol {
  associatedtype Content
  func setHeader(data: Content)
}

class MyLabel: UILable, HeaderViewProtocol {
  func setHeader(data: String) {
    self.text = data
  }
}

class MyButton: UIButton, HeaderViewProtocol {
  func setHeader(data: String) {
    self.titleLable?.text = data
  }
}

class MyImageView: UIImageView, HeaderViewProtocol {
  func setHeader(data: UIImage) {
    self.image = data
  }
}

let elements = [MyLabel(), MyButton(), UIStackView()]

associatedtype으로 Swift에서 generic을 사용할 수 있게 하는 코드입니다. setHeader를 각각 세 개 구현했고 UI 뷰를 만들어 보겠습니다. 이 코드에 어떻게 setHeader가 없는 UIStackView를 넣을 수 있을까요? 배열이 UIView로 만들어지기 때문입니다. 현재는 에러가 발생하진 않지만 실행하면 에러가 발생할 겁니다. Swift 4부터는 프로토콜과 클래스를 사용해서 만들 수 있다고는 하지만 아직 Swift 3에서는 지원하지 않으므로 Type Eraser를 사용해야 합니다.

struct AnyHeaderView<Content>: HeaderViewProtocol {
  var _setHeader: (Content) -> ()
  
  init<T: HeaderViewProtocol>(_ view: T) where Content == T.Content {
    _setHeader = view.setHeader
  }
  
  func setHeader(data: Content) {
    return _setHeader(data)
  }
}

where Content == T.Content에서 타입 안정성을 보장해줍니다. 타입 안전성을 보장하지만, 코드가 많이 복잡해졌습니다.

프로토콜은 약속의 모임이므로 약속에 대한 증거 대신 약속을 직접 주는 방식은 어떨까요? associatedtype을 generic으로 바꾸고 함수를 직접 주는 방식으로 바꿔보겠습니다.

protocol HeaderViewProtocol {
  associatedtype Content
  func setHeader(data: Content)
}

struct HeaderView<T> {
  let view: UIView
  let setHeader: (T) -> ()
}

왼쪽 코드가 오른쪽처럼 바뀝니다. 좀 더 간편해진 게 보이시나요?

프로토콜 적용할 곳, 적용하지 말 곳

프로토콜 대신 함수를 사용하는 경우를 살펴보겠습니다. 먼저 프로토콜에 함수가 1개라면 함수로 만드는 것이 좋습니다. 예를 들어 setHeader의 경우 클로저로 넘겨주는 것이 더 깔끔한 코드가 될 수 있습니다. 대신 함수가 1개 이상이면 클로저를 두 세 개 쓰는 대신 프로토콜을 쓰는 것을 추천합니다.

빈도를 생각해보면 혹시 많이 사용하지 않는 경우라면 함수를 사용하시는 것이 좋습니다. 한편 데이터 소스나 델리게이트처럼 많이 쓰이는 경우 프로토콜을 사용하는 것이 좋겠습니다.

데모

HTTP GET을 하는 코드를 만들어 보겠습니다. struct를 사용한 Resource가 있는데 이를 프로토콜로 바꿔보겠습니다. 데모 영상을 확인해 주세요.

결론

struct에서 프로토콜로 바꾸는 과정을 보여드렸습니다. Swift 프로그래밍이라고 무조건 프로토콜을 사용하기보다는 프로토콜을 필요한 경우에 잘 쓴다면 좀 더 Swift스러운 코드를 만들 수 있을 것으로 생각합니다. 항상 문제에 대한 최적의 해결책을 선택하시는 것이 좋습니다.


본 영상과 글은 let us: Go!의 비디오 스폰서인 Realm에서 제공합니다. 모바일 개발자가 더 나은 앱을 더 빠르게 만들도록 돕는 Realm 모바일 데이터베이스Realm 모바일 플랫폼을 통해 핵심 로직에 집중하고 개발 효율을 높여 보세요! 공식 문서에서 단 몇 분 만에 시작할 수 있습니다. 또한 Realm 홈페이지에서는 모바일 개발자를 위한 다양한 최신 기술 뉴스와 튜토리얼을 제공하고 있으니 즐겨찾기하고 자주 들러 주세요!

다음: Realm Swift를 사용하면 iOS 앱 모델 레이어를 효과적으로 작성할 수 있습니다.

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

Joonsoo Choi