기어가더라도 제대로

[Swift - 기초] Protocol, Opaque return type(some) 본문

Swift - 기초

[Swift - 기초] Protocol, Opaque return type(some)

Damagucci-juice 2022. 10. 1. 12:39
- Protocol 의 생성과 사용
- Opaque(불투명) 반환 타입의 사용

프로토콜이란?

  • 데이터 타입에게 기대하는 기능을 정의
  • Swift 식으로 하는 계약
  • 앱의 나머지 부분에서 이 프로토콜을 따라야함
  • 실제 구현은 고려하지 않고 "이 타입은 이 기능을 따를거야~" 라고 명시

예시 

  • 출퇴근을 하는 사람을 시뮬레이션 하는 코드가 있다고 가정
  • 이 사람은 다양한 교통 수단을 이용해서 "통근" 이라는 기능을 수행해야함
    • 기차, 차, 오토바이, 공유 킥보드, 비행기 등 무슨 교통 수단을 타더라도 다음의 두 기능은 수행해야함
  • 어떤 수단을 타는지보다 "통근"을 "얼마의 시간" 동안 했다는 것이 중요

  • 새로운 프로토콜 타입이므로 가장 앞 대문자를 쓰는 캐멀케이스 사용
  • 이 프로토콜이 수행해야하는 행동(메서드)을 리스트업
  • 구체적인 "코드"는 적지 않는다.
    • 함수의 바디 부분을 나타내는 스코프{ }  가 없음
    • 메서드 이름, 파라미터, 반환값을 명시할 뿐 
  • implement type(구체 타입) 의 설계도를 작성 -> 프로토콜을 "추상타입" 이라고도 함
  • 프로토콜의 요구사항을 충족할 실제 구현 타입을 만들어야함
    • "프로토콜을 만족한다, 충족한다(adopting, conforming)"  고 표현
  • 프로토콜이 구체 타입이 수행할 모든 기능과 속성을 명시할 필욘 없음
  • 필요 최소한으로만 구현해 놓고 구체 타입이 선택적으로 추가 가능

주의 사항

  1. 서브 클래스 명시하는 것 처럼 프로토콜을 명시
  2. 프로토콜(Vehicle)에서 선언한 메서드는 구체타입(Car) 에서 반드시 선언되어야함
    1. 이름, 반환타입, 파라메터 까지 동일해야함
  3. Car 의 메서드들은 우리가 프로토콜에서 선언한 메서드들의 실제 구현 사항을 제공
  4. "func openSunroof()" 는 프로토콜에 없지만 Car 에서 자의적으로 추가된 사항
    1. 프로토콜은 아주 최소한의 요구사항만을 적어놓는다. 필요하면 구체타입에서 더 구현해도 됨

그래서 우리가 뭘 하려 했더라? 아 맞다 출근!

우리는 다양한 교통수단을 이용해 "출근"을 해야한다. 

Car 를 이용해 성공적으로 출근할 수 있었는데, 다른 기차나, 전동 킥보드를 타고선 출근하기가 어려워 보인다.
여기서 프로토콜이 도움을 준다.

스위프트는 어떤  Vehicle 이든지 estimateTime(), travel() 이 두 함수를 만족한다는 걸 안다. 교통수단을 Car 타입이 아닌 Vehicle 을 이용해서 다녀보자

이제 우리는 Vehicle 프로토콜을 만족하는 어떠한 타입이여도 "통근"에 이용할 수 있게 되었다.
자전거를 이용해서 출퇴근이 가능해졌다.

Vehicle 프로토콜을 만족한다면 Car 든 Bicycle 이든 모두 Vehicle 이다. 그래서 commute() 함수에 사용 가능하다. 
프로토콜을 사용하면 commute 함수의 vehicle 자리에 구체적인 타입을 밝혀 놓지 않고 Vehicle 을 만족하는 어떤 타입도 받을 수 있다. 

