접근 제어

접근 제어 (Access Control)

코드를 선언 단위, 파일, 모듈별로 얼마나 보이게 할지 정하는 기능이에요. 다른 소스 파일이나 모듈의 코드에서 내 코드의 일부에 접근하지 못하게 막아서, 구현 세부사항을 숨기고 접근 가능한 인터페이스만 골라서 드러낼 수 있죠.

접근 수준은 타입(클래스, 구조체, 열거형)은 물론 그 타입에 속한 프로퍼티, 메서드, 이니셜라이저, 서브스크립트에도 각각 지정할 수 있어요. 프로토콜이나 전역 상수, 변수, 함수에도 제한을 걸 수 있고요.

Swift는 여러 접근 수준을 제공하는 한편, 일반적인 상황에 맞는 기본 접근 수준을 마련해 두어서 명시적으로 지정할 필요를 줄여줘요. 실제로 단일 타깃 앱을 만들 때는 명시적인 접근 수준을 전혀 쓰지 않아도 될 때가 많아요.

Note: 이하에서 접근 제어를 적용할 수 있는 여러 요소(프로퍼티, 타입, 함수 등)를 간단히 **엔티티(entity)**라고 부를게요.

출처: The Swift Programming Language

본문

모듈, 소스 파일, 패키지

Swift의 접근 제어 모델은 모듈, 소스 파일, 패키지라는 개념을 바탕으로 해요.

**모듈(module)**은 코드 배포의 단일 단위예요. 하나의 단위로 빌드되어 배포되는 프레임워크나 앱을 가리키는데, 다른 모듈에서 Swift의 import 키워드로 불러올 수 있죠.

Xcode의 각 빌드 타깃(예: 앱 번들이나 프레임워크)은 Swift에서 별도의 모듈로 취급돼요. 앱 코드의 일부를 독립된 프레임워크로 묶는다면 — 여러 앱에서 재사용하려고 캡슐화한다던가 — 그 프레임워크 안에서 정의한 모든 것은 앱이나 다른 프레임워크 안에서 불러 쓸 때 별도의 모듈이 돼요.

**소스 파일(source file)**은 모듈 안에 있는 Swift 소스 코드 파일 하나예요(실질적으로는 앱이나 프레임워크 안의 파일 하나). 각 타입을 별도의 소스 파일에 정의하는 게 흔하지만, 한 소스 파일에 여러 타입과 함수 등을 함께 정의할 수도 있어요.

**패키지(package)**는 하나의 단위로 개발하는 모듈들의 묶음이에요. 패키지를 이루는 모듈은 Swift 소스 코드가 아니라, 사용하는 빌드 시스템 설정에서 정의해요. 예를 들어 Swift Package Manager로 빌드한다면 Package.swift 파일에서 PackageDescription 모듈의 API를 써서 패키지를 정의하고, Xcode를 쓴다면 Package Access Identifier 빌드 설정에서 패키지 이름을 지정해요.

접근 수준 (Access Levels)

Swift는 엔티티에 쓸 수 있는 여섯 가지 접근 수준을 제공해요. 이 접근 수준은 엔티티가 정의된 소스 파일, 그 소스 파일이 속한 모듈, 그 모듈이 속한 패키지를 기준으로 상대적으로 정해져요.

  • Open accesspublic access는 정의된 모듈의 어떤 소스 파일에서든, 그리고 그 모듈을 import한 다른 모듈의 소스 파일에서도 엔티티를 쓸 수 있게 해줘요. 보통 프레임워크의 공개 인터페이스를 지정할 때 open이나 public을 써요. open과 public의 차이는 아래에서 설명할게요.
  • Package access는 엔티티가 정의된 패키지 안의 어떤 소스 파일에서든 쓸 수 있게 하지만, 그 패키지 밖의 소스 파일에서는 쓸 수 없게 해요. 여러 모듈로 나뉜 앱이나 프레임워크 안에서 주로 써요.
  • Internal access는 정의된 모듈 안의 어떤 소스 파일에서든 쓸 수 있게 하지만, 모듈 밖의 소스 파일에서는 쓸 수 없게 해요. 앱이나 프레임워크의 내부 구조를 정의할 때 주로 쓰죠.
  • File-private access는 엔티티를 자신이 정의된 소스 파일 안에서만 쓸 수 있게 제한해요. 특정 기능의 구현 세부사항을 파일 전체에서 쓸 때, 그 세부사항을 숨기고 싶을 때 file private을 써요.
  • Private access는 엔티티를 둘러싼 선언과, 같은 파일 안에 있는 그 선언의 extension 안에서만 쓸 수 있게 제한해요. 구현 세부사항이 하나의 선언 안에서만 쓰일 때 그걸 숨기고 싶으면 private을 쓰죠.

