Swift 접근 제어: open·public·package·internal·fileprivate·private

반응형

Swift 접근 제어는 declaration을 어디서 사용할 수 있는지 module, package, source file, enclosing declaration 경계로 정한다. 현재 Swift에는 open, public, package, internal, fileprivate, private 여섯 access level이 있다. SwiftUI 전용 기능이 아니며 class, struct, enum, protocol, property, method, initializer 등에 적용되는 language rule이다.

여섯 access level을 경계로 비교한다

Swift access control 문서의 기준을 좁은 범위부터 넓은 범위로 정리하면 다음과 같다.

level 사용할 수 있는 범위 주된 용도
private enclosing declaration과 같은 file의 해당 type extension 한 type의 implementation detail
fileprivate 같은 source file file 안 여러 declaration의 협력
internal 같은 module app·framework 내부 API, 기본값
package 같은 package에 속한 module들 multi-module package 내부 API
public importing module에서도 사용 외부에 공개하는 API
open importing module에서 사용하고 subclass·override 가능 확장을 허용하도록 설계한 class API

open은 class와 class member에만 적용된다. public class는 외부 module에서 사용할 수 있지만 외부 module에서 subclass할 수 없고, public member도 외부에서 override할 수 없다. 외부 subclass와 override까지 contract로 허용할 때 open을 선택한다.

package는 Swift 5.9부터 추가된 경계다

기존의 다섯 단계만 기억하면 package를 빠뜨리기 쉽다. package는 같은 package로 build된 서로 다른 module이 symbol을 공유하되 package 밖에는 공개하지 않게 한다.

// Engine module
public struct Engine {
    public init() {}

    package func warmUpCache() {
        // 같은 package의 다른 module에서 사용
    }
}

Swift Evolution SE-0386은 이 modifier가 Swift 5.9에 implemented되었고 public보다 좁고 internal보다 넓은 범위를 갖는다고 정의한다. package boundary는 source folder 이름이 아니라 SwiftPM이나 Xcode 같은 build system이 compiler에 전달하는 package identity로 정해진다.

internal은 기본이고 public member는 자동이 아니다

access modifier를 적지 않은 대부분의 declaration은 internal이다.

public struct Article {
    public let title: String

    public init(title: String) {
        self.title = title
    }
}

Articlepublic으로 선언해도 member가 자동으로 모두 public이 되지는 않는다. 외부 module에서 property를 읽고 initializer를 호출하게 하려면 각각 명시해야 한다. 공개 surface를 의도적으로 선택하게 하는 rule이다.

또한 public function의 parameter나 return type에 private·internal type을 넣을 수 없다. API를 호출하는 위치에서 signature의 type을 볼 수 없기 때문이다. access level은 declaration 하나가 아니라 signature 전체에서 가장 제한적인 type과 맞아야 한다.

private와 fileprivate의 차이

fileprivate는 같은 source file 어디에서나 접근할 수 있다. private는 enclosing declaration으로 제한되지만, 같은 file에서 그 type을 확장한 extension은 예외적으로 private member에 접근할 수 있다.

struct Session {
    private var token: String?
}

extension Session {
    // 같은 source file이면 접근 가능
    var isSignedIn: Bool {
        token != nil
    }
}

“private는 class 밖에서 절대 접근 불가”라고만 외우면 이 same-file extension rule을 놓친다. 반대로 extension을 다른 file로 옮기면 접근할 수 없다. code를 file 단위로 나눌 때 access level이 갑자기 넓어지는 이유가 되기도 한다.

getter는 공개하고 setter는 좁힐 수 있다

read access와 mutation access가 같을 필요는 없다.

public final class DownloadState {
    public private(set) var completedBytes: Int = 0

    public func record(bytes: Int) {
        completedBytes += bytes
    }
}

외부에서는 completedBytes를 읽을 수 있지만 직접 바꿀 수 없다. state transition을 method 안에 모아 invariant를 지키는 데 유용하다. private(set) 외에도 필요에 따라 fileprivate(set), internal(set), package(set)을 사용할 수 있다.

SwiftUI View에서는 local state부터 좁힌다

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            counterLabel
            Button("Increment") {
                count += 1
            }
        }
    }

    private var counterLabel: some View {
        Text("Count: \(count)")
    }
}

view가 소유하는 local state와 helper view는 private에서 시작하는 편이 자연스럽다. 그러나 모든 type에 습관적으로 fileprivate를 붙이는 것이 안전한 설계는 아니다. 실제로 같은 file의 다른 declaration과 공유해야 할 때만 scope를 넓힌다. Apple State 문서도 view state를 private로 선언해 owner boundary를 유지하도록 안내한다.

access control과 extension file 분리의 관계는 Swift extension 경계, view와 model의 public surface는 SwiftUI View 구조에서 이어서 볼 수 있다.

선택 원칙

  1. 가장 좁은 level에서 시작하되 테스트·module contract를 고려한다.
  2. framework API는 외부에 필요한 declaration만 public으로 만든다.
  3. 외부 subclass·override를 의도적으로 지원할 때만 open을 사용한다.
  4. 여러 module로 나눈 package 내부 공유는 package를 검토한다.
  5. read-only public API가 필요하면 setter access를 별도로 낮춘다.
  6. file을 나눌 때 same-file private extension access가 사라지는지 확인한다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글