Apple 제공 Swift 프로그래밍 가이드(3.1)의 Access Control 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
Access control 을 이용하면 다른 소스파일/모듈에 있는 코드에서 자신의 코드에 접근하는 범위를 제한할 수 있다. 자신의 코드 구현부를 숨기거나, 외부에서 사용할 인터페이스를 확실하게 지정하고 싶을 때 유용하다.
Access level 을 클래스, 구조체, ENUM 등의 타입에 각각 구체적으로 지정할 수 있으며, 타입 안에 속한 프로퍼티, 메서드, 이니셜라이저, 서브스크립트에까지도 지정할 수 있다. 프로토콜은 특정 컨텍스트(전역 상수, 변수, 함수 등)로 제한될 수 있다.
Default access level이 제공된다. 따라서 만약 단일 타겟 앱single-target app을 만든다면 access control level을 굳이 명시할 필요가 없다.
<NOTE> 이제부터는 access control이 적용되는 여러 측면들, 예를 들어 프로퍼티, 타입, 함수 등을 간략하게 통틀어 "엔터티들entities"이라고 부르겠다.
모듈과 소스파일
Swift에서의 access control model 은 모듈과 소스파일의 개념을 기반으로 한다.
모듈은 코드 배포의 단위이다. 프레임워크나 어플리케이션은 모듈 단위로 빌드되고 묶이며 import 키워드를 통해 다른 모듈에 import 될 수 있다.
Xcode의 빌드 타켓 (앱 번들, 프레임워크) 각각은 Swift에서 각각 개별적인 모듈로 취급된다. 만약 기존 코드 중 일부분만을 묶어서 독립적인 프레임워크로 만들었다면, 그 프레임워크 안에 정의된 모든 것들이 모여서 하나의 별개 모듈을 구성한다고 보면 된다.
소스 파일은 모듈 안의 Swift 소스 파일 하나를 가리킨다. (엄밀히는 앱/프레임워크 안의 파일 하나) 소스 파일 하나에는 타입 하나만 정의하는 것이 일반적이지만, 파일 하나에 여러 개의 타입, 함수 등을 정의하는 것도 가능하다.
Swift는 access control을 모듈과 소스파일 단위로 하기 때문에, 만약 클래스 A, B가 같은 파일에 정의되어 있다면, 클래스 A에서 클래스 B의 File-private 프로퍼티에 접근할 수도 있다.
Access Levels
엔터티를 위한 다섯 종류의 Access level 이 있다. Access level 은 엔터티가 정의된 소스 파일에 대해 상대적이며, 소스 파일이 속한 모듈에 대해서도 상대적이다.
Open access와 Public access : 내부 모듈이건 외부 모듈이건, 어떤 소스파일에서건, 엔터티에 접근 가능. 보통 프레임워크의 public 인터페이스를 위해서 사용한다. Open과 Public의 차이는 아래에서 설명.
Internal access : 엔터티가 정의된 모듈에 속한 모든 소스 파일에서 엔터티에 접근 가능. 외부 모듈에서는 접근 불가. 보통 앱이나 프레임워크의 Internal 구조체를 정의할 때 사용한다.
File-private access : 엔터티가 정의된 소스 파일에서만 엔터티에 접근 가능. 파일 전체에서 사용되는 기능의 자세한 구현을 숨겨야 할 때 사용한다.
Private access : 엔터티의 접근 범위를 enclosing declaration으로 제한. 엔터티가 정의된 enclosing scope (ex: 함수, 클래스) 에서만 사용되는 기능의 자세한 구현을 숨겨야 할 때 사용한다.
Open access 가 가장 높은 access Level(=가장 덜 제한적)을 가지고, Private access 가 가장 낮은 access Level(=가장 제한적)을 가진다.
Open access 는 클래스와 클래스 멤버들에만 적용된다. Open access 가 Public access 와 다른 점을 살펴보자.
Public Class : 이 클래스가 정의된 모듈 내에서만 subclassing과 override 허용
Open Class : 어디서든(외부 모듈에서도) subclassing과 override 허용
여담이지만 Swift2에서는 Public, Internal, Private 3종류 밖에 없었다. 따라서 Private인 요소라도 같은 파일 내에만 있으면 접근이 가능했는데, 그런 점을 보완하여 File-private와 Private로 나뉜 것 같다.
<Access level 의 기본 원칙>
Swift에서의 Access level은 다음과 같은 기본 원칙을 따른다.
No entity can be defined in terms of another entity that has a lower (more restrictive) access level.
즉, 엔터티는 자신보다 더 제한적인 Access Level로 정의되면 안 된다는 말이다.
(*어떻게 번역할지가 애매하여 원문을 첨부했습니다)
예를 들어 (*번역이 쉽지 않아서 원문을 첨부했습니다)
A public variable cannot be defined as having an internal, file-private or private type, because the type might not be available everywhere that the public variable is used.
(public 변수는 internal이나 private 타입으로는 정의될 수 없습니다. 이런 타입들은 public 변수가 사용되는 모든 곳에서 다 사용가능하지는 않기 때문입니다. public 으로 만들 변수의 타입이 internal/file-private/private Access Level로 설정된 타입인 경우에는 외부 모듈/다른 소스 파일에서 해당 타입 자체에 접근할 수가 없기 때문에 public 변수로 사용할 수가 없게됩니다. 그런 경우를 설명하고 있는 것 같습니다.)
A function cannot have a higher access level than its parameter types and return type, because the function could be used in situations where its constituent types are not available to the surrounding code.
(함수는 자신의 파라미터 타입과 리턴 타입보다 더 높은 Access Level을 가질 수 없습니다. public 함수의 파라미터/리턴 타입이 internal/private Acess level로 설정된 타입이라면 외부 모듈/다른 소스 파일에서는 이 함수가 public이라도 사용할 수가 없을 것입니다. 그런 경우를 말하고 있는 것 같습니다)
<Default Access Levels>
모든 엔터티는 default로 internal Access Level이다. (아주 적은 예외가 있긴 한데 나중에 설명하겠다.) 따라서 internal Access Level이 아닌 public이나 private Access Level로 설정하고 싶다면 코드에 직접 명시해야 한다.
<싱글 타켓 앱Single-Target App을 위한 Access Level>
간단히 싱글 타겟 앱을 만들 경우에는 일반적으로 앱 안에 필요한 코드가 전부 포함되어 있으며 외부 모듈에서 그 코드를 사용할 필요는 없다. 이런 요구사항을 반영한 결과 default access level이 internal이 된 것이다. 따라서 싱글 타켓 앱을 만들 때는 access level을 따로 신경쓰지 않아도 된다. 물론 private access level을 통해 코드 중 일부분의 세부구현을 다른 코드가 알지 못하도록 만들 수는 있다.
<프레임워크를 위한 Access Level>
프레임워크를 만들 때는 외부 모듈에서 사용할 수 있도록 인터페이스에 open/public access level을 명시해준다. 프레임워크의 public-facing interface가 프레임워크를 위한 application programming interface (API) 가 되는 것이다.
<NOTE> 프레임워크 내부의 상세 구현부의 access level은 따로 명시하지 않으면 default level, 즉 internal이다. 이것을 private로 만들어서 프레임워크 내부의 다른 소스 파일에서는 알 수 없도록 만들 수도 있고, open/public으로 만들어서 프레임워크 API의 한 부분으로 만들어버릴 수도 있다.
<유닛 테스트Unit Test 타겟을 위한 Access Level>
유닛 테스트 타겟을 가진 앱에서는 앱의 코드가 테스트 타겟(즉, 외부모듈)에서 접근 가능해야 할 것이다. 따로 명시를 해주지 않으면 default access level은 internal이기 때문에 테스트 타겟에서는 앱의 코드에 접근할 수가 없다. 하지만 유닛 테스트 타겟에 한해서 특별히 internal access level을 가진 엔터티에도 접근할 수 있는 방법을 제공한다. @testable 속성과 함께 product 모듈을 import하고 testing enabled를 설정하여 product 모듈을 컴파일하면 된다.
Access Control 문법
엔터티에게 access level 을 설정하려면 다음과 같이 한다.
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}
public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}
internal 의 경우에는 명시하지 않더라도 default로 설정이 된다.
class SomeInternalClass {} // implicitly internal
let someInternalConstant = 0 // implicitly internal
Custom Types
커스텀 타입을 위한 access level을 정해주고 싶다면 타입을 정의할 때 같이 해주면 된다.
예를 들어 file-private 클래스를 하나 정의한다면, 그 클래스는 자신이 정의되어 있는 소스 파일 내에서만 접근이 가능해진다.
타입의 access control level 은 그 타입의 멤버들의 default access level에 영향을 미친다. (여기서 멤버란 프로퍼티, 메서드, 이니셜라이저, 메서드, 서브스크립트 등)
타입의 access level 이 private --> 멤버들은 default로 private
타입의 access level 이 file-private --> 멤버들은 default로 file-private
타입의 access level 이 internal / public --> 멤버들은 default로 internal
<NOTE> 타입의 access level이 public이라도 멤버의 default access level은 internal이라는 것을 기억해야 한다. 이 규칙은 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
public func somePublicMethod() {} // 의미없는 짓이다.. 외부모듈에서는 이 클래스를 모른다
}
fileprivate class SomeFilePrivateClass { // explicitly file-private class
func someFilePrivateMethod() {} // implicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
internal var someInternalProperty = 0 // 의미없는 짓이다.. 다른 소스파일에서는 이 클래스를 모른다
}
private class SomePrivateClass { // explicitly private class
func somePrivateMethod() {} // implicitly private class member
}
<Tuple Types>
튜플의 access level 은 튜플을 구성하는 타입 중 가장 제한적인 access level 에 맞춰진다. 예를들어 튜플을 구성하는 타입이 하나는 internal access level, 하나는 private access level을 가진다면 이 튜플의 access level 은 private 다.
<NOTE> 튜플의 타입 자체에 명시적으로 access level을 줄 수는 없다. 튜플을 구성하는 타입들의 access level에 따라 자동으로 결정되는 방식이다.
<Function Types>
함수의 access level 은 파라미터 타입, 리턴 타입들 중 가장 제한적인 access level과 매치되어야 한다. 만약 매치가 되지 않는다면 함수의 정의부에 매치되는 access level을 명시해주어야 한다.
예제를 보자. someFunction이라는 글로벌 함수가 있다. 이 함수는 access level을 명시하고 있지 않으므로 default로 internal level이 된다. 따라서 아래의 경우 컴파일이 되지 않는다.
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 함수의 access level -> Internal
// 함수 리턴타입의 access level -> Private
}
함수의 리턴 타입이 private 이므로 이 함수 자체도 private가 되어야 한다. 고쳐보도록 하자.
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 함수 자체의 access level == 리턴 타입의 access level
}
<Enumeration Types>
ENUM의 case 각각은 ENUM의 access level과 자동으로 같아진다. 개별적인 case 하나에만 다른 access level 을 주는 것은 불가능하다.
아래 예제를 보자. CompassPoint ENUM은 public 이다. 따라서 case 모두 public이다.
public enum CompassPoint {
case north
case south
case east
case west
}
<Raw Values and Associated Values>
ENUM에서 사용되는 raw values, associated values 의 타입은 ENUM 자체의 access level과 같거나 더 높아야 한다. 예를 들면, internal ENUM에서 private level 타입의 raw-value 는 사용할 수 없다.
<Nested types>
private type 안의 nested type -> 자동으로 private level
file-private type 안의 nested type -> 자동으로 file-private level
public / internal type 안의 nested type -> 자동으로 internal level
Subclassing
서브클래스는 슈퍼클래스보다 높은 access level을 가질 수 없다. 예를 들어, internal 클래스를 상속받아 public 클래스로 만들 수는 없다.
슈퍼클래스의 멤버(메서드, 프로퍼티, 이니셜라이저, 서브스크립트 등)를 서브클래스에서 override 할 때는 원래의 access level 보다 더 높게 만들 수 있다. 아래 예제를 통해 살펴보자.
public class A {
fileprivate func someMethod() {}
private func somePrivateMethod() {}
}
internal class B: A {
// Public 클래스를 상속받아서 Internal 클래스로 정의
override internal func someMethod() {}
// file-private 메서드를 internal로 오버라이드
override internal func somePrivateMethod() {}
// 에러. A의 private 메서드는 A를 벗어나면 접근불가. 오버라이드 불가능
}
access level이 허용되는 범위 내에서는 서브클래스의 멤버가 자신보다 access level이 낮은 슈퍼클래스의 멤버를 호출하는 것도 유효하다.
public class A {
internal func someInternalMethod() {}
fileprivate func someMethod() {}
private func somePrivateMethod() {}
}
internal class B: A {
override internal func someMethod() {
// file-private 메서드를 internal로 오버라이드
super.someInternalMethod()
// 이건 당연히 된다
super.someMethod()
// Internal 멤버안에서 슈퍼클래스의 file-private 멤버를 호출할 수 있다
// file-private이니 같은 소스 파일 내에 있을 때만 가능하다
super.somePrivateMethod()
// 이 경우는 에러
// A의 Private멤버는 A를 벗어나면 다른 곳에서 접근불가
// 서브클래스에서도 슈퍼클래스의 Private멤버에 대해 알 수 없다
}
}
상수, 변수, 프로퍼티, 서브스크립트
상수, 변수, 프로퍼티는 자신의 타입보다 더 높은 access level 을 가질 수 없다. Private 타입을 가지고 Public 프로퍼티를 만들 수는 없는 것이다. 서브스크립트 또한 index 타입이나 return 타입보다 더 높은 access level 을 가질 수 없다.
만약 상수, 변수, 프로퍼티, 서브스크립트가 private 타입으로 쓰인다면 private 라고 명시해주어야 한다.
private var privateInstance = SomePrivateClass()
<Getters and Setters>
상수, 변수, 프로퍼티, 서브스크립트의 Getter, Setter는 자동으로 자신이 속해있는 곳의 access level과 동일한 access level 을 가진다.
변수의 read-write 스코프 제한을 위해서 setter에는 getter보다 더 낮은 access level을 줄 수도 있다. (예를들면 값을 가져가는 건 어디서건 자유롭게 할 수 있지만 값을 셋팅하는 건 오직 같은 소스 파일 내에서만 가능하게 하는 식이다) 문법은 private(set) 이나 internal(set) 같은 키워드를 이용한다. 나중에 예제로 확인하자.
<NOTE> 이 룰은 stored / computed 프로퍼티 둘 다에 적용된다. stored 프로퍼티의 getter, setter를 따로 명시하지 않아도 적용되기 때문에, 이것들의 access level 를 바꾸려면 stored 프로퍼티의 getter, setter를 만들고 access level 을 명시해야 한다.
아래 예제는 TrackedString 이라는 구조체이다. String 프로퍼티가 변경된 횟수를 세고 있다.
struct TrackedString { // default access level : internal
// setter : private, getter : default(internal)
// 구조체 밖에서는 값을 고칠 수 없다
// 같은 모듈 내에만 있으면 값을 알 수 있다
private(set) var numberOfEdits = 0
// default access level : internal
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
var stringToEdit = TrackedString()
stringToEdit.value = "초기 스트링"
stringToEdit.value += "스트링에 다른 스트링을 붙임"
stringToEdit.value += "한 번 더 붙임"
print("number of edits : \(stringToEdit.numberOfEdits)")
// "number of edits : 3"
// numberOfEdits 프로퍼티를 Public으로 만들어보자
public struct TrackedString {
// Public 으로 만들면 내부 멤버들은 default로 Internal이 된다
// setter : private, getter : Public
// public을 명시하지 않으면 getter는 디폴트로 Internal
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
이니셜라이저
커스텀 이니셜라이저의 access level 은 자신이 속한 타입의 access level 과 같거나 더 낮을 수 있다. 단 하나의 예외로는 required 이니셜라이저가 있는데, 이 경우에는 access level 이 자신이 속한 클래스의 access level 과 정확히 같아야 한다.
함수에서와 마찬가지로, 이니셜라이저 역시 파라미터의 타입은 이니셜라이저 자체의 access level보다 더 낮은 access level을 가질 수 없다.
<Default Initializers>
디폴트 이니셜라이저는 자신이 속한 타입과 같은 access level을 가진다. 그러나 속한 타입이 Public으로 정의되었을 경우 디폴트 이니셜라이저는 Internal이다. 따라서 Public 디폴트 이니셜라이저가 필요한 경우에는, 디폴트 이니셜라이저 대신에 똑같은 기능의 이니셜라이저를 명시적으로 만들어놓고 Public으로 정의해야 한다. (=스스로 만들어야 한다)
<Default Memberwise Initializers for Structure Types>
구조체 타입을 위해 제공되었던 디폴트 memberwise 이니셜라이저는 기본적으로 Internal이다. 그러나 구조체의 stored 프로퍼티가 하나라도 private인 경우에는 private 가 된다.
디폴트 이니셜라이저와 마찬가지로, Public인 memberwise 이니셜라이저가 필요하다면 스스로 만들어야 한다.
프로토콜
프로토콜 타입에 명시적으로 access level을 할당하고 싶다면 프로토콜을 정의할 때 하면 된다.
프로토콜에 정의된 요구사항들의 access level 은 프로토콜 자체의 access level 과 같다. 같지 않게는 할 수 없다. 프로토콜의 요구사항은 그 프로토콜을 따르는 어떤 타입에서든 전부 접근 가능해야 하기 때문이다.
<note> Public Protocol의 요구사항도 전부 Public이다. (Internal이 아니다!)
<프로토콜 상속>
기존 프로토콜을 상속받아서 새로운 프로토콜을 정의한다면, 새로운 프로토콜은 기존 프로토콜의 access level과 같거나 더 낮은 access level로 만들어야 한다. Internal 프로토콜을 상속받아서 Public 프로토콜을 만들 수는 없다.
<프로토콜 준수(따르기)>
타입은 자신과 같거나 더 낮은 access level을 가지는 프로토콜을 따를 수 있다. 예를들면 Public 타입은 Internal 프로토콜을 따를 수 있다. 그러나 이 경우 구현한 프로토콜은 Internal 범위에서만 사용될 것이다. (프로토콜이 Internal이니..)
<NOTE> Swift에서도 Objective-C와 마찬가지로 프로토콜 준수protocol conformance는 글로벌이다. 따라서 같은 프로그램 내에서는 타입 하나가 하나의 프로토콜을 두 가지 방식으로 따르게 할 수는 없다.
확장
access 범위가 허용되는 내에서 클래스, 구조체, ENUM을 확장할 수 있다. 확장 내부에서 추가되는 타입 멤버type member는 기존 타입에서의 타입 멤버와 같은 수준의 default access level을 가지게 된다. 만약 Public/Internal 타입을 확장하고 그 안에서 새로운 타입 멤버를 추가한다면 그것은 default level, 즉 Internal이 된다. (Private 타입의 경우 새로운 타입 멤버는 Private이다)
확장을 정의하면서 access level 을 명시적으로 지정할 수도 있다. private extension 같은 식으로 명시하면 그 안에서 정의되는 모든 멤버는 새로운 default access level 을 가지게 된다. 이 새로운 default 들은 타입 멤버들에 대한 다른 확장 안에서 다시 오버라이드 될 수 있다.
<확장을 통해 프로토콜 따르기>
프로토콜을 따르게 하기 위해 확장을 하는 경우에는 확장의 access level을 명시할 수 없다. 확장 안에서의 프로토콜 구현부의 default access level 은 프로토콜의 access level 에 따라 결정된다.
제네릭
제네릭 타입/함수의 access level 은 제네릭 타입/함수 자체의 access level 과 타입 파라미터에 걸린 타입제약의 access level 중 가장 낮은 access level로 결정된다. 예를들어 Public으로 정의된 제네릭 타입이 있다고 해보자. 그 제네릭 타입은 타입 파라미터 T를 가진다. 이 T에 Internal 타입을 넘겨서 변수를 하나 만들었다. 이 변수는 Internal이며 외부 모듈에서 접근할 수 없다.
Type Aliases
타입 별칭은 그것이 가리키는 실제 타입과 같거나 더 낮은 access level 을 가질 수 있다. Internal 타입을 위한 타입 별칭으로는 Internal, File-private, Private 가 가능하고, Private 별칭인 경우에는 다른 소스 파일에서는 그 별칭을 사용할 수 없다.
<NOTE> 이 룰은 안전한 프로토콜 준수를 위한 associated type의 타입 별칭에도 똑같이 적용된다.
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Advanced Operators (0) | 2017.06.05 |
---|---|
Generics (0) | 2017.03.29 |
Protocols (3) (2) | 2017.03.25 |
Protocols (2) (0) | 2017.03.25 |
Protocols (1) (0) | 2017.03.23 |