Open access가 가장 높은(제약이 가장 적은) 수준이고, private access가 가장 낮은(제약이 가장 많은) 수준이에요.

Open access는 클래스와 클래스 멤버에만 적용돼요. 그리고 아래 doc:AccessControl#Subclassing에서 다루겠지만, 모듈 밖의 코드가 서브클래싱하고 오버라이드하는 것을 허용한다는 점에서 public과 달라요. 클래스를 open으로 표시한다는 건, 다른 모듈의 코드가 이 클래스를 슈퍼클래스로 쓰는 영향을 고려했고 그에 맞게 클래스를 설계했다는 뜻을 명시적으로 밝히는 거예요.

접근 수준의 기본 원칙

Swift의 접근 수준에는 전체를 관통하는 기본 원칙이 하나 있어요: 어떤 엔티티도 자기보다 낮은(더 제약이 많은) 접근 수준을 가진 다른 엔티티로 정의될 수 없다. 예를 들면:

  • public 변수를 internal, file-private, private 타입으로 정의할 수 없어요. public 변수가 쓰이는 모든 곳에 그 타입이 존재한다는 보장이 없기 때문이죠.
  • 함수는 자신의 매개변수 타입과 반환 타입보다 높은 접근 수준을 가질 수 없어요. 함수가 쓰이는 상황에서 구성 타입들이 주변 코드에 없다면 쓸 수 없으니까요.

이 기본 원칙이 언어의 여러 측면에 어떤 함의를 주는지는 아래에서 자세히 다룰게요.

기본 접근 수준 (Default Access Levels)

코드의 모든 엔티티는(나중에 이 챕터에서 설명할 몇 가지 예외를 빼면) 명시적인 접근 수준을 지정하지 않으면 기본값으로 internal이 돼요. 그래서 대부분의 경우 코드에서 명시적으로 접근 수준을 쓸 필요가 없죠.

단일 타깃 앱에서의 접근 수준

단순한 단일 타깃 앱을 작성할 때는 앱의 코드가 앱 안에서 자기완결적이고, 앱의 모듈 밖으로 제공될 필요가 없어요. 기본 접근 수준인 internal이 이미 이 요구에 맞죠. 그래서 별도의 접근 수준을 지정할 필요가 없어요. 다만 앱 모듈 안의 다른 코드에게 구현 세부사항을 숨기고 싶다면, 코드 일부를 file private이나 private으로 표시하고 싶을 수도 있어요.

프레임워크에서의 접근 수준

프레임워크를 개발할 때는 그 프레임워크의 공개 대면 인터페이스를 open이나 public으로 표시해서, 프레임워크를 import하는 앱 같은 다른 모듈이 보고 접근할 수 있게 해줘요. 이 공개 대면 인터페이스가 바로 프레임워크의 응용 프로그래밍 인터페이스(API)예요.

Note: 프레임워크의 내부 구현 세부사항은 여전히 기본 접근 수준인 internal을 쓰거나, 프레임워크 내부 코드의 다른 부분에게 숨기고 싶다면 private이나 file private으로 표시할 수 있어요. open이나 public으로 표시할 필요가 있는 건, 프레임워크의 API 일부로 만들고 싶은 엔티티뿐이에요.

유닛 테스트 타깃에서의 접근 수준

유닛 테스트 타깃이 있는 앱을 작성할 때는 테스트를 하기 위해 앱의 코드를 그 모듈에 제공해야 해요. 기본적으로는 open이나 public으로 표시한 엔티티만 다른 모듈에서 접근할 수 있어요. 그런데 프로덕트 모듈의 import 선언을 @testable 속성으로 표시하고 테스트를 활성화한 채로 그 모듈을 컴파일하면, 유닛 테스트 타깃이 어떤 internal 엔티티든 접근할 수 있게 돼요.

