객체지향을 접하다 보면 중요하게 나오는 키워드가 ‘정보 은닉’과 ‘캡슐화(Encapsulation)’ 입니다. 이는 특정 개념을 나타내는 데이터와 메소드를 하나로 묶어서 손쉽게 사용하게 만들고, 그 상세 구현 내용은 감춤으로써 재사용성을 높이고 잘못된 사용을 방지하는 효과를 가집니다. 이를 위해 사용하는 개념 중 하나가 바로 접근 제어(Access Control)입니다. 접근 제어는 코드의 특정 부분을 다른 코드가 어느 정도까지 접근할 수 있는지 여부를 결정합니다.
이 글은 다음 글을 참조하여 작성되었습니다. Swift Language Guidelines - Access Control
Swift에서는 클래스, 구조체, 열거형 등의 개별 타입이나 타입에 속한 프로퍼티, 메소드, 생성자, 첨자 등에 접근 제어를 할 수 있습니다.또한 프로토콜, 전역 상수, 변수, 함수 등도 상황에 맞는 접근 제어를 정해줄 수 있습니다. 또한 일일이 접근 제어 권한을 작성하는 번거로움을 위해 기본 권한을 자동으로 제공해줍니다. 그래서 단일 타겟 앱을 만들 때에는 사실상 접근 권한을 설정해 줄 필요는 전혀 없습니다.
이하 접근 제어를 적용할 수 있는 대상을 통틀어 엔티티(Entity)라 명칭합니다.
C++나 Java등에서는 public, protected, private의 3단계 접근 제어를 제공하는데에 반해, swift는 총 5단계의 접근 제어를 제공합니다.
- open - 가장 낮은 수준의 제한입니다. 모듈 내에서 뿐만 아니라, 해당 모듈을 import 한 곳에서도 자유롭게 사용할 수 있습니다. 또한 서브클래싱이나 오버라이드에도 제한이 없습니다. 단, 서브클래싱이나 오버라이드와 관계없는 함수타입, 전역 상수, 구조체 등에는 open을 사용할 수 없고 public을 써야 합니다.
- public - 사용만 할 때는 open과 비슷합니다. 하지만 서브클래싱이나 오버라이드는 모듈 내부에서만 할 수 있습니다. 또, 서브클래싱이나 오버라이드가 없는 모든 엔티티는 open대신 public을 써야합니다.
- internal - 모듈 내부에서는 자유롭게 사용할 수 있지만, 모듈 바깥에서는 아무리 모듈을 import해도 사용할 수 없습니다.
- fileprivate - 해당 엔티티가 선언된 파일 내에서만 자유롭게 사용할 수 있습니다.
- private - 엔티티가 정의된 블록, 같은 파일 내에서의 extension 안에서만 자유롭게 사용할 수 있습니다.
접근 제어는 다음과 같은 대원칙을 따릅니다.
어떤 엔티티도 더 낮은 접근 권한(더 제한적인)을 가진 엔티티의 관점에서 정의되면 안된다.
예시)
- public 변수는 public 미만의 접근 권한(internal, fileprivate, private)의 타입으로 정의되면 안됩니다. 이는 public변수가 사용되는 곳에서 해당 타입에 접근하지 못하기 때문입니다.
- 함수는 매개변수 타입과 반환 타입보다 높은 권한 수준을 가져서는 안됩니다.
- 접근 제어 사례
-
기본 접근 권한 : 코드 안의 모든 엔티티는 일부 예외를 제외하고는 명시적으로 선언하지 않으면 internal의 기본 권한을 가집니다. 대부분의 경우는 접근 권한을 설정할 필요는 없습니다.
-
단일 타겟 앱 : 모든 필요한 코드가 앱 안에 들어가 있고 외부 모듈에 노출할 필요가 없기 때문에, 기본 접근 권한인 interanl로 충분합니다. 그래도 상세 구현을 앱 내에서의 다른 모듈에 숨기고 싶다면 fileprivate나 private를 사용할 수 있습니다.
-
프레임워크 : 프레임워크의 인터페이스는 open이나 public으로 선언해야 외부 모듈에서 import해서 사용할 수 있습니다. 내부 상세 구현은 internal, fileprivate, private 등으로 외부에 보이지 않게 숨길 수도 있습니다. open과 public으로 선언된 엔티티들은 해당 프레임워크의 API가 됩니다.
-
유닛 테스트 : 유닛 테스트를 위해서는 모듈 내의 코드를 사용 가능하게 해야 하는데, 원칙적으로는 open과 public만 사용가능합니다. 하지만 해당 모듈을 testing을 활성화 시키고 컴파일 한 뒤, import할 때 @testable 어노테이션을 주면 internal 엔티티까지 외부 모듈이 접근가능하게 할 수 있습니다.
-
- 접근 제어 규칙
- 커스텀 타입 : 접근 권한에 따라 사용할 수 있는 범위가 달라집니다. 또한 타입 멤버의 기본 접근 권한은 타입의 접근 권한에 영향을 받습니다. 예를 들어, 타입이 fileprivate이면 타입의 기본 접근 권한은 fileprivate입니다.
- 단, public이상의 접근 권한에 대해서는 멤버 기본 접근 권한은 internal입니다. 이는 외부에 내부 구현이 의도치 않게 드러나는 것을 막기 위함입니다.
-
튜플 타입 : 모든 튜플의 멤버들 중, 가장 낮은 권한을 가진 멤버에게 맞춰집니다. 다만 튜플은 독립적인 선언을 가지지 않기 때문에 권한은 자동으로 추론되고, 사용자가 직접 명시할 수는 없습니다.
- 함수 타입 : 매개변수 타입들과 반환 타입 중 가장 권한 수준이 낮은 것을 따라갑니다. 이렇게 자동으로 정해진 권한 수준이 현재 문맥 상으로 정해지는 권한 수준과 다를 경우 함수의 권한 수준을 명시적으로 적어줘야 합니다. 이 때 권한을 계산된 것보다 낮게 줄 수는 있지만 높일 수는 없습니다.
//func someFunction() -> (SomeInternalClass, SomePrivateClass) // 접근 권한 수준이 private여야하는데 명시적으로 적지 않아서 컴파일에 실패한다. private func someFunction() -> (SomeInternalClass, SomePrivateClass) // OK
-
열거형 : 열거형의 각 케이스는 열거형 타입의 접근 권한과 동일하게 가고, 개별적으로 권한을 설정해주는 것은 지원하지 않습니다. 또한 rawValue로 쓰이는 타입은 열거형 타입의 접근 권한보다 높거나 같아야 한다.
-
중첩 타입 : 부모 타입의 접근 제어를 따라갑니다. 다만, public이상의 경우에는 명시적으로 적어주지 않으면 internal이 됩니다. 따라서 public으로 만들고 싶으면 명시적으로 적어주어야 합니다.
- 서브클래싱
-
부모클래스보다 더 높은 접근 권한을 가질수는 없습니다.
-
현재 상황에서 접근할 수 있는 모든 엔티티는 오버라이드가 가능합니다.
-
오버라이드가 되는 엔티티는 이전보다 더 높은 권한 수준을 가질 수 있습니다.
-
서브클래스에서 현재 상황에서 접근할 수 있는 부모클래스의 엔티티를 접근할 수 있습니다.
-
-
상수, 변수, 프로퍼티, 첨자 : 자신의 타입보다 접근 권한이 더 높을 수는 없습니다.
- Getter & Setter : 기본적으로 자신이 속한 엔티티의 접근 권한을 따라갑니다. 다만, R/W 범위를 제한하기 위해서 setter에 getter보다 더 낮은 접근 권한을 줄 수 있습니다. 이는 저장 프로퍼티와 계산 프로퍼티 모두에 동일하게 적용될 수 있습니다.
public struct TrackedString { public private(set) var numberOfEdits = 0 //getter는 public, setter는 private public var value: String = "" { didSet { numberOfEdits += 1 } } public init() {} }
-
생성자 : 생성하는 타입의 접근 권한보다 같거나 낮아야 합니다. 유일한 예외는 ‘required 생성자’로 이는 반드시 생성하는 타입과 같은 접근 권한을 가져야 합니다. 또한 함수와 메소드와 마찬가지로, 생성자의 접근 권한보다 더 낮은 접근 권한을 가진 인자는 사용할 수 없습니다.
-
기본 생성자 : 타입의 접근 권한과 같은 권한을 가진다. 다만 위에서와 마찬가지로 public 이상의 타입에 대해서는 기본 권한이 internal로 고정되므로, public이상을 원한다면 반드시 명시적으로 쓸 필요가 있다.
-
멤버 초기화 연산자 : 가장 접근 권한이 낮은 멤버 변수의 권한을 따라갑니다.
-
-
프로토콜 : 프로토콜 선언할 때 접근 권한을 정해줍니다. 프로토콜의 경우 내부 요구사항들은 프로토콜의 접근 권한을 따라가고, 개별적으로 설정해 줄 수 없습니다. 이는 프로토콜을 적용하는 모든 타입이 요구사항을 맞출 수 있도록 보장하기 위함입니다.
- 프로토콜 적용 : 타입은 자신보다 낮은 접근 권한을 가진 프로토콜을 적용할 수 있습니다. 다만 이 경우는 해당 타입의 접근 권한은 타입과 프로토콜 중 낮은 권한을 가진 것에 맞춰집니다. 또, 프로토콜을 구현할 때는 프로토콜을 적용한 타입의 접근 권한 이상의 권한을 가지도록 해 줘야 합니다.
Swift와 Obj-C에서는 프로토콜 적용이 전역적으로 되기 때문에, 하나의 프로그램에서 한 타입에 한 프로토콜을 2가지 이상의 방법으로 적용할 수는 없습니다.
-
익스텐션 : 기본적으로는 확장되는 원래의 타입의 접근 권한을 따라갑니다. 하지만 익스텐션 자체에도 접근 권한을 줘서, 해당 익스텐션에서 정의되는 멤버들에 일괄적으로 접근 권한을 설정할 수도 있고, 익스텐션 내에서 멤버별로 접근 권한을 오버라이드 할 수도 있습니다. 다만 프로토콜을 적용하기 위한 익스텐션에서는 명시적으로 접근 권한을 줄 수 없고, 프로토콜의 접근 권한을 따라갑니다.
- 동일한 파일에서는 익스텐션을 전체 정의의 일부처럼 사용할 수 있습니다. 즉, private 멤버에 접근할 수 있다는 것입니다. 이는 코드를 구조화하는데에 유용한 특성입니다.
-
제네릭 : 제네릭 자체에 적용된 접근 권한과 매개 타입의 접근 권한 중 가장 낮은 쪽을 따라갑니다.
-
타입앨리어스 : 앨리어스를 하려는 타입보다 같거나 낮은 접근 권한을 가질 수 있습니다. 이는 연관 타입(associated type)의 경우에도 동일하게 적용됩니다.
- 커스텀 타입 : 접근 권한에 따라 사용할 수 있는 범위가 달라집니다. 또한 타입 멤버의 기본 접근 권한은 타입의 접근 권한에 영향을 받습니다. 예를 들어, 타입이 fileprivate이면 타입의 기본 접근 권한은 fileprivate입니다.
쉽게 생각할 수 있지만, 알고보면 복잡한 접근제어에 대해서 알아보았습니다. 적절한 접근 제어를 통해 프로그램의 의도를 더욱 명확하게 할 수 있으니, 적재적소에 사용해 보아야겠습니다:)