Swift

New access modifier - package

GREEN.1229 2024. 3. 4. 19:08

안녕하세요. 그린입니다 🍏

이번 포스팅에서는 Swift 5.9에서 새로 나온 접근 제어자인 package에 대해 알아보겠습니다 🙋🏻

 

 


나온 배경

원래 기존에 접근제어자라고 하면 흔히 알고 있는 5가지가 있죠!

open, public, internal, fileprivate, private

만약 여러분들이 모듈화를 시키고 다른 모듈 패키지의 코드를 가져와 사용한다고 가정할때, 대부분 public으로 작업했을겁니다.

저도 물론 그렇구요..!

여기서 public으로 작업하게되면, 패키지 내부와 외부 모두에서 접근할 수 있게 됩니다.

이 경우는 편리하긴 하지만, 바람직하지 않을때도 있습니다 🥲

패키지 외부의 코드를 공유하지 않고 패키지 내의 모듈 간에 코드를 공유하여야 하는 경우도 있으니까 말이죠!

그렇기에, 어떤 심볼의 가시성 범위를 더 효과적으로 제어하기 위해서 새로운 접근 제어자가 필요했고, 이 배경으로 package 접근 제어자가 나타났습니다 🙋🏻

 

자, 그럼 왜 탄생했는지 간략히 알아봤으니 이 접근 제어자는 어떤 역할을 해주고 어떻게 사용되는지 보겠습니다 😃


package 이전

기존에 모듈 두개를 만든다면 이렇게 구성을 하고 사용했어요.

// Engine 모듈
public struct MainEngine {
    public init() { ...  }
    // Intended to be public
    public var stats: String { ...  }
    // A helper function made public only to be accessed by Game
    public func run() { ...  }
}

// Game 모듈
import Engine

public func play() {
    MainEngine().run() // Can access `run` as intended since it's within the same package
}

// App
import Game
import Engine

let engine = MainEngine()
engine.run() // Can access `run` from App even if it's not an intended behavior
Game.play()
print(engine.stats) // Can access `stats` as intended

 

 

이 코드에선 App이 Engine을 임포트하고 있고 API에 접근할 수 있죠.

패키지 외부에서 API가 실제로 사용되지 않더라도 접근하고 사용할 수 있는것이죠.

우리가 원하는건 모듈간에는 액세스가 되지만 App 프로젝트에서는 run이 액세스가 되지 않았으면 하는 경우입니다.

 

즉, Engine과 Game 모듈을 제공하는 하나의 gamePkg 오픈소스라면 사실 Game 모듈의 run만 사용할 수 있는것을 권장합니다.


package 이후

package 접근 제어자를 사용하면 아래와 같이 사용됩니다.

// Engine 모듈
public struct MainEngine {
    public init() { ...  }
    public var stats: String { ...  }
    package func run() { ...  }
}

// Game 모듈
import Engine

public func play() {
    MainEngine().run() // Can access `run` as it is a package symbol in the same package
}

// App
import Game
import Engine

let engine = MainEngine()
engine.run() // Error: cannot find `run` in scope

 

즉, run 메서드의 접근 제어자가 package이기에, 같은 패키지 내부인 Game 모듈에서는 접근이 가능합니다.

다만, 해당 패키지를 가져와 사용하는 App에선 막혀있죠.

 

모듈을 빌드할때 다음과 같이 새 플래그가 -package-name 명령으로 호출되어 전달됩니다.

 

swiftc -module-name Engine -package-name gamePkg ...
swiftc -module-name Game -package-name gamePkg ...
swiftc -module-name App -package-name appPkg ...

 

결국, 모듈 빌드 시 gamePkg에 속한 Engine, Game은 gamePkg라는 동일한 이름을 가지게되고 App은 다른 appPkg 이름으로 기록됩니다.

그렇기에, 이 패키지 이름이 다르기에 package 접근 제어자의 동작인 같은 패키지 이름을 비교하기에, appPkg에선 접근이 되지 않죠.

 

SPM을 이용해 패키지를 설정할때 코드를 한번 보겠습니다.

 

import PackageDescription

