Swift – Access Control

Swift – Access Control

액세스 컨트롤은 다른 모듈이나 다른 소스파일로부터 코드의 일부에 접근하는 것을 제한하는 것을 의미한다. 이 기능을 사용하면 구현 코드를 숨기거나, 다른 곳에서 액세스할 수 있는 API를 선별적으로 지정할 수 있다.

액세스 레벨은 개별타입(클래스, 구조체, 열거체)에 대해서 프로퍼티, 메소드, 이니셜라이저, 서브스크립팅등에 대해 적용할 수 있다. 프로토콜은 특정한 컨텍스트에 대해 제한될 수 있는데 이는 전역 상수나 변수, 함수와 유사한 특성을 가진다. 그외에도 Swift는 명시적인 액세스 컨트롤을 할 필요가 그리 많지 않다. 만약 단일 타깃 앱을 개발하고 있다면 명시적으로 액세스 컨트롤을 정의할 필요가 없다.

모듈과 소스파일

Swift의 액세스 컨트롤은 모듈과 소스파일을 기반으로 한다. 모듈은 코드가 배포되는 기본 단위이다. 프레임워크나 앱을 빌드하는 경우, 이는 하나의 단위로 묶이며, 다른 Swift 코드에서 import 키워드를 써서 반입될 수 있다.

각각의 빌드 타킷(앱번들, 프레임워크)는 Xcode에서 구분되는 모듈로 취급된다. 만약 여러 앱에서 공통적으로 쓰이는 코드들을 묶어서 프레임워크로 빌드했다면, 프레임 내의 모든 것은 독립된 모듈을 구성하는 부분이 된다.

소스 파일은 모듈 내에서의 개별 .swift 소스 파일이다. 비록 개별 타입들은 각각 분리된 파일에 정의하는 것이 일반적임에도, 하나의 파일에 이 모든 것들을 다 우겨 넣을 수도 있다.

엑세스 레벨

코드 내 엔티티에 대해서 Swift는 크게 세 가지 액세스 레벨을 제공한다. 이 엑세스 레벨은 엔티티가 정의되어 있는 소스 파일에 대해 상대적인데, 동시에 파일이 속해있는 모듈에 대해서도 상대적이다.

  1. public 액세스는 그 엔티티가 정의된 소스 파일이 속한 모듈 어디서든, 그리고 다른 모듈에서도 접근할 수 있다. 보통 프레임워크의 개방된 API를 작성할 때 선언한다.
  2. internal 액세스는 모듈 내에서만 액세스할 수 있다. 이는 앱이나 프레임워크의 내부 구조를 정의할 때 사용한다.
  3. private 액세스는 해당 소스 파일레서만 접근할 수 있도록 액세스를 제한한다. 특정 기능의 구현을 숨기고 싶을 때 이를 이용한다.

규칙

Swift는 아래와 같은 대원칙을 가진다.

자신보다 제한적인 엑세스 레벨을 가진 엔티티와 관련지어 정의될 수 없다.

예를 들어,

  • public인 변수는 internal, private 인 타입으로 정의될 수 없다. 왜냐면 실제 public 범위에서는 변수에 넣을 값의 타입이 보이지 않기 때문이다.
  • 함수는 파라미터나 리턴타입이 자신보다 낮은(더 제한된) 액세스 레벨로 정의될 수 없다. 역시 마찬가지로 외부에서 파라미터나 리턴타입이 정의되지 않은 것과 같은 상태로는 이를 쓸 수 없기 때문이다.

기본 엑세스 레벨

코드상의 모든 엔티티의 기본 엑세스 범위는 기본적으로 internal이다. 따라서 public 한 엔티티를 만드려면 명시적으로 public 키워드를 사용해서 선언해야 한다.

싱글 타깃

