Access Control
접근 제어(Access Control)는 다른 소스 파일(source file)이나 모듈(module)에서 오는 코드와 우리의 코드 간에 접근을 제한을 할 수 있는 기능이다. 이러한 특성을 통해 코드의 상세 구현부를 숨길 수 있고, 허용된 기능만 사용하는 인터페이스를 제공할 수 있다.
각 타입 ( class
, struct
, enum
) 혹은 타입 내부의 프로퍼티, 메서드, 이니셜라이저, 서브스크립트 등에 특정한 접근 수준(access level)을 할당할 수 있다. 프로토콜은 전역 상수, 변수, 함수처럼 특정 맥락(context)에 의해 제한될 수 있다.
다양한 접근 수준을 제공함과 더불어, Swift는 매번 접근 수준을 명시해주는 수고로움을 덜어주기 위해 기본적으로 접근 수준을 지정해준다. 즉 아무 것도 명시하지 않으면 internal
의 접근 수준을 가지게 되는 것이다. 또한 Single-target app을 작성하는 경우, 굳이 접근 제어 수준을 지정하지 않고도 작업을 수행할 수 있다.
Modules and Source Files
Swift의 접근 제어 모델은 묘듈과 소스 파일의 개념에 기반한다.
여기서 모듈이란, 배포할 코드 묶음의 단위를 말한다. 프레임워크나 어플리케이션이 모듈 단위가 될 수 있고 import
키워드를 통해 이를 다른 모듈에서 가져올 수 있다. (ex. import UIKit
하는 것)
소스 파일은 단일 swift 소스 코드 파일을 의미한다. 즉 .swift
라고 명시되어 있는 파일 하나를 의미하는 것이다. 개별 타입들은 개별 소스파일 내에서 정의되어야 하지만, 하나의 단일 소스 파일은 다양한 타입, 함수들을 정의할 수 있다.
Access Level
Swift는 5가지의 다른 접근 수준을 제공한다. 이 접근 수준은 엔티티(=저장되고, 관리되어야 하는 데이터의 집합)가 정의된 소스 파일과 관계가 있고, 또한 소스 파일을 갖고 있는 모듈에 대해서도 관계가 있다.
Open & public
open
과 public
키워드를 통해 지정된 요소들은 어디에서나 사용될 수 있다. 모듈이 정의된 소스 파일 내에서나, 다른 모듈을 가져다 쓰는 소스파일 등에서 사용 가능하다. 대표적으로 open
또는 public
키워드는 주로 프레임워크에서 외부와 연결될 인터페이스를 구현할 때 많이 사용된다. open
과 public
의 차이는 아래서 다뤄보자.
Internal
Internal
키워드는 앞서 봤듯이 접근 수준을 명시하지 않으면 기본적으로 할당되는 수준이다. Internal
이 명시된 요소는 소스 파일이 속해 있는 모듈 어디에서든 쓰일 수 있지만, 해당 모듈 외부의 소스 파일에서는 사용될 수 없다. 대표적으로 app이나 프레임워크의 내부 구조를 정의할 때 사용된다.
file-private
fileprivate
키워드는 해당 요소가 정의되어 있는 소스 파일 내에서만 사용 가능하도록 해준다. 해당 소스 파일 외부에서 값이 변경되거나 함수를 호출하면 부작용이 생길 수 있는 경우에 사용하면 좋다.
private
private
키워드는 해당 요소가 선언되어 있는 그 내부에서 사용 가능하도록 제한하고 있고, 같은 파일 내에서 확장(extention)하는 경우에도 사용 가능하도록 제한한다. 즉, 기능을 정의하고 구현한 범위 내에서만 사용할 수 있다.
open
키워드가 가장 높은(제한이 적은) 접근 수준이고 private
키워드가 가장 낮은(제한이 많은) 접근 수준이다.
open
키워드는 클래스와 클래스 멤버들에만 적용 가능하다는 점에서 public
과 차이가 있다.
Guiding Principle of Access Levels
Swift의 접근 수준은 다음의 윈칙을 따른다.
상위 요소보다 하위 요소가 더 높은 접근 수준을 가질 수 없다.
즉, public
변수는 internal
, file-private
,private
수준을 가지는 상태로 정의할수 없다. 왜냐하면 public
변수가 사용되는 모든 곳에서 해당 접근 수준을 사용할 수 없기 때문이다.
함수도 파라미터 타입이나 반환 타입보다 높은 접근 수준을 가지면 안된다. 왜냐하면 함수는 구성 타입들이 코드 밖에서 사용될 수 없는 상황이 있기에 비슷하거나 낮은 접근 수준을 가져야 한다.
Default Access Levels
코드 내의 모든 요소들은 접근 수준을 명시하지 않아도 internal
로 지정된다. 이 덕분에 많은 경우에서 일일이 접근 수준을 지정하지 않아도 코드를 작성할 수 있다.
Access Levels for Single-Target Apps
Single-target app을 작성할 때, 앱 내의 코드는 앱 모듈 밖에서 사용할 수 있도록 작성하지 않아도 된다. 그 덕분에 기본 접근 수준으로도 작성 가능하다. 하지만 그 내부에서 앱 모듈 내의 다른 코드로 부터 상세 구현을 숨기기 위해 fileprivate
나 private
키워드를 사용할 수는 있다.
Access Levels for Frameworks
프레임워크를 개발할 때, 앱에서 프레임워크를 import하여 다른 모듈에서도 사용할 수 있게 해줘야 한다. 이러한 public-facing 인터페이스는 프레임워크에서의 API와 같은 것이다.
Access Levels for Unit Test Targets
유닛 테스트를 할 때, 앱 내의 코드는 모듈이 테스트 가능하도록 만들어줘야 한다. 기본적으로 open
또는 public
을 통해 다른 모듈에서 접근 가능하도록 해주었지만, 유닛 테스트 타겟은 internal
요소에도 접근할 수 있는데, 이는 @testable
속성이 선언되어 있고, 그 모듈이 컴파일 되었을 때 테스트 가능하게 된다.
Access Control Syntax
open
, public
, internal
, fileprivate
, private
중 하나를 요소의 선언부 앞쪽에 선언함으로써 접근 수준을 정의할 수 있다.
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`로 취급된다.
class SomeInternalClass {} // implicitly internal
let someInternalConstant = 0 // implicitly internal
Custom Types
커스텀 타입에 대해 접근 수준을 명확하게 해줌으로써 접근 제어를 할 수 있다.
타입의 접근 수준은 타입의 멤버들(프로퍼티, 메서드, 이니셜라이저, 서브스크립트)에게도 영향을 준다. 타입이 private
라면 멤버들 또한 private
가 되는 것이다.
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
튜플 타입의 접근 수준은 사용된 타입들 중 가장 제한적인 요소가 된다. 예를 들어서, 2개의 서로 다른 타입으로 튜플을 구성한다고 할 때, A의 접근 수준이 internal
이고, B의 접근 수준이 private
라고 할 때, 해당 튜플은 더 낮은 수준인 private
가 되는 것이다.
Function Types
함수 타입의 접근 수준은 함수의 파라미터와 반환값 중 더 제한적인 것을 따르게 된다. 하지만 함수의 접근 수준이 상황별 기본값과 일치하지 않는 경우 함수 정의 시에, 명시적으로 접근 수준을 지정해줘야 한다.
아래의 예는 명시적인 접근 수준 지정이 되어있지 않은 전역 함수인 someFunction( )
이다. 아무 접근 수준을 지정해주지 않았으니 internal
로 지정되겠지? 싶을 수 있다. 하지만 이 경우에는 그렇게 작동되지 않고, 오히려 컴파일 에러를 불러일으킨다.
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
이 함수의 리턴 타입은 튜플로 internal
과private
로 이루어져 있다. 앞서 본 것 처럼, 더 낮은 수준의 접근 제어자를 택하기에 리턴값은 private
하게 된다.
리턴값이 private
하기 때문에, 함수의 접근 수준 또한 초기 선언시에 private
로 명시해줘야 한다.
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
리턴값이 private
이기 때문에 함수는 public
, open
그리고 기본값인 internal
조차 될 수 없는 것이다.
Enumeration Types
열거형의 개별 케이스(case)들은 열거형 자체의 접근 수준을 이어받으며, 개별적으로 그 접근 수준을 정의할 수 없다.
아래의 예시 처럼, CompassPoint
열거형은 명백히 public
이라고 되어 있기 때문에 4가지 케이스 모두 public
의 접근 수준을 갖게 된다.
public enum CompassPoint {
case north
case south
case east
case west
}
Raw Values and Associated Values
열거형 내의 raw values나 associated values의 경우, 열거형과 같거나 더 높은 접근 수준을 가져야한다. 즉 열거형이 internal
일 때, raw value가 private
일 수 없다는 것이다.
Nested Types
nested type의 접근 수준은 둘러싸고 있는 타입(containing type)의 접근 수준과 같다. (둘러싸고 있는 타입이 public
만 아니라면) 둘러싸고 있는 타입이 public
이라면 nested type은 자동적으로 pulblic
이 아닌 internal
이 된다. 그럼에도 만약 nested type의 접근 수준을 public
으로 해주고 싶다면 그렇게 내부에서 명시해주면 된다.
Subclassing
현재 접근 맥락상 접근이 가능하고 하위 클래스와 같은 모듈 내에 정의된 모든 클래스에 대해서 하위 클래스 사용이 가능하다. 클래스의 접근 수준이 open
이라면 다른 모듈에서도 하위 클래스를 만들 수 있는 것이다. 하지만 하위 클래스는 상위 클래스보다 높은 접근 수준을 가지면 안된다. 즉 상위 클래스가 의 접근 수준이 internal
인데, 하위 클래스의 접근 수준이 public
일 수는 없다는 것이다.
추가적으로, 같은 모듈에 정의된 클래스에 대해 현재 접근 맥락상 보이는 클래스 멤버(메서드, 프로퍼티, 이니셜라이저, 서브스크립트)를 override할 수 있다. 클래스가 다른 모듈에 정의되어 있는 경우는, open
클래스 멤버를 override할 수 있다.
override는 상속된 클래스 멤버를 상위 클래스 버전보다 더 높은 접근 수준을 갖도록 해줄 수 있다. 즉 아래의 예시 처럼, A 클래스의 메서드는 fileprivate
인데, 이를 상속한 B 클래스의 메서드는 internal
로 만들 수 있다는 것이다.
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
또한 하위 클래스의 멤버는 상위 클래스의 멤버(하위 클래스 멤버보다 더 낮은 접근 수준을 갖는)를 호출하는 것이 가능하다.
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
super.someMethod()
}
}
왜냐하면 상위 클래스 A와 하위 클래스 B가 같은 소스 파일 내에 정의되어 있기 때문에, B의 SomeMethod( )
구현부에 super.someMethod()
를 부를 수 있다.
Constants, Variables, Properties, and Subscripts
상수, 변수, 프로퍼티는 해당 타입보다 더 공개적일 수 없다. 즉 더 높은 접근 수준을 가질 수 없다는 것이다. private
타입은 public
프로퍼티를 가질 수 없고, 서브스크립트는 인덱스 타입이나 반환값 보다 더 공개적일 수 없다는 것이 이와 유사하다.
만약 상수, 변수, 프로퍼티를 private
타입으로 사용하고 싶다면 그렇게 명시해주면 된다.
private var privateInstance = SomePrivateClass()
Getters and Setters
상수, 변수, 프로퍼티, 서브스크립트에 대한 getter와 setter의 접근 수준은 상수, 변수, 프로퍼티, 서브스크립트가 가지는 접근 수준과 자동적으로 동일하게 된다.
읽기 전용 변수, 프로퍼티, 서브스크립트로 제한하기 위해, getter에 비해 setter에게 더 낮은 접근 수준을 부여할 수 있다. 더 낮은 접근 수준을 부여하기 위해서는 fileprivate(set)
, private(set)
, internal(set)
을 var
앞에 또는 서브스크립트 구현부에 적어주면된다.
아래는 TrackedString
이라는 구조체를 정의하여, 문자열 프로퍼티가 몇 번 수정되었는지 추적할 수 있게 한 예시이다.
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
TrackedString
구조체는 " "를 초기값으로 가지는 value
라는 문자열 저장 프로퍼티를 선언해주고 있다. 또한 변경 횟수를 저장하기 위한 numverOfEdits
프로퍼티도 갖고 있으며, value
가 수정될 때 마다 이 값은 1씩 상승한다.
value
는 아무런 접근 수준을 지정해주지 않았기에 internal
이 되고, numberOfEdits
프로퍼티의 getter 또한 아무런 접근 수준을 지정해주지 않았기에 internal
이 된다. 이 덕분에 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"
이제 외부 소스 파일에서 프로퍼티를 수정할 수 없기 때문에 기능적으로 더 안전하고 편리하게 되는 것이다.
위에 처럼 setter말고도 getter도 함께 접근 수준을 지정해줘야 하는 경우도 있다. 아래와 같이 지정해줌으로써 사용할 수 있다.
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
Initializers
커스텀한 이니셜라이저는 초기화해야 하는 타입과 같거나 더 낮은 접근 수준으로 지정될 수 있다. 예외적으로 required init
은 속해 있는 클래스와 동일한 접근 수준을 가져야 한다.
함수나 메서드의 파라미터와 같이, 타입의 이니셜라이저가 가지는 파라미터는 이니셜라이저의 접근 수준보다 더 낮을 수 없다.
Default Initializers
기본적인 이니셜라이저의 접근 수준은 초기화해야하는 그 타입의 접근 수준과 같다. (타입이 public
만 아니라면) 타입이 public
이라면 기본 이니셜라이저는 자동적으로 pulblic
이 아닌 internal
이 된다. 그럼에도 만약 기본 이니셜라이저를 인자가 없는 public
으로 해주고 싶다면 그렇게 내부에서 명시해주면 된다.
Default Memberwise Initializers for Structure Types
구조체의 어떤 저장 프로퍼티가 private
라면, 기본 멤버와이즈 이니셜라이저(프로퍼티를 매개변수로 가지는 이니셜라이저) 는 private
접근 수준을 갖게 된다.
즉, 구조체 내 저장 프로퍼티의 접근 수준을 따라가는 것이다. 마찬가지로 원하는 접근 수준을 지정하기 위해 타입의 정의 부분에서 원하는 접근 수준을 적어주면 된다.
Protocols
만약 프로토콜 타입에 접근 수준을 할당하고 싶다면, 프로토콜을 정의하는 부분에 적어주면된다.
프로토콜 내의 규약(정의)들은 자동적으로 프로토콜의 접근 수준을 따른다. 프로토콜의 접근 수준과 다른 접근 수준을 가진 규약은 있을 수 없다는 것이다. 이 덕분에 프로토콜의 규약(요구사항)들이 프로토콜을 채택했을 때 나타나는 것을 보장해준다.
Protocol Inheritance
기존 프로토콜을 상속하여 새로운 프로토콜을 정의한다고 했을 때, 새로운 프로토콜은 상속받아오는 프로토콜의 접근 수준과 같아진다. 즉, internal
프로토콜을 상속하여 public
프로토콜을 새로 만들 수 없다는 것이다.
Protocol Conformance
타입은 자기보다 낮은 접근 수준의 프로토콜을 채택(준수)할 수 있다. 쉽게 이야기하면public
타입이 internal
프로토콜을 채택하여 준수할 수 있다는 것이다. 이 때 구현해야하는 프로토콜의 규약들은 최소한 internal
의 접근 수준을 갖게 된다.
Extensions
클래스, 구조체, 열거형 접근이 가능한 맥락에서는 확장이 가능하다. 어떤 타입의 멤버가 확장을 통해 새로 추가되었다고 할 때, 기존 타입 멤버의 접근 수준을 따르게 된다. 즉 fileprivate
타입을 확장하여 타입 멤버를 추가한다면 이 또한 기본적으로 fileprivate
접근 수준을 갖게 되는 것이다.
대신, 확장 시에 모든 멤버들에게 새로운 접근 수준을 정의해줌으로써 명확한 접근 수준을 정의할 수 있다.
하지만 프로토콜을 채택하기 위해 확장하는 경우에는 접근 수준을 제공할 수 없다. 대신에, 프로토콜의 접근 수준이 각 프로토콜의 요구사항들에 대해 기본 접근 수준을 정해준다.
Private Members in Extensions
동일한 파일 내에서 확장하는 경우, 확장자(extention)의 코드가 원래 클래스, 구조체, 열거형의 선언의 일부로 작성된 것 처럼 작동한다. 즉, extend의 개념을 설명하는 부분이랑 유사한데, extend를 통해 기존 구현부를 기본적으로 가져오고 거기에 추가적인 부분을 구현할 수 있다는 말과 같다.
1. 기존 선언시 private 멤버를 선언하여도 같은 파일 내의 확장시에는 해당 멤버에 접근 가능하다.
2. 하나의 확장에서 private 멤버를 선언하면, 같은 파일의 다른 확장에서 해당 멤버에 접근 가능하다.
3. 확장자에서 private 멤버를 선언하면 같은 파일 내의 기존 선언에서 접근 가능하다.
이 동작들은 타입이 private한 요소가 있건없건, 동일한 방식으로 코드를 구성할 때 확장을 사용할 수 있다는 것을 의미한다.
말로만 보니까 엄청 헷갈린다...
protocol SomeProtocol {
func doSomething()
}
아래와 같이 프로토콜을 채택할 수 있다는 것이다.
struct SomeStruct {
private var privateVariable = 12
}
extension SomeStruct: SomeProtocol {
func doSomething() {
print(privateVariable)
}
}
구조체에 구현된 privateVariable
을 확장시에도 접근할 수 있는 것을 볼 수 있다.
Generics
generic 타입/함수의 접근 수준은 스스로의 접근 수준과 타입 파라미터에 대한 타입 제약 중 최소한의 수준을 갖는 접근 수준을 할당받는다. 즉 min(generic 타입/함수, 타입 제약)
인 느낌
Type Aliases
정의한 모든 type aliases는 접근 제어를 위해 별개의 타입으로 다뤄진다. type alias는 타입보다 같거나 낮은 접근 수준을 갖을 수 있다. 예를 들어, private
type alias는 private
, fileprivate
,internal
, public
, open
타입을 별칭으로 사용할 수 있지만, public
인 타입 별명은 internal
, fileprivate
, private
타입에 별명을 붙일 수 없다!!
정리하자면 다음과 같다.
키워드 | 접근도 | 범위 | 기타 |
open | 가장 높음 | 모듈 외부 가능 | 클래스에서만 사용 가능 |
public | 높음 | 모듈 외부 가능 | |
internal | 중간 | 모듈 내부 가능 | 기본값 |
fileprivate | 낮음 | 모듈 내부 가능 | |
private | 가장 낮음 | 정의된 곳 안에서만 가능 |
Ref: https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html