접근 제어 문법 (Access Control Syntax)

엔티티의 접근 수준을 정의하려면 doc:AccessControl#Access-Levels에 나열된 수정자 중 하나(예: public이나 private)를 그 엔티티 선언의 맨 앞에 놓으면 돼요. 예를 들어:

public class SomePublicClass {}
internal struct SomeInternalStruct() {}
private func somePrivateFunction() {}

위 코드는 SomePublicClass가 public, SomeInternalStruct가 internal, somePrivateFunction()이 private이라고 선언해요. 명시적인 접근 수준을 쓰지 않으면 기본 접근 수준 수정자는 doc:AccessControl#Default-Access-Levels에서 설명한 대로 internal이에요. 아래 코드에서 SomeInternalStruct는 암시적으로 internal이죠:

struct SomeInternalStruct() {}

커스텀 타입 (Custom Types)

커스텀 타입에 명시적인 접근 수준을 지정하고 싶다면 타입을 정의하는 시점에 해요. 그 새 타입은 접근 수준이 허용하는 곳 어디에서든 쓸 수 있어요. 예를 들어 file-private 클래스를 정의하면, 그 클래스는 file-private 클래스가 정의된 소스 파일 안에서만 프로퍼티의 타입이나 함수의 매개변수·반환 타입으로 쓸 수 있어요.

타입의 접근 수준은 그 타입 멤버(프로퍼티, 메서드, 이니셜라이저, 서브스크립트)의 기본 접근 수준에도 영향을 줘요. 타입의 접근 수준을 private이나 file private으로 정의하면 그 멤버들의 기본 접근 수준도 private이나 file private이 돼요. 타입의 접근 수준을 internal이나 public으로 정의하거나(명시적으로 지정하지 않고 internal 기본값을 쓰거나) 하면, 그 타입 멤버들의 기본 접근 수준은 internal이 돼요.

Important: public 타입은 기본적으로 public 멤버가 아니라 internal 멤버를 가져요. 타입 멤버를 public으로 만들고 싶다면 반드시 명시적으로 그래야 해요. 이 요구는 타입의 공개 API를 내가 명시적으로 공개하기로 선택한 것만 공개되게 하고, 타입의 내부 동작이 실수로 public API처럼 보이는 걸 막아줘요.

public class SomePublicClass {                   // explicitly public class
    public var somePublicProperty = 0            // explicitly public class member
    var someInternalProperty = 0                 // implicitly internal class member
    fileprivate func someFilePrivateMethod() {}  // explicitly file-private class member
    private func somePrivateMethod() {}          // explicitly private class member
}

class SomeInternalClass {                        // implicitly internal class
    var someInternalProperty = 0                 // implicitly internal class member
    fileprivate func someFilePrivateMethod() {}  // explicitly file-private class member
    private func somePrivateMethod() {}          // explicitly private class member
}

fileprivate class SomeFilePrivateClass {         // explicitly file-private class
    func someFilePrivateMethod() {}              // implicitly file-private class member
    private func somePrivateMethod() {}          // explicitly private class member
}

private class SomePrivateClass {                 // explicitly private class
    func somePrivateMethod() {}                  // implicitly private class member
}

튜플 타입 (Tuple Types)

튜플 타입의 접근 수준은 그 튜플에 쓰인 모든 타입의 접근 수준 중 가장 제한적인 것이 돼요. 예를 들어 서로 다른 두 타입(하나는 internal, 하나는 private)으로 튜플을 구성하면, 그 복합 튜플 타입의 접근 수준은 private이 돼요.

Note: 튜플 타입은 클래스, 구조체, 열거형, 함수처럼 독립적인 정의가 있는 게 아니에요. 튜플 타입의 접근 수준은 튜플 타입을 이루는 타입들로부터 자동으로 결정되고, 명시적으로 지정할 수 없어요.

함수 타입 (Function Types)

함수 타입의 접근 수준은 함수의 매개변수 타입과 반환 타입의 접근 수준 중 가장 제한적인 것으로 계산돼요. 함수의 계산된 접근 수준이 문맥상의 기본값과 맞지 않으면, 함수 정의에서 접근 수준을 명시적으로 지정해야 해요.

