프로토콜 지향언어, Swift

1 답변 글타래를 보이고 있습니다
  • 글쓴이
    • HyunJi
      참가자
      • 글작성 : 9
      • 답글작성 : 10

      Swift프로토콜 지향 언어이다.
      프로토콜 지향 방식으로 애플리케이션을 설계하는 것은 객체지향 방식으로 설계하는 것과 상당한 차이가 있다.
      프로토콜 지향 설계에서는 클래스 계층 구조로 시작하는 대신 프로토콜로 시작해야 한다.
      프로토콜은 클래스나 구조체가 어떤 기준을 만족하거나 또는 특수한 목적을 달성하기 위해 구현해야 하는 메소드와 프로퍼티의 목록이다. (객체지향에서의 인터페이스와 거의 비슷하다.)
      iOS는 특정 컨트롤에서 발생하는 각종 이벤트를 효율적으로 관리하기 위해 대리자(delegate)를 지정하여 이벤트 처리를 위임하고, 실제로 이벤트가 발생하면 위임된 대리자가 콜백 메소드를 호출해주는 델리게이트 패턴(Delegate Pattern)을 많이 사용하는데, 이 패턴을 구현하기 위해 이용되는 것이 바로 프로토콜이다.
      프로토콜을 채택하거나 따르는 타입은 프로토콜에서 정의한 요구 사항을 구현할 것을 약속하기 때문에 프로토콜이 계약의 역할을 한다고 말한다.
      프로토콜을 채택한 타입이 프로토콜이 정의한 기능을 모두 구현하지 않았을 경우에는 컴파일 타임에서 에러가 발생하고, 프로젝트는 컴파일되지 않는다.
      Swift에서는 어떠한 클래스나 구조체 또는 열거형도 프로토콜을 따를 수 있다.
      스위프트 표준 라이브러리는 프로토콜에 기반을 두기 때문에 애플은 개발자에게 프로토콜 지향 프로그래밍 패러다임을 사용하게 권장할 뿐만 아니라, 애플 스스로 스위프트 표준 라이브러리에서 프로토콜을 사용하고 있다.

      프로토콜 문법

      프로토콜을 정의할 때에는 protocol 키워드를 사용한다.

      protocol <프로토콜명> {
      …프로퍼티 명세…
      }

      이렇게 작성된 프로토콜은 클래스나 구조체를 통해 구현된다.
      프로토콜을 구현한다는 것은 프로토콜에 선언된 명세에 따라 실질적으로 프로퍼티에 값을 할당하여 선언하거나 메소드의 내용을 작성하는 것을 의미한다.
      스위프트에서 프로토콜을 구현할 수 있는 구현체들에는 구조체, 클래스, 열거형, 익스텐션이 있다.

      struct / class / enum / extention 타입이름: 구현할 프로토콜 명{

      }

      프로토콜 프로퍼티

      프로토콜에 선언되는 프로퍼티에는 초기값을 할당할 수 없다.
      연산 프로퍼티인지 저장 프로퍼티 인지도 구분하지 않는다.
      프로퍼티의 종류, 이름, 변수/상수 구분, 타입, 읽기 전용인지 읽고 쓰기가 가능한지에 대해서만 정의한다.

      protocol SomePropertyProtocol {
        var name: String { get set }
        var description: String { get }
      }
      

      인스턴스 프로퍼티 두 개를 SomePropertyProtocol 프로토콜에 선언하고 있는데, 실제 내용을 선언한 것이 아니라 이러이러한 종류의 프로퍼티가 선언될 것이라고 설명하는 의미이다.
      프로퍼티 명세에는 초기값을 할당하는 대신 읽기 전용/ 읽고 쓰기에 대한 여부를 get과 set 키워드로 표시해야 한다.
      (읽기 전용 – get, 읽고 쓰기 전용 – set)
      프로퍼티가 연산 프로퍼티일 경우에는 get 키워드만으로 읽기 전용 속성으로 설정하거나 get과 set을 모두 사용하여 읽고 쓸 수 있는 프로퍼티로 설정할 수 있다.
      하지만 저장 프로퍼티로 사용하려면 반드시 get과 set 키워드를 모두 추가해야 한다.

      struct RubyMember: SomePropertyProtocol {
        var name: "홍길동"
        var description: String {
          return "Name: \(self.name)"
        }
      }
      

      앞에서 정의한 SomePropertyProtocol 프로토콜을 구현한 구조체이다.
      프로토콜에서 get, set으로 선언된 name 프로퍼티는 저장 프로퍼티로, 읽기 전용으로 선언된 description 프로퍼티는 연산 프로퍼티로 구현한다.
      프로토콜에서 선언된 프로퍼티 중 일부를 누락하면 오류가 발생하지만, 프로토콜과 상관없이 필요한 프로퍼티를 구현체에서 더 추가하는 것은 문제가 되지 않는다.

      프로토콜 메소드

      프로토콜에 선언되는 메소드도 프로퍼티와 크게 다르지 않다.

      protocol SomeMethodProtocol {
        func execute(cmd: String)
        func showPort(p: Int) -> String
      }
      

      두 메소드는 모두 매개변수가 정의되어 있지만, 모든 프로토콜에서 반드시 메소드에 매개변수를 정의해야 하는 것은 아니다.
      메소드의 반환 값 역시 선택 사항으로, 메소드의 성격에 맞게 생략하거나 작성해주면 된다.
      이러한 메소드는 중괄호와 메소드 몸체가 없다는 점 제외하고는 클래스와 구조체에서 정의했던 것과 똑같다.

      struct RubyService: SomeMethodProtocol {
        func execute(cmd: String) {
          if cmd == "start" {
            print("실행합니다")
          }
        }
        func showPort(p: Int) -> String {
          return "Port: \(p)"
        }
      }
      

      앞에서 정의한 SomeMethodProtocol 프로토콜을 구현하고 있는 구조체이다.
      프로토콜에 선언된 메소드 중에서 일부를 구현하지 않고 누락하면 오류가 발생하지만, 프로토콜에 정의되어 있지 않더라도 구현체에 임의로 메소드를 추가하는 것은 문제가 되지 않는다.
      또한, 프로토콜에서 정의된 메소드는 구현체에서도 매개변수명까지 완전히 일치해야 한다.

      protocol NewMethodProtocol {
          mutating func execute(cmd command: String, desc: String)
          func showPort(p: Int, memo desc: String) -> String
      }
      struct RubyNewService: NewMethodProtocol {
          func execute(cmd command: String, desc: String) {
              if command == "start" {
                  print("\(desc)를 실행합니다.")
              }
          }
          func showPort(p: Int, memo desc: String) -> String {
              return "Port: \(p), Memo: \(desc)"
          }
      }
      

      위의 코드는 외부 매개변수명이 포함된 프로토콜 메소드와 이를 구현한 예제이다.
      외부 매개변수명은 프로토콜을 그대로 따라야 하지만 내부 매개변수명은 임의로 바꾸어 사용해도 된다.

      struct RubyNewService2: NewMethodProtocol {
          func execute(cmd comm: String, desc d: String) {
              if comm == "start" {
                  print("\(d)를 실행합니다.")
              }
          }
          func showPort(p: Int, memo description: String) -> String {
              return "Port: \(p), Memo: \(description)"
          }
      }
      

      또한 내부 매개변수명과 외부 매개변수명이 프로토콜에서 통합되어 선언되어 있을 경우 구현체에서는 이를 분리하여 내부와 외부 매개변수명으로 나누고 따로 작성해도 된다.
      즉, 외부 매개변수명은 프로토콜을 그대로 따라야 하지만 내부 매개변수명은 임의로 변경이 가능하다.

      프로토콜에서의 mutatinng, static 사용

      Swift에서는 구조체 내의 메소드가 프로퍼티를 변경하는 경우, 메소드 앞에 반드시 mutatinng 키워드를 붙여 해당 메소드가 프로퍼티 값을 수정하는 메소드임을 표시하도록 강제한다.
      (물론 구조체 뿐만 아니라 모든 값 타입에 적용 가능하다.)
      클래스와 같은 참조 타입은 mutatinng 키워드를 붙이지 않아도 메소드 내에서 마음대로 프로퍼티를 수정할 수 있지만, 구조체나 열거형은 프로토콜의 메소드에 mutating 키워드가 추가되어있지 않을 경우 프로퍼티의 값을 변경할 수 없다.
      (프로토콜에 선언되지 않은 mutating 키워드를 임의로 구현할 수 없기 때문)
      이렇게, 프로토콜은 자신을 구현하는 구조체가 마음대로 프로퍼티를 수정하지 못하도록 통제할 수 있는 권한을 갖고 있다.
      일반적으로 프로토콜에서 메소드 선언에 mutating 키워드가 붙지 않는 것은 다음 두 가지 중 하나로 해석할 수 있다.

      1. 구조체나 열거형 등 값 타입의 객체에서 내부 프로퍼티의 값을 변경하기를 원치 않을 때
      2. 주로 클래스를 대상으로 간주하고 작성된 프로토콜일 때
      protocol MService {
          mutating func execute(cmd: String)
          func showPort(p: Int) -> String
      }
      struct RubyMService: MService {
          var paramCommand: String?
          mutating func execute(cmd: String) {
              self.paramCommand = cmd
              if cmd == "start" {
                  print("실행합니다")
              }
          }
          func showPort(p: Int) -> String {
              return "Port: \(p), now command: \(self.paramCommand!)"
          }
      }
      
      struct RubyMService2: MService {
          var paramCommand: String?
          func execute(cmd: String) {
              if cmd == "start" {
                  print("실행합니다")
              }
          }
          func showPort(p: Int) -> String {
              return "Port: \(p), now command: \(self.paramCommand!)"
          }
      }
      

      구조체에서 mutating 키워드를 붙이지 않은 것은 실제로 구현된 execute(cmd:) 메소드 내에서 프로퍼티를 변경하지 않기 때문으로, 프로토콜 쪽에서 mutating 키워드가 추가되어 있어도 실제 구현하는 쪽에서 프로퍼티의 변경이 없다면 키워드를 생략해도 된다.

      프로토콜 구조체 결과
      mutating mutating OK
      mutating OK
      mutating ERROR
      OK
      class RubyThread: MService {
          var paramCommand: String?
          func execute(cmd: String) {
              self.paramCommand = cmd
              if cmd == "start" {
                  print("실행합니다")
              }
          }
          func showPort(p: Int) -> String {
              return "Port: \(p), now command: \(self.paramCommand!)"
          }
      }
      

      클래스의 경우는 조금 다른데, 클래스는 참조 타입의 객체이므로 메소드 내부에서 프로퍼티를 수정해도 mutating 키워드를 붙일 필요가 없다.
      mutating 키워드가 붙어있는 프로토콜 메소드를 구현할 때도 클래스에서는 프로퍼티의 수정 여부와 관계없이 mutating 키워드를 사용하지 않는다.

      protocol SomeTypeProperty {
          static var defaultValue: String { get set }
          static func getDefaultValue() -> String
      }
      struct TypeStruct: SomeTypeProperty {
          static var defaultValue = "default"
          static func getDefaultValue() -> String {
              return defaultValue
          }
      }
      class ValueObject: SomeTypeProperty {
          static var defaultValue = "default"
          class func getDefaultValue() -> String {
              return defaultValue
          }
      }
      

      타입 메소드나 타입 프로퍼티도 프로토콜에 정의할 수 있다. 프로토콜의 각 선언 앞에 static 키워드를 붙이면 된다.
      프로토콜은 구조체나 열거형, 그리고 클래스에 모두 사용할 수 있는 형식으로 정의되어야 하기 때문에 클래스에서 타입 메소드를 선언할 때 사용할 수 있는 또 다른 키워드인 class는 프로토콜에서는 사용할 수 없다.
      하지만 프로토콜에서 static 키워드로 선언되었더라도 실제로 클래스에서 구현할 때는 필요에 따라 static이나 class 키워드를 선택하여 사용할 수 있다.

      프로토콜과 이니셜라이저

      프로토콜에서는 이니셜라이저도 정의할 수 있다.

      protocol SomeInitProtocol {
          init()
          init(cmd: String)
      }
      

      이니셜라이저인 만큼 반환 타입은 없으며 이름은 init으로 통일한다.
      이니셜라이저가 포함된 프로토콜을 구현할 때 주의할 점

      1. 외부 매개변수명까지는 완전히 일치해야 한다.
      2. 프로토콜에 선언된 초기화 메소드는 기본 제공 여부에 상관없이 모두 직접 구현해 주어야 한다.
      3. 클래스에서 이니셜라이저를 구현할 때에는 반드시 required 키워드를 붙여야 한다.
      struct SInit: SomeInitProtocol {
          var cmd: String
          init() {
              self.cmd = "start"
          }
          init(cmd: String) {
              self.cmd = cmd
          }
      }
      class CInit: SomeInitProtocol {
          var cmd: String
          required init() {
              self.cmd = "start"
          }
          required init(cmd: String) {
              self.cmd = cmd
          }
      }
      

      클래스에서 부모 클래스로부터 물려받은 초기화 구문과 프로토콜로부터 구현해야 하는 이니셜라이저가 충돌할 경우, 초기화 메소드에 required 키워드와 override 키워드를 모두 붙여주어야 한다.

      protocol Init {
          init()
      }
      class Parent {
          init() {
          }
      }
      class Child: Parent, Init {
          override required init() {
          }
      }
      

      override와 required의 순서는 상관없다.
      초기화 메소드뿐만 아니라 경우에 따라서 메소드나 연산 프로퍼티에서도 똑같은 일이 벌어질 수 있는데, 이때에도 부모 클래스와 프로토콜 양쪽에서 같은 내용이 정의되어 있을 때 해당 클래스에서는 구현과 동시에 override 키워드를 붙여야 한다.
      단, 일반 메소드나 연산 프로퍼티에 required 키워드는 붙이지 않는다. required 키워드는 초기화 메소드에만 붙는다.
      객체에서 구현할 수 있는 프로토콜의 개수에는 제한이 없다.
      두 개 이상의 프로토콜을 구현하고자 할 때는 구현할 프로토콜들을 쉼표로 구분하여 작성한다.
      이때 프로토콜의 선언 순서는 상관없지만, 각 프로토콜에서 구현해야 하는 내용들은 빠짐없이 구현되어야 한다.

      struct MultiImplement: NewMethodProtocol, SomeInitProtocol {
          var cmd: String
          init() {
              self.cmd = "default"
          }
          init(cmd: String) {
              self.cmd = cmd
          }
          mutating func execute(cmd: String, desc: String) {
              self.cmd = cmd
              if cmd == "start" {
                  print("시작합니다")
              }
          }
          func showPort(p: Int, memo desc: String) -> String {
              return "Port: \(p), Memo: \(desc)"
          }
      }
      

      프로토콜을 구현할 클래스가 다른 클래스로부터 상속된 것이라면 먼저 상속에 대한 선언부터 해야 한다.
      부모 클래스가 있다면 반드시 프로토콜 선언보다 앞서 작성되어야 한다.

      @objc의 사용

      때로는 프로토콜이 선택 가능한 요구 사항을 정의하기를 바라는 경우가 있다.
      선택 가능한 요구 사항은 메소드나 프로퍼티의 구현을 요구하지 않는다.
      선택 가능한 요구사항을 사용하기 위해서는 프로토콜을 표시할 때 @objc 속성이 프로토콜 앞부분에 위치해야 한다.
      오직 클래스만이 @objc 속성을 사용하는 프로토콜을 채용할 수 있고, 구조체와 열거형은 불가능하다.

      @objc protocol Phone {
          var phoneNumber: String { get set }
          @objc optional var emailAddress: String { get set }
          func dialNumber()
          @objc optional func getEmail()
      }
      

      optional 키워드를 사용하면 프로퍼티나 메소드가 선택 가능하다는 것으로 표시할 수 있다.

    • 야곰
      키 마스터
      • 글작성 : 37
      • 답글작성 : 579

      정말 멋진 글입니다! 최고네요! 진짜 물개박수!👏
      완벽한 글이라 이대로도 정말 좋지만 굳이 몇가지 의견 적어봅니다.

      • [struct / class / enum / extention 객체명]에서 객체명 대신에 타입이름이 좀 더 나은 표현같습니다. 스위프트에서 객체는 클래스의 인스턴스에 한정하여 칭하고 있습니다.
      • 스위프트에서는 초기화 메서드라고 하지 않고 이니셜라이저라고 합니다.
      • mutating 키워드는 구조체 뿐만 아니라 값 타입에 모두 적용할 수 있습니다. 즉, 열거형 등 다른 값타입에서도 사용 가능한데, 강조된 문장이 구조체에서만 적용할 수 있는 것 같아보여 오해의 소지가 있을 것 같아요.
        물론 아래 문장에서 부연설명 하기는 하지만 처음부터 그렇게 설명하면 좀 더 좋은 표현이지 않을까합니다 🙂

      좋은글 앞으로도 또 부탁드려요!

      • HyunJi
        참가자
        • 글작성 : 9
        • 답글작성 : 10

        네! 수정 완료했습니다 🙂 피드백 감사해요!!!!

1 답변 글타래를 보이고 있습니다
  • 답변은 로그인 후 가능합니다.

logo landscape small

사업자번호 : 743-81-02195
통신판매업 신고번호 : 제 2022-충북청주-1278 호
고객센터 : 카카오톡채널 @yagom