스코프 기반 접근 수준은 클래스나 클래스 확장의 구현 세부사항을 파일 단위가 아닌 클래스/확장 수준에서 숨길 수 있게 한다. 이는 클래스나 확장 정의의 특정 부분이 다른 클래스나 확장을 위한 공개 API를 구현하기 위해 존재하며, 해당 클래스나 확장의 스코프 외부에서는 직접 사용해서는 안 된다는 의도를 간결하게 표현한다.
현재 클래스의 구현 세부 사항을 숨기는 유일한 신뢰할 수 있는 방법은 코드를 별도의 파일에 넣고 private로 표시하는 것이다. 이 방법은 다음과 같은 이유로 이상적이지 않다:
구현 세부 사항이 완전히 숨겨져야 하는지, 아니면 private로 표시된 API를 잘못 사용할 위험 없이 관련 코드와 공유할 수 있는지 명확하지 않다. 파일에 여러 클래스가 이미 존재하는 경우, 특정 API가 완전히 숨겨져야 하는지 아니면 다른 클래스와 공유할 수 있는지 알기 어렵다.
이 방법은 파일당 하나의 클래스 구조를 강제하며, 이는 매우 제한적이다. 관련 API와/또는 관련 구현을 같은 파일에 배치하면 일관성을 보장하고 특정 API나 구현을 찾는 시간을 줄일 수 있다. 같은 파일에 있는 클래스들이 숨겨진 API를 공유할 필요는 없지만, 현재 접근 수준으로는 이러한 공유 가능성을 표현할 방법이 없다.
또 다른, 덜 신뢰할 수 있는 방법은 숨기려는 API 앞에 _
를 붙이거나 비슷한 방법을 사용하는 것이다. 이 방법은 작동하지만 컴파일러에 의해 강제되지 않으며, 이러한 API는 코드 완성과 같은 도구에 표시된다. 따라서 프로그래머는 노이즈를 걸러내야 한다. 비록 이러한 도구들이 _
접두사 표준을 가진 메서드를 숨기는 것을 쉽게 지원할 수 있지만, 여전히 private API를 사용할 위험이 크다. 특히 private API가 public API와 비슷한 일을 하지만 내부 상태에 대한 추가 가정 때문에 더 최적화된 경우 위험이 더 크다.
기존 솔루션은 어떤 면에서 타입이 없는 컬렉션을 다루는 방법과 유사하다. 일반적으로 컬렉션에 타입을 암시하는 이름을 붙일 수 있지만(예: _
를 사용해 private를 나타내는 것과 유사), 명시적으로 타입을 지정하는 것과는 다르다. 제네릭과 마찬가지로, 구현 세부 사항을 다른 클래스와 공유하지 않으려는 의도는 코드가 프로젝트의 어디에 있는지에 의존하는 것보다 언어의 지원을 받는 것이 훨씬 명확하다. 또한, 타입이 없는 컬렉션의 경우 다른 타입의 요소를 추가할 수 있다(의도적이든 아니든). 제네릭은 이를 불가능하게 만들며, 이는 컴파일러에 의해 강제된다. 마찬가지로, 전용 접근 수준 수정자를 사용하면 컴파일러 수준에서 구현 세부 사항을 숨길 수 있고, 클래스가 공유하지 않으려는 컨텍스트에서 실수로 또는 의도적으로 구현 세부 사항을 잘못 사용하는 것을 방지할 수 있다.
API가 정의된 범위 내에서만 접근 가능함을 나타내는 새로운 접근 수준 제어자를 추가한다. 이 방식으로 표시된 프로퍼티, 함수, 그리고 중첩 타입은 클래스나 클래스 확장 정의 외부에서 완전히 숨겨진다.
첫 번째 리뷰 후, 코어 팀은 이 접근 수준에 private
을 사용하고 일관성을 위해 다른 접근 수준 제어자의 이름을 변경하는 것이 가장 적합하다고 결정했다. 가장 널리 사용되는 이름 집합은 다음과 같다:
(이 이름은 James Berry가 제안한 이름을 Chris Lattner가 조정한 것이다)
함수, 변수, 상수, 서브스크립트, 초기화 구문을 private
접근 제어자로 정의하면 해당 렉시컬 스코프 내에서만 접근할 수 있다. 예를 들어:
class A {
private var counter = 0
// 내부 상태를 숨기는 public API
func incrementCount() { ++counter }
// 숨겨진 API, 이 렉시컬 스코프 외부에서는 보이지 않음
private func advanceCount(dx: Int) { counter += dx }
// incrementTwice()는 여기서 보이지 않음
}
extension A {
// counter는 여기서 보이지 않음
// advanceCount()는 여기서 보이지 않음
// 확장의 다른 메서드를 구현하는 데만 유용할 수 있음
// 다른 곳에서는 숨겨져 있으므로 이 확장 외부에서는 incrementTwice()가 코드 완성에 나타나지 않음
private func incrementTwice() {
incrementCount()
incrementCount()
}
}
private
접근 제어자를 사용해 타입을 정의할 때는 상황이 조금 더 복잡해진다. 물론 타입 자체는 정의된 렉시컬 스코프 내에서만 보이지만, 타입의 멤버는 어떻게 될까?
class Outer {
private class Inner {
var value = 0
}
func test() {
// Outer.test에서 Inner의 생성자를 참조할 수 있을까?
let inner = Inner()
// Outer.test에서 Inner의 'value' 프로퍼티를 참조할 수 있을까?
print(inner.value)
}
}
만약 private 타입의 멤버들도 private
로 간주된다면, 해당 멤버들은 타입 외부에서 사용할 수 없다는 것이 명확하다. 그러나 현재 멤버의 접근 수준이 해당 타입의 접근 수준보다 더 넓을 수는 없다. 이 제약은 딜레마를 만든다: 타입은 자신이 정의된 렉시컬 스코프 내에서 참조할 수 있지만, 그 멤버들은 참조할 수 없다.
형식적인 문제를 무시한다면, 가장 기대되는 동작은 private
로 명시적으로 표시되지 않은 멤버들은 private 타입을 감싸는 스코프 내에서 접근이 허용되는 것이다. 이 목표를 달성하기 위해 몇 가지 기존 규칙을 완화한다:
기본 접근 제어 수준은 어디서나 internal
이다.
컴파일러는 더 제한적인 접근 제어를 가진 타입 내에서 더 넓은 접근 제어 수준이 사용될 때 경고하지 않는다. 예를 들어 private
타입 내에서 internal
을 사용할 수 있다. 이는 타입 설계자가 타입을 더 넓게 공개했을 때 사용할 접근 수준을 선택할 수 있도록 한다. (멤버들은 여전히 감싸는 렉시컬 스코프 외부에서 접근할 수 없다. 타입 자체가 여전히 제한적이기 때문에 외부 코드는 해당 타입의 값을 절대 만나지 않는다.)
멤버의 타입은 해당 멤버가 접근 가능한 곳에서만 접근 가능한 선언을 참조할 수 있다. (이 변경은 선언의 타입이 더 넓은 접근을 가진 선언을 참조할 수 없다는 기존 규칙을 완화한다.) 이 변경은 다음 코드를 허용한다:
struct Outer {
private typealias Value = Int
private struct Inner {
var value: Value
}
}
그리고 다음 코드는 여전히 불법으로 처리한다:
struct Outer {
private struct Inner {
private typealias Value = Int
var value: Value
}
}
프로토콜 요구 사항을 충족하는 멤버는 절대 private
일 수 없다. 마찬가지로 required
생성자는 절대 private
일 수 없다.
이전과 마찬가지로 명시적 접근 제어자를 가진 확장은 기본 internal
접근을 재정의한다. 따라서 private
로 표시된 확장 내에서 기본 접근 수준은 fileprivate
이다. (확장은 항상 파일 스코프에서 선언되기 때문이다.) 이는 파일 스코프에서 private
로 선언된 타입의 동작과 일치한다.
이전과 마찬가지로 확장에 명시적 접근 제어자를 사용하면 해당 확장 내에서 허용되는 최대 접근 수준을 설정한다. 그리고 컴파일러는 명시적 접근 제어자를 가진 확장 내에서 너무 넓은 접근 수준이 사용될 때 경고한다.
기존 코드에서는 동일한 의미를 유지하기 위해 private
을 fileprivate
로 변경해야 한다. 하지만 대부분의 경우, private
의 새로운 의미가 이전과 동일하게 컴파일되고 실행될 가능성이 높다.
아무 조치도 취하지 않고 _
와 /
를 사용하거나 코드를 더 많은 파일로 분할한 후 private
수정자를 사용하는 방법. 제안하는 해결책은 의도를 훨씬 명확하게 표현하며 컴파일러에 의해 강제된다. 또한 언어가 코드를 어떻게 조직화해야 하는지를 강요하지 않는다.
파일의 일부에서 API를 숨길 수 있는 범위가 지정된 네임스페이스를 도입하는 방법. 이 방법은 추가적인 그룹화와 중첩을 도입하며, API를 논리적으로 더 적합한 방식이 아닌 접근 수준에 따라 그룹화하도록 강제한다.
다른 접근 수정자를 도입하고 현재 이름을 변경하지 않는 방법. 이 제안은 원래 기존 코드와 완전히 호환되도록 이 접근 방식을 따랐지만, 코어 팀은 다른 언어에서의 의미와 더 가깝기 때문에 이 수정자에 private
를 사용하는 것이 더 낫다고 판단했다.
private
및 fileprivate
타입 내에서 기본 접근 수준으로 internal
대신 fileprivate
를 사용한다. 이는 원래 모델에서 좀 더 좁은 변경이지만, 불필요하게 넓은 접근에 대한 경고가 유용하지 않다는 점을 확인한 후에는 이점이 없었다.
새로운 “parent” 접근 수준을 도입한다. 이는 엔티티가 바로 바깥 범위가 아니라 부모 어휘 범위 내에서 접근 가능하도록 선언한다. 이 아이디어는 private
에 대해 효과적이지만, 더 넓은 접근을 가진 타입 내에서는 지나치게 구체적이며 추가된 복잡성을 감수할 만한 가치가 없다. 또한 이 접근 수준에 대한 이름을 언어 내에서 결정하거나, 이 수준의 접근이 명시적으로 표현될 수 없고 private
타입 내에서만 기본 접근으로 사용 가능하도록 결정해야 한다.
새로운 “default” 접근 수준을 도입한다. 이는 범위 내에서 기본 접근을 명시한다. private
타입 내에서는 (2)의 “parent” 의미를 가지며, 다른 곳에서는 이전 버전의 Swift에서 정한 규칙을 따른다. 이 아이디어 역시 표현력의 작은 증가를 위해 모델에 복잡성을 더하며, 언어 내에서 이에 대한 이름을 결정해야 한다.