아래 예제는 someFunction()이라는 전역 함수를 함수 자체에 특정 접근 수준 수정자 없이 정의해요. 이 함수가 기본 접근 수준인 "internal"을 가질 거라 예상할 수도 있는데, 실제로는 그렇지 않아요. 사실 someFunction()은 아래처럼 쓰면 컴파일되지 않아요:

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

이 함수의 반환 타입은 위 doc:AccessControl#Custom-Types에서 정의한 커스텀 클래스 두 개로 이루어진 튜플 타입이에요. 두 클래스 중 하나는 internal, 다른 하나는 private으로 정의됐어요. 따라서 복합 튜플 타입의 전체 접근 수준은 private이 돼요(튜플을 이루는 타입들의 접근 수준 중 최솟값).

함수의 반환 타입이 private이므로, 함수 선언이 유효하려면 함수의 전체 접근 수준을 private 수정자로 표시해야 해요:

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

someFunction()의 정의를 public이나 internal 수정자로 표시하거나 internal 기본값을 쓰는 건 유효하지 않아요. 함수의 public이나 internal 사용자가 함수 반환 타입에 쓰인 private 클래스에 적절한 접근 권한이 없을 수 있으니까요.

열거형 타입 (Enumeration Types)

열거형의 개별 케이스는 자동으로 자신이 속한 열거형과 같은 접근 수준을 받아요. 개별 열거형 케이스에 다른 접근 수준을 지정할 수는 없어요.

아래 예제에서 CompassPoint 열거형은 명시적으로 public 접근 수준을 가져요. 따라서 열거형 케이스 north, south, east, west도 public 접근 수준이 돼요:

public enum CompassPoint {
    case north
    case south
    case east
    case west
}
원시 값과 연관 값 (Raw Values and Associated Values)

열거형 정의에서 원시 값이나 연관 값에 쓰는 타입은 최소한 열거형의 접근 수준만큼 높아야 해요. 예를 들어 internal 접근 수준의 열거형의 원시 값 타입으로 private 타입을 쓸 수는 없어요.

중첩 타입 (Nested Types)

중첩 타입의 접근 수준은 포함하는 타입과 같아요. 단, 포함하는 타입이 public인 경우는 달라요. public 타입 안에 정의된 중첩 타입은 자동으로 internal 접근 수준을 가져요. public 타입 안의 중첩 타입을 공개하려면 그 중첩 타입을 명시적으로 public으로 선언해야 해요.

서브클래싱 (Subclassing)

현재 접근 문맥에서 접근할 수 있고 서브클래스와 같은 모듈에 정의된 클래스라면 어떤 클래스든 서브클래싱할 수 있어요. 다른 모듈에 정의된 open 클래스도 서브클래싱할 수 있고요. 서브클래스는 슈퍼클래스보다 높은 접근 수준을 가질 수 없어요 — 예를 들어 internal 슈퍼클래스의 public 서브클래스를 쓸 수는 없죠.

또한 같은 모듈에 정의된 클래스의 경우, 어떤 접근 문맥에서 보이는 클래스 멤버(메서드, 프로퍼티, 이니셜라이저, 서브스크립트)든 오버라이드할 수 있어요. 다른 모듈에 정의된 클래스의 경우에는 open 클래스 멤버만 오버라이드할 수 있어요.

오버라이드는 상속받은 클래스 멤버를 슈퍼클래스 버전보다 더 접근 가능하게 만들 수 있어요. 아래 예제에서 클래스 AsomeMethod()라는 file-private 메서드를 가진 public 클래스예요. 클래스 BA의 서브클래스인데, 접근 수준은 "internal"로 낮춰졌죠. 그럼에도 BsomeMethod()의 원래 구현보다 높은 "internal" 접근 수준으로 someMethod()를 오버라이드해요:

public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {}
}

서브클래스 멤버가 자신보다 더 낮은 접근 권한을 가진 슈퍼클래스 멤버를 호출하는 것도 유효해요. 단, 슈퍼클래스 멤버에 대한 호출이 허용된 접근 수준 문맥 안에서 일어나야 해요(즉, file-private 멤버 호출은 슈퍼클래스와 같은 소스 파일 안에서, internal 멤버 호출은 슈퍼클래스와 같은 모듈 안에서):

public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {
        super.someMethod()
    }
}