변수도 프로토콜에 선언

  • 메서드와 프로퍼티를 프로토콜에 선언 가능
  • var 키워드로 선언 가능
  • 프로퍼티의 경우 읽기 전용인지, 읽고 쓸 수 있는지 명시
    • { get }: 읽기 전용
    • { get set }: 읽고 쓰기 가능
  • 기본 값을 프로토콜에 넣을 수 없기 때문에 프로퍼티에선 타입 추론이 필요
  • 프로토콜에 변경 사항이 생겼으니 기존에 채택하는 Car, Bicycle 은 오류를 냄

  • name 을 보면 프로토콜에선 var { get } 으로 선언
    • 실제 구현은 let 으로 선언
    • 실제 구현에서 상수로 선언하면 읽기는 가능하지만 쓰기가 불가능함
      • { get }  만족
  • currentPassengers 를 보면 프로토콜에선 var { get set } 으로 선언
    • 실제 구현은 var 로 선언
    • 실제 구현에서 변수로 선언하면 읽기, 쓰기 가능
      • { get set } 모두 만족
func getTravelEstimates(using vehicles: [Vehicle], distance: Int) {
    for vehicle in vehicles {
        let estimate = vehicle.estimateTime(for: distance)
        print("\(vehicle.name): \(estimate) hours to travel \(distance)km")
    }
}

vehicles 파라메터를 보면 Vehicle 배열을 받는다. 
이는 [car, bike] 이런 값도 들어갈 수 있다는 것이다. 
더불어 프로토콜은 구체 타입이 무한으로 채택할 수 있다. 클래스 상속은 한 부모만 가질 수 있는것과 대조적이다. 
순서는 "SomeType: 부모클래스, 프로토콜1, 프로토콜2 ..." 순서이다.


[심화] 불투명 반환 타입 - Opaque return type

비슷한 일을 하는 두 함수

  • 서로 타입이 다른 것만 빼면 비슷한 일을 하는 두 함수
  • Int 와 Bool 의 공통점 "Equatable" 프로토콜을 만족한다. 
    • Equatable: 동일한 데이터인지 비교 가능하다
    • "==" 메서드 사용 가능
    • struct 는 class 와 다르게 그 타입이 가지고 있는 data의 값으로 스스로를 식별! 
      • class 는 인스턴스 고유의 정체성이 있음
      • struct는 자기가 가지고 있는 속성의 값 그 자체가 정체성임

숫자끼리 비교 가능

  • 에러 발생 코드
  • Equatable 을 반환하는 것이 에러
  • 이 코드가 왜 실행이 안되는지를 이해하는 것이 Opaque return type을 이해하는데 키 포인트!
  • 무엇보다도.. 함수에서 프로토콜을 리턴 가능
  • 근데 여기선 왜 안될까?

예를들어 수화물과 승객 수를 파라메터로 받아서 적합한 차량을 렌트해주는 함수가 있다고 생각해보자. 
결국 승합차, SUV, 미니밴 중에서 반환 타입이 나올 것이고, 이 타입들은 Vehicle 프로토콜을 만족하니까 
반환 타입에 따라 함수를 따로 선언할 필요 없이 Vehicle 만 내보내면 된다.
어차피 Vehicle 은 선언된 프로퍼티와 메서드를 수행할 수 있을 테니까 별 문제가 없어 보인다. 

  • 근데 진짜 문제가 없을까? 코딩적인 관점에서는 문제가 될 수 있다.
  • 받아보는 Vehicle 이 미니밴인지, SUV인지, 승합차인지 알 수 가 없다.
  • 서로 대치가 가능하기 때문

다시 돌아와서 Int 가 반환되는지, Bool 이 반환 되는지는 위의 함수에서 중요한 쟁점이다.
뭉뚱 그려서 Equatable 타입이라고 하면 구체적인 타입을 알 수 없다.

두 타입 모두 Equatable 을 만족하기는 하지만, 서로 대치 될 수 없다. Int 와 Bool 을 ==(비교) 할 수는 없지 않은가?

