궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [프로토콜#1 - 프로퍼티 요구사항]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과Zedd0202님의 게시글을 참고하여 작성되었습니다.

 

Protocols

프로토콜(protocol) 이란 쉽게 생각해서 인증서라고 보면 된다. 

  • 프로토콜은 특정 작업이나 기능에 적합한 메서드, 프로퍼티 및 기타 요구사항의 청사진을 정의한다.
  • Struct, Class, EnumProtocol을 채택해서 특정 기능을 실행하기 위해 프로토콜의 요구사항을 실제로 구현할 수 있다.
  • 프로토콜은 정의를 하고 제시만 할 뿐 스스로 기능을 구현하지는 않는다. 즉, 조건만 정의한다. 
  • 메서드 구현부인 { } 중괄호는 사용 불가 하지만, 메서드 이름, 매개변수, 반환 타입 등만 작성 가능하며 매개변수도 사용 가능.
  • 하나의 타입으로 사용되기 때문에 아래와 같이 타입 사용이 허용되는 모든 곳에 프로토콜을 사용할 수 있다.
  • 여러 개의 프로토콜을 채택할 수 있다.
  • ⭐️ 프로토콜은 메서드, 프로퍼티 등을 '정의'만 하고, 이 프로토콜을 채택한 곳에서 '구현'을 한다.
protocol MyProtocol {
    // 프로토콜 구현부    
}

struct MyStruct: MyProtocol {}
class MYClass: MyProtocol {}

클래스처럼 앞에 protocol 키워드를 붙이고, 프로토콜 이름을 정해주면 사용이 가능하다.

 

그리고 이 프로토콜을 다른 클래스, 구조체, 열거형에서 위처럼 채택해줄 수 있다. 상속받는 것처럼 콜론 : 뒤에 프로토콜 이름을 넣어주면 된다.  

 

그리고 프로토콜은 여러 개를 채택할 수 있다.

struct MyStructure: FirstProtocol, AnotherProtocol {
  // struct definition goes here
}

만약 슈퍼클래스를 상속하고 있을 때는 순서가 중요하다. 슈퍼클래스첫 번째 프로토콜또 다른 프로토콜

 

반드시 슈퍼클래스를 먼저 써주고, 그다음 프로토콜을 채택해줘야 한다.

프로퍼티 요구사항

  • 프로토콜은 어떠한 conforming type(해당 프로토콜을 준수하는 타입)에게 특정 이름과 타입인, 인스턴스 프로퍼티 또는 타입 프로퍼티를 요구할 수 있다.
  • 프로토콜이 읽기 및 쓰기 프로퍼티를 요구하면, 해당 프로퍼티 요구사항은 상수 저장 프로퍼티 또는 읽기 전용 연산 프로퍼티로 충족되선 안된다.
  • 프로토콜에서 읽기만 필요로 하는 경우, 모든 종류의 프로퍼티에서 요구 사항을 충족시킬 수 있다.(필요하다면 쓰기만 정의 가능)
  • 읽기와 쓰기 프로퍼티는 선언 후 {get set} 을 사용하고, 읽기 프로퍼티는 {get} 을 사용한다.
  • ✔️ 프로토콜은 이 프로토콜이 저장 프로퍼티 인지, 연산 프로퍼티 인지 명시하지 않는다. (프로토콜을 채택한 타입은 프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞도록 구현해주면 됨)
  • ✔️ 프로토콜은 각 프로퍼티에 getter(읽기)만 가능한지, getter/setter(읽기/쓰기) 모두 가능한지 명시해야만 한다.(쓰기만 불가)
  • ✔️ 프로퍼티 요구사항은 항상 변수 프로퍼티로 선언됨(var 키워드)
protocol SomeProtocol {
   var settableProperty: String { get set }
   var notNeedToBeSettableProperty: String { get }
}

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
    static var anotherTypeProperty: Int { get }
}

SomeProtocol 프로토콜을 정의하고, 변수로 두 개의  프로퍼티를 선언했다. settableProperty은 읽기/쓰기 모두 가능한 프로퍼티, notNeedToBeSettableProperty은 읽기만 가능하다.

 

AnotherProtocol은 타입 프로퍼티인데 반드시 static 키워드를 붙여 정의해야 한다.

 

클래스의 타입 프로퍼티에는 상속 가능한 타입 프로퍼티인 class 타입 프로퍼티와 상속 불가능한 static 타입 프로퍼티가 있는데, 이 두 타입 프로퍼티를 따로 구분하지 않고 모두 static 키워드를 사용하여 타입 프로퍼티를 요구하면 된다.

 

다음 예제는 단일 인스턴스 프로퍼티 요구사항이 있는 프로토콜이다.

protocol FullyNamed {
   var fullName: String { get }
}

