Swift 정리 - 12. 접근제어
Swift에서 은닉화를 가능하게 하는 접근 제어에 대해 알아보자.
객체지향 프로그래밍 패러다임에서 은닉화는 중요한 개념 중 하나다. 이를 access control을 통해 구현할 수 있다.
Access control는 다른 소스 파일과 모듈에서 내 코드에 접근하는 것을 제어한다. 이를 통해 코드의 구현 부분을 감출 수 있고, 접근되어야 하는 부분은 노출시킬 수 있다.
Modules and Source Files
Swift는 접근 제어 모델은 모듈과 소스 파일의 개념에 기반한다.
Module
모듈은 배포할 코드의 한 유닛이다. 이 배호팔 코드라는 건 하나의 유닛으로 빌드되고 전달되거나 Swift의 import
키워드로 다른 모듈에서 import할 수 있는 프레임 워크나 앱을 의미한다. 쉽게 생각해서 프레임워크, 라이브러리, 애플리케이션이 모듈 단위가 될 수 있고 import
키워드를 상요해서 불러올 수 있다.
Xcode의 각 빌드 타겟(app bundle이나 프레임워크)은 Swift에서 별개의 모듈로 다뤄진다. 만약 캡슐화나 코드를 여러 앱에서 재사용하기 위해 앱의 코드를 stand-alone 프레임워크로 묶었다면 해당 프레임워크에서 정의된 모든 것은 앱 내에서 import 되고 사용되거나, 다른 프레임워크에서 사용될 때 별개의 모듈로서 작동한다.
Source file
소스 파일은 모듈 내에서 하나의 Swift 소스 코드 파일을 의미한다. 개별 타입들을 별개의 소스 파일로 정의하는 것이 일반적이지만, 여러 타입, 함수 등의 정의를 포함할 수 있다.
Access Levels
Swift는 다섯 개의 다른 접근 수준을 제공한다. 이 접근 레벨은 모듈과 소스파일에 상대적이다.
- Open, public : 정의된 모듈 내의 어떤 소스 코드 파일이나 정의된 모듈을 import하는 다른 모듈의 소스 파일에서 접근할 수 있다. 프레임워크의 public 인터페이스를 명시할 때 사용한다.
- Internal : 모듈이 정의된 곳에서의 소스파일에서 사용할 수 있지만, 모듈 밖에서는 접근할 수 없다. 앱이나 프레임워크의 내부 구조를 정의할 때 사용한다. 기본적으로 모든 요소에 암묵적으로 지정되는 기본 접근수준이다.
- File-private : 정의된 소스 파일 내에서만 사용할 수 있다. 특정 부분이나 구현된 디테일을 감추고자 할 때 사용한다.
- Private : 정의된 부분, 그리고 같은 파일 내에 있는 해당 정의의 extension에서만 사용할 수 있다. 하나의 정의 내에서만 무언가를 사용하고 싶을 때 사용한다.
Open이 가장 접근 레벨이 높고, private이 가장 낮은 접근 레벨을 갖고 있다.
Open은 클래스와 클래스 멤버에서만 쓸 수 있고, 하위 클래스에서 코드를 접근할 수 있게 하고 override할 수 있게 한다.
Guiding Principle of Access Levels
상위 요소보다 하위 요소가 더 높은 접근 수준을 가질 수 없다.
Default Access Levels
명시적으로 접근 레벨을 쓰지 않았다면 internal을 기본 접근 수준으로 갖게 된다.
Access Levels for Single-Target Apps
만약 간단한 single-target 앱을 만든다면 앱의 코드는 일반적으로 앱 내에 존재하는 코드이고 앱 모듈 밖에서 접근될 필요가 없을 것이다. 이런 경우 기본적으로 사용되는 기본 접근 수준인 internal이 이런 요구사항에 맞는다. 따라서 별도로 접근 수준을 명시하지 않아도 된다. 하지만 구현 디테일을 앱 모듈의 다른 코드에서 접근할 수 없게 하기 위해 private이나 file private으로 코드를 표시할 수도 있다.
Access Levels for Frameworks
프레임워크를 개발할때, 외부에 노출될 인터페이스 부분을 open이나 public으로 정의해서 해당 프레임워크를 import하는 앱과 같은 다른 모듈에서 접근할 수 있게 한다. 이런 public한 인터페이스는 프레임워크의 application programming interface(API)다.
Access Levels for Unit Test Targets
Unit test 타겟이 있는 앱을 만들 때, 앱에 있는 코드는 테스트 될 수 있는 접근 수준을 가지고 있어야 한다. 기본적으로, open이나 public으로 명시된 엔티티들만 다른 모듈에서 접근할 수 있다. 하지만 unit test 타겟은 #testable
로 테스트할 모듈을 import해서 컴파일하면 internal 엔티티에도 접근할 수 있다.
Custom Types
커스텀 타입에 명시적인 접근 수준을 주고 싶다면 해당 타입을 정의할 때 하면 된다. 특정 타입의 접근 수준은 해당 타입의 멤버(프로퍼티, 메서드, 이니셜라이저, 서브스크립트)의 기본 접근 수준도 해당 접근 수준이 된다.
public 타입은 기본적으로 internal 멤버를 가진다. 만약 타입의 멤버가 public한 접근 수준을 갖게 하고 싶다면 이를 명시적으로 작성해줘야 한다. 이는 특정 public한 API를 public하게 만들지만, 내부 구현은 실수로라도 노출시키지 않게 하기 위해서다.
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
튜플 타입의 접근 레벨은 해당 튜플에서 사용된 모든 타입의 접근 수준 중 가장 제한적인 접근 수준(가장 낮은 접근 수준)을 따른다.
Function Types
함수의 접근 레벨은 함수의 파라미터 타입과 리턴 타입의 접근 수준 중 가장 제한적인 접근 수준으로 계산된다.
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
Enumeration Types
열거형의 개별 케이스는 속한 열거형의 접근 수준과 같다. 개별 열거형 케이스에 다른 접근 수준을 부여할 수 없다.
public enum CompassPoint {
case north
case south
case east
case west
}
열거형 정의에서 사용된 raw value나 associated value에 열거형의 접근 수준보다 더 낮은 접근 수준을 사용할 수 없다.
Nested Types
중첩된 타입에서 외부 타입이 public이 아닌한 내부 타입은 외부 타입과 같은 접근 레벨을 갖는다. Public의 내부 타입은 기본적으로 internal이기 때문에 내부 타입의 접근 수준을 다르게 하고 싶다면 명시적으로 작성해야 한다.
Subclassing
자식 클래스는 부모 클래스보다 더 높은 접근 수준을 가질 수 없다. 오버라이딩의 경우 자식 클래스에서 부모 클래스의 접근 수준보다 더 높은 접근 수준을 갖게 할 수 있다.
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()
}
}
Constants, Variables, Properties, and Subscripts
상수, 변수, 프로퍼티는 해당 프로퍼티보다 더 높은 접근 수준을 가질 수 없다.
private var privateInstance = SomePrivateClass()
Getters and Setters
상수, 변수, 프로퍼티, 서브스크립트의 getter와 setter는 해당 상수, 변수, 프로퍼티, 서브스크립트의 접근 수준과 같은 접근 수준을 가진다. 하지만 setter를 getter보다 더 낮은 접근 수준을 갖게 해서 읽기만 가능한 접근 수준을 만들게 할 수 있다.
fileprivate(set)
, private(set)
, internal(set)
와 같이 쓰면 된다.
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
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"
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
Initializers
커스텀 이니셜라이저는 해당 이니셜라이저를 가진 타입과 같거나 더 낮은 레벨의 접근 수준을 가질 수 있다. Required 이니셜라이저는 해당 클래스의 접근 레벨과 무조건 같은 수준을 갖는다.
Default Initializer
Swift는 구조체나 클래스가 모든 프로퍼티에 기본값을 제공하고 따로 이니셜라이저를 정의하고 있지 않을 때 기본 이니셜라이저를 제공한다고 했다.
기본 이니셜라이저는 해당 타입과 같은 접근 수준을 갖고 있는데, 역시 여기서도 public
은 예외다. public 타입의 기본 이니셜라이저는 internal 접근 수준을 갖는다. 만약 인자가 없는 이니셜라이저를 다른 모듈에서도 사용할 수 있게 하고 싶다면 해당 타입에 명시적으로 public 타입의 인자가 없는 이니셜라이저를 제공해야 한다.
Default Memberwise Initializers for Structure Types
만약 구조체의 저장 프로퍼티 중 하나라도 private하면 기본 멤버와이즈 이니셜라이저는 private하다.