슈퍼클래스 A와 서브클래스 B가 같은 소스 파일에 정의돼 있으므로, BsomeMethod() 구현에서 super.someMethod()를 호출하는 게 유효해요.

상수, 변수, 프로퍼티, 서브스크립트

상수, 변수, 프로퍼티는 자신의 타입보다 더 public일 수 없어요. 예를 들어 private 타입으로 public 프로퍼티를 쓰는 건 유효하지 않아요. 마찬가지로 서브스크립트도 자신의 인덱스 타입이나 반환 타입보다 더 public일 수 없어요.

만약 상수, 변수, 프로퍼티, 서브스크립트가 private 타입을 사용한다면, 그 상수·변수·프로퍼티·서브스크립트도 반드시 private으로 표시해야 해요:

private var privateInstance = SomePrivateClass()

게터와 세터 (Getters and Setters)

상수, 변수, 프로퍼티, 서브스크립트의 게터와 세터는 자동으로 자신이 속한 상수·변수·프로퍼티·서브스크립트와 같은 접근 수준을 받아요.

세터에는 대응하는 게터보다 낮은 접근 수준을 줄 수 있어서, 그 변수·프로퍼티·서브스크립트의 읽기-쓰기 범위를 제한할 수 있어요. 낮은 접근 수준은 varsubscript 도입자 앞에 fileprivate(set), private(set), internal(set), package(set)을 쓰면 지정돼요.

Note: 이 규칙은 저장 프로퍼티와 계산 프로퍼티 모두에 적용돼요. 저장 프로퍼티에 명시적인 게터와 세터를 쓰지 않더라도, Swift는 저장 프로퍼티의 백킹 저장소에 접근을 제공하는 암시적 게터와 세터를 생성해줘요. fileprivate(set), private(set), internal(set), package(set)을 써서 이 합성된 세터의 접근 수준을 계산 프로퍼티의 명시적 세터와 똑같은 방식으로 바꿀 수 있어요.

아래 예제는 문자열 프로퍼티가 수정된 횟수를 추적하는 TrackedString이라는 구조체를 정의해요:

struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}

TrackedString 구조체는 value라는 저장 문자열 프로퍼티를 정의하고, 초기 값은 ""(빈 문자열)이에요. 구조체는 또한 numberOfEdits라는 저장 정수 프로퍼티를 정의하는데, value가 수정된 횟수를 추적하는 데 쓰여요. 이 수정 추적은 value 프로퍼티의 didSet 프로퍼티 옵저버로 구현되는데, value 프로퍼티가 새 값으로 설정될 때마다 numberOfEdits를 1씩 증가시켜요.

TrackedString 구조체와 value 프로퍼티는 명시적인 접근 수준 수정자를 제공하지 않으므로 둘 다 기본 접근 수준인 internal을 받아요. 그런데 numberOfEdits 프로퍼티의 접근 수준은 private(set) 수정자로 표시해요. 이는 프로퍼티의 게터는 여전히 기본 접근 수준인 internal이지만, 세터는 TrackedString 구조체의 일부인 코드 안에서만 설정할 수 있다는 뜻이에요. 이렇게 하면 TrackedString은 내부적으로 numberOfEdits 프로퍼티를 수정할 수 있지만, 구조체 정의 밖에서 이 프로퍼티를 쓸 때는 읽기 전용 프로퍼티처럼 보여줘요.

TrackedString 인스턴스를 만들고 문자열 값을 몇 번 수정하면, numberOfEdits 프로퍼티 값이 수정 횟수에 맞춰 갱신되는 걸 볼 수 있어요:

var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")
// Prints "The number of edits is 3".

numberOfEdits 프로퍼티의 현재 값은 다른 소스 파일에서도 조회할 수 있지만, 다른 소스 파일에서 그 프로퍼티를 수정할 수는 없어요. 이 제한이 TrackedString의 수정 추적 기능 구현 세부사항을 보호하면서도, 그 기능의 한 측면에는 편리하게 접근할 수 있게 해줘요.

필요하다면 게터와 세터 모두에 명시적인 접근 수준을 지정할 수도 있어요. 아래 예제는 TrackedString 구조체가 명시적으로 public 접근 수준으로 정의된 버전을 보여줘요. 따라서 구조체의 멤버들(numberOfEdits 프로퍼티 포함)은 기본적으로 internal 접근 수준을 가져요. publicprivate(set) 접근 수준 수정자를 조합하면 구조체의 numberOfEdits 프로퍼티의 게터는 public, 세터는 private으로 만들 수 있어요:

