👨🏻‍💻iOS 공부/iOS & Swift

[Swift] Access Control

728x90
반응형

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

openpublic 키워드를 통해 지정된 요소들은 어디에서나 사용될 수 있다. 모듈이 정의된 소스 파일 내에서나, 다른 모듈을 가져다 쓰는 소스파일 등에서 사용 가능하다. 대표적으로 open 또는 public 키워드는 주로 프레임워크에서 외부와 연결될 인터페이스를 구현할 때 많이 사용된다. openpublic 의 차이는 아래서 다뤄보자.

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을 작성할 때, 앱 내의 코드는 앱 모듈 밖에서 사용할 수 있도록 작성하지 않아도 된다. 그 덕분에 기본 접근 수준으로도 작성 가능하다. 하지만 그 내부에서 앱 모듈 내의 다른 코드로 부터 상세 구현을 숨기기 위해 fileprivateprivate 키워드를 사용할 수는 있다.

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
}

이 함수의 리턴 타입은 튜플로 internalprivate로 이루어져 있다. 앞서 본 것 처럼, 더 낮은 수준의 접근 제어자를 택하기에 리턴값은 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

 

Access Control — The Swift Programming Language (Swift 5.5)

Access Control Access control restricts access to parts of your code from code in other source files and modules. This feature enables you to hide the implementation details of your code, and to specify a preferred interface through which that code can be

docs.swift.org

 

728x90
반응형