프로토콜에 읽기 전용 String 타입인 변수 fullName을 정의했다. 즉, 프로토콜에서 fullName이라는 프로퍼티를 요구했다.

 

지금 까지 본 것이 바로 정의 부분.

 

이제 채택준수 부분을 알아보자!

 

FullyNamed 프로토콜을 구조체에서 채택하려고 아래와 같이 코드를 작성하면 오류가 나는 것을 확인할 수 있다.

protocol FullyNamed {
    var fullNamed: String { get }
}

//error: 프로토콜 요구사항을 지키지 않음 
struct Person: FullyNamed {
    //code
}

✔️ 프로토콜은 이 프로토콜이 저장 프로퍼티 인지, 연산 프로퍼티 인지 명시하지 않는다. (프로토콜을 채택한 타입은 프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞도록 구현해주면 됨)

 

요구사항을 다음과 같이 지켜주면 된다.

protocol FullyNamed {
    var fullNamed: String { get }
}

struct Person: FullyNamed {
    var fullNamed: String 
}

✔️ 프로토콜에서 읽기만 필요로 하는 경우, 모든 종류의 프로퍼티에서 요구 사항을 충족시킬 수 있다.(필요하다면 쓰기(setter)만 정의 가능)

 

이제 이 부분을 알아보자면 '읽기(getter)만 필요로 하는 경우, 연산 프로퍼티든 저장 프로퍼티든 어느 것으로 선언해도 상관없다'라는 의미이다.

상수 저장 프로퍼티

protocol FullyNamed {
    var fullName: String { get }
}
struct Person: FullyNamed {
    let fullName: String
}

var seogun = Person(fullName: "서근 개발노트")

seogun.fullName = "서근" 
//error! 'fullName' 은 상수 let 으로 선언되었다.

Person 구조체의 저장 프로퍼티가 let 상수로 정의되어 있기 때문에 최초 한번 "서근 개발노트"를 제외한 다음 값 수정은 불가하다.

변수 저장 프로퍼티

protocol FullyNamed {
    var fullName: String { get }
}
struct Person: FullyNamed {
    var fullName: String
}

var seogun = Person(fullName: "서근 개발노트")

seogun.fullName = "서근"

연산 프로퍼티

연산 프로퍼티는 값을 저장할 수 있고, var로 선언하며, 반환할 변수가 따로 있어야 한다. 그리고 클래스, 구조체, 열거형에 값을 저장할 저장 프로퍼티가 반드시 하나 있어야 한다.

protocol FullyNamed {
    var fullName: String { get }
}
struct Person: FullyNamed {
    var name: String
    var fullName: String {
       return name
    }
}

var seogun = Person(name: "서근 개발노트")

seogun.fullName = "서근"
//프로퍼티에 할당할 수 없음: 'fullName'은(는) 읽기 전용 프로퍼티이다.

연산 프로퍼티를 보면 fullName에 에러가 났는데, 이유는 fullName은 읽기 전용 프로퍼티 이기 때문이다. 연산 프로퍼티는 값을 저장하지 않기 때문이다.

 

이를 해결하기 위해선 쓰기(setter)를 추가하면 되는데 아까 '만약 필요하다면 쓰기(setter)만 정의 가능' 부분이 그것이다.

 

하지만 setter만 가질 순 없기 때문에 get을 추가해주면 된다.

protocol FullyNamed {
    var fullName: String { get }
}
struct Person: FullyNamed {
    var name: String
    var fullName: String {
        get {
            return name
        }
        set {
            self.name = newValue
        }
    }
}

var seogun = Person(name: "서근 개발노트") //서근 개발노트

seogun.fullName = "서근" //서근

✔️ 프로토콜이 읽기(getter) 및 쓰기(setter) 프로퍼티를 요구하면, 해당 프로퍼티 요구사항은 상수(let) 저장 프로퍼티 또는 읽기 전용 연산 프로퍼티로 충족되선 안된다.

 

이 문장을 풀어보면 다음과 같다. 

protocol FullyNamed {
    //fullName은 읽기/쓰기 프로퍼티
    var fullName: String { get set }
}

fullName은 읽기와 쓰기 프로퍼티로 정의되어있는데, 당연히 쓰기(setter)가 있으니, 값을 변경할 수 없는 상수 저장 프로퍼티는 사용할 수 없다! 또 읽기 전용 연산 프로퍼티도 setter로 정의되어있는데 '읽기 전용' 이기 때문에 탈락!

protocol FullyNamed {
    var fullName: String { get set }
}

struct Person: FullyNamed {
    let fullName: String
    //오류 : Person 유형이 'FullyNamed' 프로토콜을 준수하지 않는다.
}

var seogun = Person(fullName: "서근")

읽기/쓰기 프로퍼티면 let 상수를 var 변수로 수정해줘야 한다!

 

그리고 읽기 전용 프로퍼티 부분도 읽기/쓰기 프로퍼티로 정의되어있는데 getter 전용이기 때문에 오류

