객체지향을 접하다 보면 중요하게 나오는 키워드가 ‘정보 은닉’‘캡슐화(Encapsulation)’ 입니다. 이는 특정 개념을 나타내는 데이터와 메소드를 하나로 묶어서 손쉽게 사용하게 만들고, 그 상세 구현 내용은 감춤으로써 재사용성을 높이고 잘못된 사용을 방지하는 효과를 가집니다. 이를 위해 사용하는 개념 중 하나가 바로 접근 제어(Access Control)입니다. 접근 제어는 코드의 특정 부분을 다른 코드가 어느 정도까지 접근할 수 있는지 여부를 결정합니다.

이 글은 다음 글을 참조하여 작성되었습니다. Swift Language Guidelines - Access Control

Swift에서는 클래스, 구조체, 열거형 등의 개별 타입이나 타입에 속한 프로퍼티, 메소드, 생성자, 첨자 등에 접근 제어를 할 수 있습니다.또한 프로토콜, 전역 상수, 변수, 함수 등도 상황에 맞는 접근 제어를 정해줄 수 있습니다. 또한 일일이 접근 제어 권한을 작성하는 번거로움을 위해 기본 권한을 자동으로 제공해줍니다. 그래서 단일 타겟 앱을 만들 때에는 사실상 접근 권한을 설정해 줄 필요는 전혀 없습니다.

이하 접근 제어를 적용할 수 있는 대상을 통틀어 엔티티(Entity)라 명칭합니다.

C++나 Java등에서는 public, protected, private의 3단계 접근 제어를 제공하는데에 반해, swift는 총 5단계의 접근 제어를 제공합니다.

  1. open - 가장 낮은 수준의 제한입니다. 모듈 내에서 뿐만 아니라, 해당 모듈을 import 한 곳에서도 자유롭게 사용할 수 있습니다. 또한 서브클래싱이나 오버라이드에도 제한이 없습니다. 단, 서브클래싱이나 오버라이드와 관계없는 함수타입, 전역 상수, 구조체 등에는 open을 사용할 수 없고 public을 써야 합니다.
  2. public - 사용만 할 때는 open과 비슷합니다. 하지만 서브클래싱이나 오버라이드는 모듈 내부에서만 할 수 있습니다. 또, 서브클래싱이나 오버라이드가 없는 모든 엔티티는 open대신 public을 써야합니다.
  3. internal - 모듈 내부에서는 자유롭게 사용할 수 있지만, 모듈 바깥에서는 아무리 모듈을 import해도 사용할 수 없습니다.
  4. fileprivate - 해당 엔티티가 선언된 파일 내에서만 자유롭게 사용할 수 있습니다.
  5. private - 엔티티가 정의된 블록, 같은 파일 내에서의 extension 안에서만 자유롭게 사용할 수 있습니다.

접근 제어는 다음과 같은 대원칙을 따릅니다.

어떤 엔티티도 더 낮은 접근 권한(더 제한적인)을 가진 엔티티의 관점에서 정의되면 안된다.