let package = Package(
    name: "SampleSDK",
    products: [
        .library(
            name: "Essentials",
            targets: ["Essentials"]
        ),
        .library(
            name: "Utilities",
            targets: ["Utilities"]
        ),
    ],
    targets: [
        .target(
            name: "Essentials",
            dependencies: ["Utilities"]
        ),
        .target(
            name: "Utilities"
        ),
    ]
)

 

이렇게 Essentials 모듈이 Utilities 모듈을 의존하고 있게 SampleSDK 패키지를 설정할 수 있습니다.

이렇다면, 위에서 설명했던것처럼 Essentials에서는 Utilities에서 package 접근 제어로 설정된 기능들을 접근할 수 있지만,

SampleSDK 패키지를 사용하는 앱 단에서는 접근할 수 없죠.

 

package func trace()

// Essentials 모듈
import Utilities
trace() // 🙆🏻

// App
import Utilities
trace() // ❌

 

이 패키지 매니페스트에서 기본 동작 자체를 선택적으로 해제할 수도 있습니다.

 

import PackageDescription

let package = Package(
    name: "SampleSDK",
    products: [
        .library(
            name: "Essentials",
            targets: ["Essentials"]
        ),
        .library(
            name: "Utilities",
            targets: ["Utilities"]
        ),
    ],
    targets: [
        .target(
            name: "Essentials",
            dependencies: ["Utilities"],
            packageAccess: false // <== opt-out !!! 🙋🏻🙋🏻🙋🏻
        ),
        .target(
            name: "Utilities"
        ),
    ]
)

 

packageAccess 옵션이 기본적으로는 true인데, false로 변경해준다면 가능하죠!

그럼, 이제 Essentials 모듈에서 Utilities 모듈에 포함된 package 접근 제어자에는 접근하지 못하게 되는것입니다.

 

추가로, Xcode에서 기존 엑코 플젝에서 모듈을 나눴다면 이 또한, 앱 타겟 빌드 설정에서 플래그 추가로 package 기능에 접근할 수도 있습니다.

 

 

이렇게 Utilities 라이브러리 모듈에서 Other Swift Flags에 해당 패키지 이름 옵션을 추가해줍니다.

띄워쓰기가 있어 +를 눌러 한 라인에 입력 시 문자열로 치환되어 정상적으로 반영되지 않기에 +를 눌러 띄워쓰기 기준으로 각자 입력이 필요합니다.

 

 

그리고 해당 Utilities 라이브러리 모듈을 사용할 Green에서 Other Swift Flags로 해당 설정한 플래그를 지정해줍니다.

 

아..! 당연히 Green 타겟에서는 아래처럼 Utilities 라이브러리를 넣어줘야하구요ㅎㅎ

 

 

그럼 Utilities 코드가 package로 되어있다 가정하면

 

package class Utilities {
  package init(value: Int = 3) {
    self.value = value
  }
  
  package var value = 3
}

 

Green 앱에서는 해당 클래스에 접근할 수 있게 됩니다.

 

import SwiftUI
import Utilities

struct ContentView: View {
  let utilities = Utilities()
  
  var body: some View {
    Text(String(utilities.value))
  }
}

 

 

그럼 이렇게 의도한것처럼 잘 나오게 됩니다 😃

 

어렵지 않죠!?ㅎㅎ


마무리

Swift 5.9에서 새로 소개된 package 접근 제어자에 대해 알아봤습니다!

SDK 개발이 아니더라도, 충분히 모듈간 설정을 통해 좀 더 캡슐화하고 응집과 결합되는 부분들에 대해 세부적으로 의도를 가지고 가져가볼 수 도 있을것 같습니다 😀

 


레퍼런스

https://github.com/apple/swift-evolution/blob/main/proposals/0386-package-access-modifier.md

 

A new access modifier in Swift: package

Detailed explanation about Swift's new package access modifier introduced in Swift 5.9

blog.eidinger.info

 

[Swift 5.9+][SE-0386] 새로운 Access Modifier인 Package를 Xcode Project에서 사용하기

Swift 5.9에서 Swift Package에 새로운 접근 제어자 Package가 추가됐습니다. SE-0386 Package 접근 제어자는 특정 도메인이나 역할을 가진 모듈만 접근할 수 있게 해, 유용할 것입니다. 그러나 Swift Package가

minsone.github.io