[Swift] extension과 private (SE-0169)
Swift 3 에서 private 으로 정의된 프로퍼티나 함수의 액세스 컨트롤 범위는 바로 정의된 범위 그 자체입니다.
struct Date: Equatable, Comparable {
private let secondsSinceReferenceDate: Double
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
위 예제에서 secondsSinceReferenceDate 라는 프로퍼티는 private 으로 지정되었기 때문에 struct Date { } 가 정의된 범위 내에서만 사용할 수 있습니다.
Swift 3 에서는 private 의 사용 범위를 더욱 더 제한함으로써 데이터를 안전하게 사용하려고 했지만 불편한 부분이 있었습니다. 바로 extension 입니다.
extension 은 Objective-C(Objective-C 에선 category 라고 부릅니다.) 와 Swift 에서 개발자들에 의해 가장 빈번히 그리고 즐겨 사용하게 되는 기술입니다.
위 코드를 extension 을 이용해 분리하면 다음과 같이 됩니다.
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
논리적으로 구분되는 부분을 명시적으로 다른 영역에 표현해서 코드 가독성을 높여줍니다.
하지만 위와 같이 바꿨을 때 Swift 3 에선 에러가 나게 되는데요. 바로 private 의 액세스 범위가 struct Date { } 를 벗어날 수 없기 때문입니다.
새로 생성된 extension 에선 secondsSinceReferenceDate 에 접근할 수 없습니다.
이 문제를 해결하는 방법은 secondsSinceReferenceDate 의 접근 제한자를 private 에서 fileprivate 으로 바꾸는 것입니다.
struct Date {
fileprivate let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
fileprivate 은 액세스 범위를 해당 파일로 두고 있습니다.
그래서 private 을 fileprivate 으로 바꾸면 되는데, 경험 있는 Swift 개발자들은 이 모습을 봤을 때 뭔가 이질감을 느꼈을 겁니다.
private 은 말 그대로 외부에서 접근 불가능하다는 의미를 가지지만, extension 에서도 private 에 접근하지 못한다는 건 뭔가 취지에 맞지 않아 보였습니다.
Swift 3 의 디자인은 private 을 적극적으로 사용하는게 목표였다고 합니다.
그리고 적절하게 extension 을 사용하는 것은 Objective-C 와 Swift 에서 굉장히 오래된 관습이고 중요한 패턴입니다.
하지만 Swift 3 에선 이 두 가지 패턴이 상극이 됐으니, 저 fileprivate 을 쓸 때마다 뭔가 불편함을 느끼지 않을 수 없었습니다.
Swift 4 에선 드디어 이 부분이 해결되었습니다.
fileprivate 이 아니라 private 을 그냥 쓰면 됩니다.
이제 다시 위 두번째 코드를 쓰면 되네요 :)
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
규칙은 간단합니다.
같은 파일 내에 쓰여진 extension 은 정의된 struct나 class 안에서 private 접근을 서로 공유합니다.
extension 이 다른 파일에 쓰였다면 private 은 서로 접근되지 않습니다.
다음은 몇 가지 예제입니다.(출처 : https://github.com/apple/swift-evolution/blob/master/proposals/0169-improve-interaction-between-private-declarations-and-extensions.md)
struct S {
private var p: Int
func f() {
use(g()) // ok, g() is accessible within S
}
}
extension S {
private func g() {
use(p) // ok, g() has access to p, since it is in an extension on S.
}
}
extension S {
func h() {
use(g()) // Ok, h() has access to g() since it defined in the access control scope for S.
}
}
// FileA.swift
struct A {
private var aMember : Int
}
// FileB.swift
extension A {
private func foo() {
bar() // ok, foo() does have access to bar()
}
}
extension A {
private func bar() {
aMember = 42 // not ok, private members may not be accessed outside their file.
}
}