예시)

  1. public 변수는 public 미만의 접근 권한(internal, fileprivate, private)의 타입으로 정의되면 안됩니다. 이는 public변수가 사용되는 곳에서 해당 타입에 접근하지 못하기 때문입니다.
  2. 함수는 매개변수 타입과 반환 타입보다 높은 권한 수준을 가져서는 안됩니다.
  • 접근 제어 사례
    1. 기본 접근 권한 : 코드 안의 모든 엔티티는 일부 예외를 제외하고는 명시적으로 선언하지 않으면 internal의 기본 권한을 가집니다. 대부분의 경우는 접근 권한을 설정할 필요는 없습니다.

    2. 단일 타겟 앱 : 모든 필요한 코드가 앱 안에 들어가 있고 외부 모듈에 노출할 필요가 없기 때문에, 기본 접근 권한인 interanl로 충분합니다. 그래도 상세 구현을 앱 내에서의 다른 모듈에 숨기고 싶다면 fileprivate나 private를 사용할 수 있습니다.

    3. 프레임워크 : 프레임워크의 인터페이스는 open이나 public으로 선언해야 외부 모듈에서 import해서 사용할 수 있습니다. 내부 상세 구현은 internal, fileprivate, private 등으로 외부에 보이지 않게 숨길 수도 있습니다. open과 public으로 선언된 엔티티들은 해당 프레임워크의 API가 됩니다.

    4. 유닛 테스트 : 유닛 테스트를 위해서는 모듈 내의 코드를 사용 가능하게 해야 하는데, 원칙적으로는 open과 public만 사용가능합니다. 하지만 해당 모듈을 testing을 활성화 시키고 컴파일 한 뒤, import할 때 @testable 어노테이션을 주면 internal 엔티티까지 외부 모듈이 접근가능하게 할 수 있습니다.

  • 접근 제어 규칙
    1. 커스텀 타입 : 접근 권한에 따라 사용할 수 있는 범위가 달라집니다. 또한 타입 멤버의 기본 접근 권한은 타입의 접근 권한에 영향을 받습니다. 예를 들어, 타입이 fileprivate이면 타입의 기본 접근 권한은 fileprivate입니다.
      • 단, public이상의 접근 권한에 대해서는 멤버 기본 접근 권한은 internal입니다. 이는 외부에 내부 구현이 의도치 않게 드러나는 것을 막기 위함입니다.
    2. 튜플 타입 : 모든 튜플의 멤버들 중, 가장 낮은 권한을 가진 멤버에게 맞춰집니다. 다만 튜플은 독립적인 선언을 가지지 않기 때문에 권한은 자동으로 추론되고, 사용자가 직접 명시할 수는 없습니다.

    3. 함수 타입 : 매개변수 타입들과 반환 타입 중 가장 권한 수준이 낮은 것을 따라갑니다. 이렇게 자동으로 정해진 권한 수준이 현재 문맥 상으로 정해지는 권한 수준과 다를 경우 함수의 권한 수준을 명시적으로 적어줘야 합니다. 이 때 권한을 계산된 것보다 낮게 줄 수는 있지만 높일 수는 없습니다.
      //func someFunction() -> (SomeInternalClass, SomePrivateClass) // 접근 권한 수준이 private여야하는데 명시적으로 적지 않아서 컴파일에 실패한다.
    
      private func someFunction() -> (SomeInternalClass, SomePrivateClass) // OK
    
    1. 열거형 : 열거형의 각 케이스는 열거형 타입의 접근 권한과 동일하게 가고, 개별적으로 권한을 설정해주는 것은 지원하지 않습니다. 또한 rawValue로 쓰이는 타입은 열거형 타입의 접근 권한보다 높거나 같아야 한다.

    2. 중첩 타입 : 부모 타입의 접근 제어를 따라갑니다. 다만, public이상의 경우에는 명시적으로 적어주지 않으면 internal이 됩니다. 따라서 public으로 만들고 싶으면 명시적으로 적어주어야 합니다.

    3. 서브클래싱
      1. 부모클래스보다 더 높은 접근 권한을 가질수는 없습니다.

      2. 현재 상황에서 접근할 수 있는 모든 엔티티는 오버라이드가 가능합니다.

      3. 오버라이드가 되는 엔티티는 이전보다 더 높은 권한 수준을 가질 수 있습니다.

      4. 서브클래스에서 현재 상황에서 접근할 수 있는 부모클래스의 엔티티를 접근할 수 있습니다.

    4. 상수, 변수, 프로퍼티, 첨자 : 자신의 타입보다 접근 권한이 더 높을 수는 없습니다.

      1. 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() {}
        }
      
      1. 생성자 : 생성하는 타입의 접근 권한보다 같거나 낮아야 합니다. 유일한 예외는 ‘required 생성자’로 이는 반드시 생성하는 타입과 같은 접근 권한을 가져야 합니다. 또한 함수와 메소드와 마찬가지로, 생성자의 접근 권한보다 더 낮은 접근 권한을 가진 인자는 사용할 수 없습니다.

        • 기본 생성자 : 타입의 접근 권한과 같은 권한을 가진다. 다만 위에서와 마찬가지로 public 이상의 타입에 대해서는 기본 권한이 internal로 고정되므로, public이상을 원한다면 반드시 명시적으로 쓸 필요가 있다.

        • 멤버 초기화 연산자 : 가장 접근 권한이 낮은 멤버 변수의 권한을 따라갑니다.

    5. 프로토콜 : 프로토콜 선언할 때 접근 권한을 정해줍니다. 프로토콜의 경우 내부 요구사항들은 프로토콜의 접근 권한을 따라가고, 개별적으로 설정해 줄 수 없습니다. 이는 프로토콜을 적용하는 모든 타입이 요구사항을 맞출 수 있도록 보장하기 위함입니다.

      • 프로토콜 적용 : 타입은 자신보다 낮은 접근 권한을 가진 프로토콜을 적용할 수 있습니다. 다만 이 경우는 해당 타입의 접근 권한은 타입과 프로토콜 중 낮은 권한을 가진 것에 맞춰집니다. 또, 프로토콜을 구현할 때는 프로토콜을 적용한 타입의 접근 권한 이상의 권한을 가지도록 해 줘야 합니다.

    Swift와 Obj-C에서는 프로토콜 적용이 전역적으로 되기 때문에, 하나의 프로그램에서 한 타입에 한 프로토콜을 2가지 이상의 방법으로 적용할 수는 없습니다.

    1. 익스텐션 : 기본적으로는 확장되는 원래의 타입의 접근 권한을 따라갑니다. 하지만 익스텐션 자체에도 접근 권한을 줘서, 해당 익스텐션에서 정의되는 멤버들에 일괄적으로 접근 권한을 설정할 수도 있고, 익스텐션 내에서 멤버별로 접근 권한을 오버라이드 할 수도 있습니다. 다만 프로토콜을 적용하기 위한 익스텐션에서는 명시적으로 접근 권한을 줄 수 없고, 프로토콜의 접근 권한을 따라갑니다.

      • 동일한 파일에서는 익스텐션을 전체 정의의 일부처럼 사용할 수 있습니다. 즉, private 멤버에 접근할 수 있다는 것입니다. 이는 코드를 구조화하는데에 유용한 특성입니다.
    2. 제네릭 : 제네릭 자체에 적용된 접근 권한과 매개 타입의 접근 권한 중 가장 낮은 쪽을 따라갑니다.

    3. 타입앨리어스 : 앨리어스를 하려는 타입보다 같거나 낮은 접근 권한을 가질 수 있습니다. 이는 연관 타입(associated type)의 경우에도 동일하게 적용됩니다.


쉽게 생각할 수 있지만, 알고보면 복잡한 접근제어에 대해서 알아보았습니다. 적절한 접근 제어를 통해 프로그램의 의도를 더욱 명확하게 할 수 있으니, 적재적소에 사용해 보아야겠습니다:)