싱글 타깃 앱은 앱 자체가 하나의 모듈이고 외부에서 그 기능을 참조할 필요가 없다. 디폴트 액세스 레벨은 이 필요에 중심을 맞추고 있다. 따라서 단일 타깃 앱을 작성할 때에는 이런 액세스 컨트롤에 대해 고민할 필요가 없다. 대신에 특정한 코드의 일부분을 가틍 ㄴ앱 모듈 내에서 참조하지 못하게 숨기는 것은 가능하다.

프레임워크

프레임워크를 만들 때는 외부에서 접근가능한 API에 대해서 명시적으로 public 속성을 선언해주면 된다. import 되었을 경우, 반입한 쪽에서는 public 한 API만 보이게 된다.

액세스 컨트롤 문법

액세스 컨트롤 문법은 선언시 맨 앞에 public, internal, private을 쓴다. 사실 internal은 암묵적으로 항상 디폴트이기 때문에 생략해도 상관없으나 명ㅇ시적인 구분을 위해서 쓰기도 한다.

커스텀 타입

함수나 상수, 변수 외에도 커스텀 타입에 대해서도 모듈 외부에서 보일 것인지, 파일 외부에서도 보이지 않게 할 것인지를 결정할 수 있다. 이는 각각 class, struct, enum 앞에 액세스 컨트롤 지시어를 붙일 수 있다. 이 때 각 타입은 프로퍼티나 메소드별로도 개별적인 액세스 컨트롤을 정의할 수 있는데, 속성들의 액세스 컨트롤은 타입의 액세스 컨트롤을 따라간다.

예를 들어 private으로 정의된 클래스의 속성들은 기본적으로 모두 private이다. 대신 public으로 정의된 타입의 속성들은 디폴트로 internal 액세스 레벨을 갖는다.

튜플

튜플은 자동적으로 튜플을 구성하는 타입 중에서 가장 낮은 액세스 레벨에 맞춰진다. 하나의 멤버라도 private이면 해당 타입은 자동으로 private이 된다.

튜플은 그 자체로 명시적인 엑세스레벨을 가지지 않는다. 위에서 설명한 원칙대로 멤버의 타입 중에서 가장 낮은 액세스 레벨에 자동으로 맞춰진다.

함수

함수의 엑세스 레벨도 자동으로 파라미터 및 리턴 타입 중에서 가장 낮은 액세스 레벨로 맞춰진다. 역으로 명시적으로 더 낮은 액세스 레벨을 만들 수는 있지만 그 보다 높은 액세스레벨은 만들 수 없다.

대신 함수의 엑세스레벨은 기본적으로 internal 이므로,

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    //...
}

이 함수는 자동으로 private이 되는것이 아니라 컴파일 시에 에러가 난다. 디폴트로 internal이 선언되므로 반드시 명시적으로 private을 써주어야 컴파일된다.

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // ...
}

Enum Types

enum 타입의 모든 멤버는 개별적인 액세스 컨트롤이 불가능하고, 그들이 속한 타입 자체의 액세스 레벨을 그대로 따른다. 만약 rawValue나 연관값을 사용한다면, 그 연관값의 타입 역시 enum 타입 자체의 액세스보다 적어도 같거나 높아야 한다.

nested types

struct, enum 타입에서는 nested type이 쓰이는 경우가 종종 있는데, enum 타입이 private 이며 중첩 타입은 private이다. 만약 internal 이상의 enum 에서는 디폴트로 internal이며 public > public 이 되게하려면 명시적으로 중첩 타입을 public으로 선언해야 한다.

클래스 상속

상속받는 자식 클래스는 부모 클래스보다 높은 액세스 레벨을 가질 수 없다. 물론 경우에 따라서는 높은 액세스레벨의 자식 클래스에서 낮은 액세스 레벨의 부모 클래스의 멤버를 참조하는 것이 가능하다. 하지만 그것은 같은 모듈 내에서라든지 같은 파일 내에서 같은 문맥상의 제약이 따른다. 이 제약은 강제된다는 보장이 없으므로 이런 식의 상속은 할 수 없다.