public struct TrackedString {
    public private(set) var numberOfEdits = 0
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    public init() {}
}

이니셜라이저 (Initializers)

커스텀 이니셜라이저에는 자신이 초기화하는 타입보다 작거나 같은 접근 수준을 지정할 수 있어요. 유일한 예외는 required 이니셜라이저(doc:Initialization#Required-Initializers에서 정의)인데, required 이니셜라이저는 자신이 속한 클래스와 같은 접근 수준이어야 해요.

함수·메서드의 매개변수와 마찬가지로, 이니셜라이저 매개변수의 타입도 이니셜라이저 자신의 접근 수준보다 더 private일 수 없어요.

기본 이니셜라이저 (Default Initializers)

doc:Initialization#Default-Initializers에서 설명했듯이, Swift는 모든 프로퍼티에 기본 값을 제공하면서 자기 자신의 이니셜라이저를 하나도 제공하지 않는 구조체나 기반 클래스에 대해 인자 없는 기본 이니셜라이저를 자동으로 제공해요.

기본 이니셜라이저는 자신이 초기화하는 타입과 같은 접근 수준을 가져요. 단, 그 타입이 public으로 정의된 경우는 예외예요. public으로 정의된 타입의 경우 기본 이니셜라이저는 internal로 간주돼요. public 타입이 다른 모듈에서 사용될 때 인자 없는 이니셜라이저로 초기화되게 하려면, 타입 정의의 일부로 public 무인자 이니셜라이저를 직접 제공해야 해요.

구조체 타입의 기본 멤버와이즈 이니셜라이저

구조체 타입의 기본 멤버와이즈 이니셜라이저는 구조체의 저장 프로퍼티 중 하나라도 private이면 private으로 간주돼요. 마찬가지로 저장 프로퍼티 중 하나라도 file private이면 이니셜라이저도 file private이 돼요. 그 외에는 이니셜라이저가 internal 접근 수준을 가져요.

위의 기본 이니셜라이저와 마찬가지로, public 구조체 타입이 다른 모듈에서 사용될 때 멤버와이즈 이니셜라이저로 초기화되게 하려면, 타입 정의의 일부로 public 멤버와이즈 이니셜라이저를 직접 제공해야 해요.

프로토콜 (Protocols)

프로토콜 타입에 명시적인 접근 수준을 지정하고 싶다면 프로토콜을 정의하는 시점에 해요. 이렇게 하면 특정 접근 문맥 안에서만 채택할 수 있는 프로토콜을 만들 수 있어요.

프로토콜 정의 안의 각 요구사항의 접근 수준은 자동으로 프로토콜과 같은 접근 수준으로 설정돼요. 프로토콜 요구사항을 자신이 속한 프로토콜과 다른 접근 수준으로 설정할 수는 없어요. 이는 프로토콜을 채택한 어떤 타입에서든 프로토콜의 모든 요구사항이 보이도록 보장해줘요.

Note: public 프로토콜을 정의하면, 프로토콜의 요구사항은 구현될 때 public 접근 수준을 요구해요. 이 동작은 public 타입 정의가 그 타입 멤버에 대해 internal 접근 수준을 의미하는 다른 타입들과 달라요.

프로토콜 상속 (Protocol Inheritance)

기존 프로토콜에서 상속받는 새 프로토콜을 정의하면, 새 프로토콜은 최대 상속받는 프로토콜과 같은 접근 수준을 가질 수 있어요. 예를 들어 internal 프로토콜에서 상속받는 public 프로토콜을 쓸 수는 없어요.

프로토콜 준수 (Protocol Conformance)

타입은 자신보다 낮은 접근 수준의 프로토콜을 준수할 수 있어요. 예를 들어 다른 모듈에서 쓸 수 있는 public 타입을 정의하되, 그 타입의 internal 프로토콜 준수는 internal 프로토콜이 정의된 모듈 안에서만 쓸 수 있게 할 수 있어요.

타입이 특정 프로토콜을 준수하는 문맥은 타입의 접근 수준과 프로토콜의 접근 수준 중 최솟값이에요. 예를 들어 타입이 public인데 준수하는 프로토콜이 internal이라면, 그 프로토콜에 대한 타입의 준수도 internal이 돼요.

타입을 작성하거나 확장해서 프로토콜을 준수하게 할 때는, 각 프로토콜 요구사항에 대한 타입의 구현이 최소한 그 프로토콜에 대한 타입의 준수와 같은 접근 수준을 갖도록 해야 해요. 예를 들어 public 타입이 internal 프로토콜을 준수한다면, 각 프로토콜 요구사항에 대한 타입의 구현은 최소한 internal이어야 해요.

Note: Swift에서, 그리고 Objective-C에서도, 프로토콜 준수는 전역적이에요 — 같은 프로그램 안에서 타입이 두 가지 다른 방식으로 같은 프로토콜을 준수하는 것은 불가능해요.

익스텐션 (Extensions)

클래스, 구조체, 열거형이 사용 가능한 어떤 접근 문맥에서든 그 타입을 익스텐션할 수 있어요. 익스텐션에서 추가한 타입 멤버는 원래 타입 선언에 있는 멤버와 같은 기본 접근 수준을 가져요. public이나 internal 타입을 익스텐션하면 추가하는 새 타입 멤버는 기본적으로 internal 접근 수준이 돼요. file-private 타입을 익스텐션하면 새 멤버는 기본적으로 file private이고, private 타입을 익스텐션하면 새 멤버는 기본적으로 private이에요.

대안으로, 익스텐션을 명시적인 접근 수준 수정자(예: private)로 표시해서 익스텐션 안에 정의된 모든 멤버의 새 기본 접근 수준을 설정할 수도 있어요. 이 새 기본값은 익스텐션 안에서 개별 타입 멤버마다 덮어쓸 수도 있어요.

프로토콜 준수를 추가하려고 익스텐션을 쓰는 경우에는 익스텐션에 명시적인 접근 수준 수정자를 제공할 수 없어요. 대신 프로토콜 자신의 접근 수준이 익스텐션 안의 각 프로토콜 요구사항 구현의 기본 접근 수준을 제공해요.

익스텐션 안의 private 멤버

익스텐션이 익스텐션 대상이 되는 클래스, 구조체, 열거형과 같은 파일에 있으면, 익스텐션 안의 코드가 원래 타입 선언의 일부로 작성된 것처럼 동작해요. 그래서 다음이 가능해요:

  • 원래 선언에서 private 멤버를 선언하고, 같은 파일에 있는 익스텐션에서 그 멤버에 접근한다.
  • 한 익스텐션에서 private 멤버를 선언하고, 같은 파일에 있는 다른 익스텐션에서 그 멤버에 접근한다.
  • 익스텐션에서 private 멤버를 선언하고, 같은 파일에 있는 원래 선언에서 그 멤버에 접근한다.

이 동작 덕분에 타입에 private 엔티티가 있든 없든, 코드를 정리하는 데 익스텐션을 같은 방식으로 활용할 수 있어요. 예를 들어 다음과 같은 간단한 프로토콜이 있다고 해볼게요:

protocol SomeProtocol {
    func doSomething()
}

이렇게 익스텐션으로 프로토콜 준수를 추가할 수 있어요:

struct SomeStruct {
    private var privateVariable = 12
}

extension SomeStruct: SomeProtocol {
    func doSomething() {
        print(privateVariable)
    }
}

제네릭 (Generics)

제네릭 타입이나 제네릭 함수의 접근 수준은, 제네릭 타입·함수 자신의 접근 수준과 그 타입 매개변수에 대한 어떤 타입 제약의 접근 수준 중 최솟값이에요.

타입 별칭 (Type Aliases)

정의하는 타입 별칭은 접근 제어 측면에서 별개의 타입으로 취급돼요. 타입 별칭은 자신이 별칭하는 타입의 접근 수준보다 작거나 같은 접근 수준을 가질 수 있어요. 예를 들어 private 타입 별칭은 private, file-private, internal, public, open 타입을 별칭할 수 있지만, public 타입 별칭은 internal, file-private, private 타입을 별칭할 수 없어요.

Note: 이 규칙은 프로토콜 준수를 충족시키는 데 쓰이는 연관 타입의 타입 별칭에도 적용돼요.

더 알아보기