필자는 Swift 언어를 매우 좋아한다. 그 이유 중 하나가 바로 이 타입에 관련된 부분인데, Swift는 처음 설계할 때부터 Type Safety(타입 안전성)를 중점으로 두고 설계된 언어이다.
타입 안정성, 다시 말해 코드에서 사용되는 변수와 상수, 그리고 함수의 반환 타입이 하나의 타입으로 명확해야 하고 그 이외 타입의 값은 할당될 수 없으며 서로 다른 타입 간의 연산도 엄격하게 허용하지 않는다. 따라서 Java에서 지원하는 묵시적 형변환(implicit type conversion)과 같은 기능도 지원하지 않는다.
정적 타입 지정 언어는 말 그대로 코드를 작성할 때 변수와 상수, 함수의 반환 타입 등에 개발자가 직접 타입을 지정해 주고 컴파일 시에 타입이 확정되는 언어를 말한다.
정적 타입 지정 언어와 반대되는 개념으로 동적 타입 지정 언어가 있다. 동적 타입 지정 언어는 타입을 명시하지 않고 변수에 할당되는 값에 따라 런타임에 타입이 동적으로 변경되는 언어라고 할 수 있다. 그 예로 Python 언어가 대표적인데, 아래와 같이 처음에는 변수 num
에 10
을 할당하면 이때 변수 num
의 타입은 int
가 되며, 변수 num
에 다시 "10"
을 할당하면 이때 변수 num
의 타입은 str
이 된다.
num = 10
print(type(num)) # <class 'int'>
num = "10"
print(type(num)) # <class 'str'>
동적 타입 지정 언어의 장점으로는 타입을 명시적으로 작성해 주지 않아도 되기 때문에 코드가 간결해 진다는 장점이 있지만, 단점으로는 예상치 못한 값이 전달될 수 있고 그 결과 컴파일 시에 에러를 발생 시키진 않지만 런타임에 가서야 에러를 발생시키며 프로그램이 강제로 종료될 수 있다.
서론이 좀 길었다고 생각되는데 다시 Swift로 돌아와서.. 소제목에서도 언급했다시피 Swift는 정적 타입 지정 언어이다.
let losslessStringConverter: (String) -> Double? = Double.init
위와 같이 변수에 타입을 지정 해주면 Double
타입에도 여러가지 이니셜라이저가 오버라이딩 되어 있는데, 컴파일러는 지정되어 있는 타입을 확인하고 String
을 받아 Optional<Double>
값을 반환하는 이니셜라이저를 할당하게 된다.
매번 변수에 타입을 지정해 줘야하는 불편함이 있지만, 필자는 정적 타입 지정 언어의 장점을 좀 더 중요하게 생각하기 때문에 조금 불편 하더라도 사전에 에러를 예방할 수 있는 정적 타입 지정 언어를 좀 더 선호 한다.
하지만 매번 저렇게 타입을 명시해 주는 것이 여간 귀찮은 일이 아니다.
Swift 창시자 '크리스 래트너'도 저렇게 매번 타입을 작성하는 것이 귀찮(?)았는지 Swift에서도 타입 추론 기능을 지원한다. 만세!🙌
타입 추론이란 개발자가 명시적으로 타입을 작성해 주지 않아도 컴파일러가 해당 변수에 할당된 값을 보고 그 변수의 타입을 유추하여 타입을 지정해 주기 때문에 타입을 직접 작성할 필요가 없다.
따라서 위의 코드는 타입 추론을 사용하면 다음과 같이 표현 가능하다.
let losslessStringConverter = Double.init as (String) -> Double?
위와 같이 타입을 표시하지 않아도 컴파일러는 변수 losslessStringConverter
에 할당 되어 있는 값을 보고 변수의 타입을 추론하게 된다. 다만 위에서 언급 했다시피 Double
타입에는 여러가지 이니셜라이저가 정의되어 있기 때문에 as
연산자를 통해 하나의 타입으로 확정지을 수 있다.
Swift는 오픈 소스로 공개 되어 있는 언어로 Swift의 발전은 현재 진행형이다. Swift 5.6에서 소개된 따끈한(🔥) 신상 문법인 Type Placeholder에 대해 알아보자.
타입 추론에도 한 가지 단점이 존재하는데, 위에서 본 예시 코드 같은 경우 단순히 하나의 매개변수를 받아 하나의 값을 반환하는 함수이기 때문에 타입이 그렇게 복잡해 보이지 않는다. 하지만 이전까지는 제네릭 타입이나 함수의 반환 타입 등 타입 추론을 사용할 수 없는 상황이 존재했다.
enum Either<Left, Right> {
case left(Left)
case right(Right)
init(left: Left) { self = .left(left) }
init(right: Right) { self = .right(right) }
}
func makePublisher() -> Some<Complex<Nested<Publisher<Chain<Int>>>>> { ... }
위의 코드와 같이 복잡한 형태의 반환 값을 가지는 함수가 있다고 가정해 보자. Either
를 makePublisher
를 이용하여 아래와 같이 초기화를 시도하면 다음과 같은 문제에 직면하게 된다.
let publisherOrValue = Either(left: makePublisher()) // Error: generic parameter 'Right' could not be inferred
제네릭 타입 Left
는 전달된 값으로 컴파일러가 타입 추론이 가능 하지만, 제네릭 타입 Right
과 관련된 변수에는 아무 값이 전달 되지 않았기 때문에 컴파일러 입장에서는 Right
의 타입을 추론할 수 없기 때문에 에러가 발생한다.
따라서 아래와 같이 반드시 제네릭 타입에 타입을 명시적으로 지정해 줘야하는데..
let publisherOrValue = Either<Some<Complex<Nested<Publisher<Chain<Int>>>>>, Int>(left: makePublisher())
코드가.. 이쁘지 않다..😱
코드의 가독성이 좋지 않을뿐 아니라 코드 작성하기도 힘들고 만에하나 에러가 발생 했다고 하더라도 원인을 찾는데도 시간이 오래 걸릴듯 하다. (참고로 제네릭에서 타입을 명시적으로 작성해야 한다면 다른 타입이 타입 추론으로 알 수 있는 상황이더라도 모든 타입을 명시적으로 작성해야 한다.)
이러한 문제를 극복하고자 Swift 5.6 이상, Xcode 13 버전 이상부터 Type Placeholder라는 기능을 지원하는데 _
문자를 사용하면 아래와 같이 표현할 수 있다.
let publisherOrValue = Either<_, Int>(left: makePublisher())
너무 간단한데? 😳
Type Placeholder를 사용하면 컴파일러는 _
문자가 들어간 곳의 타입을 makePublisher
의 반환 타입으로 추론할 수 있게 되기 때문에 더 이상 추론 가능한 제네릭 타입에 타입을 명시적으로 작성해 줄 필요가 없어졌다.
제네릭 타입에서의 Type Placeholder 예시를 좀 더 살펴보자.
import Combine
func makeValue() -> String {
return "Hello World!"
}
func makeValue() -> Int {
return 0
}
let publisher = Just(makeValue()).setFailureType(to: Error.self).eraseToAnyPublisher()
위에 코드대로 작성하면 컴파일러는 오류를 발생시킨다. makeValue
함수가 오버로드 되어 있기 때문에 어떤 함수를 호출해야 하는지 명확하지 않기 때문이다.
그렇기 때문에 아래와 같이 publisher
의 타입을 명시적으로 작성해 줘야 한다.
let publisher: AnyPublisher<Int, Error> = Just(makeValue()).setFailureType(to: Error.self).eraseToAnyPublisher()
setFailureType(to:)
의 전달 인자로 Error
타입이 전달된 것을 보아 AnyPublisher
의 제네릭 타입인 Failure
의 타입이 명백하게 Error
타입인 것을 알 수 있다.
따라서 최종적으로 아래와 같은 형태가 가능해진다.
let publisher: AnyPublisher<Int, _> = Just(makeValue()).setFailureType(to: Error.self).eraseToAnyPublisher()
이외에도 Type Placeholder를 포함할 수 있는 타입은 크게 아래와 같은 타입에서 사용할 수 있다.
Array<_> // 배열의 내부 요소 타입
[Int: _] // 딕셔너리의 value 타입
(_) -> Int // 함수의 매개변수 타입
(_, Double) // 튜플의 요소 타입
_? // 옵셔널 타입
❗️Note: Type Placeholder는 다른 타입에 포함된 타입을 대체하기 위해 사용 가능하다.
따라서 아래와 같이 top-level에 정의되어 있는 변수의 타입을 대체할 수는 없다.
let number: _ = 100.0 // error: placeholders are not allowed as top-level types
https://github.com/apple/swift-evolution/blob/main/proposals/0315-placeholder-types.md
여기 잘못 적으신 것 같아요..! 정적 타입 지정 언어를 말씀하시는 거겠죠?
매번 변수에 타입을 지정해 줘야하는 불편함이 있지만, 필자는 동적 타입 지정 언어의 장점을 좀 더 중요하게 생각하기 때문에 조금 불편 하더라도 사전에 에러를 예방할 수 있는 동적 타입 지정 언어를 좀 더 선호 한다.