비슷한 원리로 프로퍼티, 상수, 변수, 메소드들은 모두 그가 속한 타입보다 더 퍼블릭해질 수 없다. 비슷하게 함수나 서브스크립트는 클래스의 액세스 레벨 뿐만 아니라 리턴 타입이나 파라미터 타입의 액세스 레벨에도 제한을 받는다.

계산된 프로퍼티의 접근자는 일반 상수, 변수, 함수와 동일한 정책을 적용받는다.

흥미로운 점은 setter/getter의 액세스 컨트롤을 다르게 할 수 있다는 점이다. (내부 혹은 동일 소스내에서만 변경가능하게 한든지…) 이는 private(get), internal(get) 등의 제한자를 사용하여 변경할 수 있다. 아래 예시를 보면 numberOfEdits 는 set 시에는 private 하다.

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

이 소스에서 numberOfEdits 는 set에 대해서는 private이고, get에 대해서는 디폴트값인 internal이 된다. 따라서 numberOfEdits는 소스 파일 내부에서는 읽고 쓸 수 있으나, 소스 파일 외부의 같은 모듈에서는 읽기 전용이다. 또한 모듈외부에서는 보이지 않는다. (타입자체가 안 보임)

이를 public 하게 바꿔보다. 외부 모듈에서도 numberOfEdits가 읽기전용이며, value 값은 모듈외부에서도 변경하게하려면 다음과 같이 써야 한다.

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

    public init(){}
}

여기서 주의할 것이 init() 에 public을 붙이지 않으면 모듈 외부에서는 이 구조체 타입의 인스턴스를 만들 수 없게 된다.

이니셜라이저

이니셜라이저도 기본적으로 internal이나, 타입의 액세스레벨이 더 낮으면 더 낮아진다. 특히 인자를 받는 이니셜라이저는 다시 인자의 타입에도 영향을 받게 된다.

예외적으로 required 이니셜라이저는 타입의 액세스레벨과 동일해야 하며, 이 때 인자의 타입이 더 낮은 엑세스레벨을 가지고 있다면 컴파일 되지 않는다.

디폴트 이니셜라이저는 기본적으로 internal인데 명시적으로 쓰려면 public을 구현내에 써 주어야 한다.

멤버지정 이니셜라이저

구조체의 내부 구조는 기본적으로 private 한 것으로 간주되나, 실제로 멤버지정 이니셜라이저는 internal이다. 만약 public 해야 하면 이도 별도로 선언하여 구현해야 한다.

프로토콜

프로토콜은 조금 유별나다. 그 자체의 액세스 레벨도 있으나 제네릭, 부모의 레벨에 따라서 제한조건의 레벨과 같아야 한다. 그리고 모든 제한조건은 동일한 레벨이어야 한다. 따라서 제네릭 타입 조건의 엑세스 레벨이 변할 때 프로토콜의 범위도 같이 변하는 일은 없다.

타입에 적용되었을 때 타입은 프로토콜보다 작거나 같은 액세스레벨만을 가질 수 있다. 만약 프로토콜이 internal일 때 이를 따르는 타입을 public으로 정의할 수 없다.

확장

확장의 경우 기본적으로 internal이며, 원 타입이 public 일 때 public을 설정할 수 있다 역시 위의 예와 마찬가지로 확장은 원래 타입에 연관지어 정의되므로 원타입보다 높은 공개 범위를 가질 수 없다. 또한 확장 내에서 만들어지는 프로퍼티나 메소드는 모두 이 규칙을 넘을 수 없으며, 함수의 인자/리턴타입보다 넓을 수 없다.

제네릭

제네릭은 타입 파라미터로 올 수 있는 타입 중에서 최소한의 액세스레벨로 제한된다.

별칭(alias)

기본적으로 원래 가리키는 대상 타입과 동일하다. 그러나 특정 모듈/소스파일 내부에서만 별칭을 사용하고 싶을 수 있으므로 더 낮은 레벨을 정의하는 것이 가능하다.