protocol FullyNamed {
    var fullName: String { get set }
}

struct Person: FullyNamed {
    var name: String
    var fullName: String {
        return name
    }
    //오류 : Person 유형이 'FullyNamed' 프로토콜을 준수하지 않는다.
}

var seogun = Person(name: "서근")

다음과 같이 수정해줘야 한다.

protocol FullyNamed {
    var fullName: String { get set }
}

struct Person: FullyNamed {
    var name: String
    var fullName: String {
        get {
            return name
        }
        set {
            self.name = newValue
        }
    }
}

var seogun = Person(name: "서근")

이제 간단한 예시를 들고 이번 게시글은 마무리하려 한다!

 

Bird라는 슈퍼클래스가 있고 이 안에는 fly, layEgg라는 메서드가 있다고 가정한다.

class Bird {
    var isFemale = true
    
    func fly() {
        print("새가 날개짓을 하며 날아 오른다.")
    }
     
    func layEgg() {
        if isFemale {
            print("알을 낳을 수 있다.")
        } else {
            print("수컷은 알을 낳을 수 없다.")
        }
    }
}

class Eagle: Bird {
    func soar() {
        print("독수리는 놀이 날아오를 수 있다.")
    }
}

let myEagle = Eagle()
myEagle.fly()
myEagle.layEgg()
myEagle.soar()

위처럼 서브클래스를 슈퍼클래스로부터 상속시켜줬고, 서브클래스인 Ealge에 새로운 soar() 메서드를 정의해서 호출할 수 있었다.

 

하지만 만약 Airplane 또는 Penguin처럼 날갯짓을 하여 날아오를 수 없는 경우에는 어떨까?

 

그럴 때는 override를 사용하여 fly메서드를 재정의 할 수 있다.

class Airplane: Bird {
    // 슈퍼클래스 Bird의 fly메서드를 재정의 시킬 수 있다.
    override func fly() {
         print("비행기는 엔진을 사용하여 높이 날아오른다.")
    }
}

struct FlyingMuseum {
    func flyingDemo(flyingObject: Bird) {
        flyingObject.fly()
    }
}
let myAirplane = Airplane()

let museum = FlyingMuseum()
museum.flyingDemo(flyingObject: myAirplane)

//비행기는 엔진을 사용하여 높이 날아오른다.

하나 문제점은 Airplane에는 layEgg 메서드도 사용 가능하다는 것이다. 이럴 때 프로토콜을 사용하면 된다.

  1. Canfly라는 protocol을 생성하고 fly메서드 넣어준다.
  2. Bird 클래스에서 fly메서드를 삭제하고, fly메서드가 필요한 서브클래스에 fly메서드를 생성한다.
  3. Airplane은 더 이상 슈퍼클래스를 상속받지 않아도 되기 때문에 구조체로 변경 후 Canfly 프로토콜을 채택한다.
  4. flyingObject의 데이터 유형을 Bird가 아닌 Canfly로 변경한다.
protocol Canfly {
    func fly()
}

class Bird {
    var isFemale = true
     
    func layEgg() {
        if isFemale {
            print("알을 낳을 수 있다.")
        } else {
            print("수컷은 알을 낳을 수 없다.")
        }
    }
}

class Eagle: Bird, Canfly {
    
    func fly() {
        print("독수리는 날개짓을 하여 하늘 높이 날아오를 수 있다.")
    }
    func soar() {
        print("독수리는 놀이 날아오를 수 있다.")
    }
}

//펭귄은 날 수 없기 때문에 Canfly 프로토콜을 채택하지 않는다.
class Penguin: Bird {
    func swim() {
    print("펭귀은 헤엄을 굉장히 잘한다.")
    }
}

// 이제 Airplne은 어떠한 슈퍼클래스도 필요하지 않기 때문에 struct로 변경
struct Airplane: Canfly {
    // 슈퍼클래스 Bird의 fly메서드를 재정의 시킬 수 있다.
     func fly() {
         print("비행기는 엔진을 사용하여 높이 날아오른다.")
    }
}

//flyingObject의 유형을 Bird -> Canfly로 변경
struct FlyingMuseum {
    func flyingDemo(flyingObject: Canfly) {
        flyingObject.fly()
    }
}

let myEagle = Eagle()
let myPenguin = Penguin()
let myAirplane = Airplane()
//myAirplane.layEgg 사용 불가

let museum = FlyingMuseum()
museum.flyingDemo(flyingObject: myAirplane) //비행기는 엔진을 사용하여 높이 날아오른다.
museum.flyingDemo(flyingObject: myEagle) //독수리는 날개짓을 하여 하늘 높이 날아오를 수 있다.
//museum.flyingDemo(flyingObject: myPenguin) 사용 불가

 

 

읽어주셔서 감사합니다 🤟

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.