정보를 숨겨주는 프로토콜

  • 함수에서 리턴값으로 프로토콜을 보내는 것은 유용하다
  • 정확한 타입을 명시하는 것보다는 반환되는 타입의 기능성에 집중하기 때문
  • 위 예시의 Vehicle 프로토콜은
    • 좌석수, 예상 연료 사용량, 가격 
  • 등의 기능을 가지겠지만, 미니밴인지 승합차인지 뭔지 알 수 없다.
  • 만약 RaceCar, PickUpTruck 이 Vehicle 에 추가되더라도 문제 없이 수행할 수 있다. 
    • 8명의 승객을 받아도 코딩적으론 레이싱카를 반환할 수 있게 된다.
  • 정보를 숨기는 건 유용하다. 다만 조금 더 구체적일 필요가 있다.
  • Equatable 을 만족하는 두 타입을 비교하는 것이 불가능 하기 때문에, Equatable 은 사용할 수 없다. 
  • 만약에(되지도 않겠지만)  Equatable 을 반환하는 getRandomNumber() 를 두번 수행해서  정수 두개를 얻어도 서로를 비교할 수 없다. 
  • 실제로는 정수라서 비교할 수 있지만 프로토콜을 반환함으로 써 정보를 숨겼기 때문에 비교할 수 없다.
  • Equatable 인지만 Swift 가 알지 실제로 예가 Int 일지, Bool일지 아니면 또다른 타입일지 어떻게 안단 말인가?

Opaque return type 이 생긴 이유 - Some

  • 코드에서는 정보를 숨겨주지만 Swift 컴파일러에게는 정보를 숨기지 않는다. 
  • 이는 우리가 내부적으로는 유연한 코드를 만들 수 있게 한다. 
    • 미래에 다른 타입(데이터) 를 반환해도
    • Swift 가 알아서 반환되는 실제 데이터 타입을 이해하고 적절히 확인

  • getRandomNumber() 를 두 번 호출하고 그 결과로 == 를 이용해 비교 가능
  • some 키워드
    • 여전히 Equatable 을 반환하지만 some 으로 인해 Swift 는 저 타입이 실제로는 Int 라고 확인 가능
  • Opaque return type 을 반환하는 것
    • 실질적인 타입이 아니라 기능에 집중하면서도
    • 추후에 변경이 있을 때 코드를 수정하지 않고 대응 가능
    • getRandomNumber() 함수의 바디가 Double.random(in: 1...6) 이여도 가능
    • Swift 가 그 속에 있는 실제 타입을 인식할 수 있다는 것
  • Vehicle
    • 뭔진 모르겠지만 Vehicle 만 만족하는 어떤 타입
  • some Vehicle
    • 특정한 하나를 지정하진 않겠지만, 특정 종류의 Vehicle 타입

SwiftUI 에서 Opaque return type 이 중요한 이유

  • SwiftUI 에선 스크린에 레이아웃을 보여주기 위해 특정 타입을 알아야함
  • 레이아웃을 그린다고 해보자.

 

  • "스크린에 맨 위에 툴바 있고요
  • 맨 밑엔 탭바 있고요
  • 중간엔 컬러 아이콘의 그리드 한 스크롤 구현되어있고요 
  • 아이콘 밑에 이를 설명하는 레이블이 있는데요
  • 이 레이블은 굵은 폰트로 되어있고요
  • 아이콘을 누르면 이 메시지가 나타나요"

스위프트 UI에선 이 모든 설명이 레이아웃의 리턴 타입이다. 
구체적으로 하나하나 다 밝힌 저게 하나의 리턴 타입이라고 명시해야한다. 

저걸 리턴 타입이라 할 수 있을까?
아니면 some View 하나 놓고 말까..  
레이아웃의 변경점이 생길때마다 새로운 타입을 선언해야하나?
아니면 some View 하나 놓고 말까..



여기서 Opaque return type 이 우리를 구원하러 오셨다.
some View 라고만 하면 구체적인 타입을 주저리 주저리 늘어 놓지 않아도
Swift 컴파일러는 실제 늘어놓은 타입을 알 수 있지만 개발자는 some View 라고만 하면 끝이다.

Swift 는 항상 반환된 실제 데이터의 타입을 알고 SwiftUI 는 레이아웃을 만드는데 사용할 것이다. 

Comments