Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 127 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
# Cradle
<h1 align="center">Cradle</h1>

Cradle은 Swift Macro를 사용해 의존성 graph의 factory를 생성하는 라이브러리입니다.
<p align="center">
Factory를 선언하면 Cradle이 의존성을 연결하고 graph별 수명을 관리해요.
</p>

## 요구 사항
<p align="center">
<a href="Examples/ExampleApp">ExampleApp</a>
·
<a href="Sources/Cradle/Cradle.docc/DependencyGraph.md">사용 안내</a>
·
<a href="docs/Architecture.md">아키텍처</a>
·
<a href="Sources/CradleTesting/CradleTesting.docc/CradleTesting.md">테스트</a>
·
<a href="LICENSE">MIT License</a>
</p>

- Swift tools 6.3
- iOS 17 이상 또는 macOS 10.15 이상
- `CradlePlugin` 사용 시 arm64 또는 x86_64 macOS 빌드 호스트
<br />

## 설치
Cradle은 Swift Macro를 사용해 `@Provide` Factory를 반환 타입과 매개변수 타입으로 연결해요. 어떤 값을 누가 만들고 언제까지 쓸지는 graph마다 정해요.

Swift tools 6.3과 iOS 17 이상 또는 macOS 10.15 이상이 필요해요.

### Swift Package Manager
## 설치

`Package.swift`의 dependencies에 Cradle을 추가합니다. 아래 `1.0.0`은 배포가 완료된 버전 예시이며, 최초 tag가 게시되기 전에는 실제 설치에 사용할 수 없습니다.
Cradle은 Swift Package Manager에서 `1.0.0` 이상 버전으로 설치해요.
Comment thread
opficdev marked this conversation as resolved.

```swift
dependencies: [
.package(url: "https://github.com/opficdev/Cradle.git", from: "1.0.0")
.package(
url: "https://github.com/opficdev/Cradle.git",
from: "1.0.0"
)
]
```

앱 또는 라이브러리 target에는 `Cradle` product를 연결합니다.
Cradle을 쓸 target에는 `Cradle` product를 추가해요.

```swift
.target(
Expand All @@ -31,7 +46,80 @@ dependencies: [
)
```

`CradleTesting`은 테스트 target에서만 선택적으로 연결합니다. graph 선언과 `.mock` 편의 API를 함께 사용하려면 `Cradle`도 같은 target에 추가합니다.
## 첫 graph

이 예제에서는 `UserRepository`와 `LoadUserUseCase`를 graph가 만들고 보관해요. 화면마다 달라지는 `UserID`는 `userProfileViewModel`을 호출할 때 넘겨요.

```swift
import Cradle

struct UserID {
let rawValue: String
}

struct User {
let id: UserID
}

protocol UserRepository {
func load(id: UserID) -> User
}

struct LiveUserRepository: UserRepository {
func load(id: UserID) -> User {
User(id: id)
}
}

struct LoadUserUseCase {
let repository: any UserRepository

func execute(id: UserID) -> User {
repository.load(id: id)
}
}

struct UserProfileViewModel {
let user: User
}

@DependencyGraph
final class AppGraph {
@Provide
private func makeUserRepository() -> any UserRepository {
LiveUserRepository()
}

@Provide
private func makeLoadUserUseCase(
repository: any UserRepository
) -> LoadUserUseCase {
LoadUserUseCase(repository: repository)
}

@Provide(.transient)
private func makeUserProfileViewModel(
useCase: LoadUserUseCase,
@External userID: UserID
) -> UserProfileViewModel {
UserProfileViewModel(user: useCase.execute(id: userID))
}
}

let graph = AppGraph()
let viewModel = graph.userProfileViewModel(
userID: UserID(rawValue: "user-1")
)
```

`@Provide`는 반환 타입을 기준으로 Factory를 연결해요. 기본값은 graph마다 한 번 만들고 계속 쓰며, `.transient`는 접근할 때마다 Factory를 다시 호출해요. `@External`은 graph가 만들 수 없는 호출 시점 값에 붙여요.

Cradle은 graph를 만들 때 누락한 등록, 중복된 등록, 순환 의존성처럼 연결할 수 없는 구성을 컴파일 단계에서 알려줘요. Factory 본문에서 하는 임의 호출이나 실행 중 상태까지 검사하지는 않아요.

<details>
<summary>테스트와 Mermaid 산출물 추가하기</summary>

`CradleTesting`은 `DependencyOverride.mock` 편의 API를 제공해요. 테스트에서 `.mock`을 쓰려면 `CradleTesting`을, graph 선언도 한다면 `Cradle`을 같은 test target에 추가해요. `.replace`를 직접 쓰는 경우에는 `Cradle`만 필요해요.

```swift
.testTarget(
Expand All @@ -43,7 +131,7 @@ dependencies: [
)
```

`CradlePlugin`은 import하는 라이브러리가 아니라 macOS 빌드 호스트에서 실행하는 Build Tool Plugin입니다. Mermaid 개발 산출물이 필요한 target에만 연결합니다.
`CradlePlugin`은 Swift에서 import하지 않아요. macOS build host에서 실행되는 Build Tool Plugin이므로 Mermaid 개발 산출물이 필요할 때만 target에 연결해요.

```swift
.target(
Expand All @@ -57,31 +145,40 @@ dependencies: [
)
```

`@DependencyGraph`, `@Provide`, `@External`의 사용 조건과 생성 멤버 계약은 [DependencyGraph 안내](Sources/Cradle/Cradle.docc/DependencyGraph.md)에서 확인할 수 있습니다. 테스트 대역 교체는 [CradleTesting 안내](Sources/CradleTesting/CradleTesting.docc/CradleTesting.md)를 참고합니다.
`CradlePlugin`은 arm64 또는 x86_64 macOS build host에서만 쓸 수 있어요.

</details>

## 배포
<details>
<summary>Xcode에서 Mermaid 파일 열기</summary>

관리자는 Actions의 `Deploy SPM`을 수동으로 실행하고, 접두사 없는 `version`과 선택 `release_notes`를 입력합니다. 배포 흐름은 다음 순서로 진행합니다.
`CradlePlugin`은 build 때 Mermaid 원본을 만들어요. 외부 Xcode 프로젝트에서 파일을 바로 열려면 먼저 [CopyCradleMermaid.sh](Examples/ExampleApp/Scripts/CopyCradleMermaid.sh)를 소비자 프로젝트의 `Scripts/CopyCradleMermaid.sh`로 복사해요. Mermaid가 필요한 같은 target에 `CradlePlugin`을 연결한 뒤, target의 마지막 Run Script 단계에서 다음 명령을 실행하고 Based on dependency analysis를 선택 해제해요.

```sh
/bin/sh "${SRCROOT}/Scripts/CopyCradleMermaid.sh"
Comment thread
opficdev marked this conversation as resolved.
```

1. `version` 형식과 기존 tag를 확인합니다.
2. artifact, 원격 revision 소비자, 전체 test를 검증합니다.
3. 검증한 commit에 annotated tag를 만들고 원격 tag revision을 확인합니다.
4. exact version 소비자를 검증한 뒤 GitHub Release를 생성하고 게시 상태를 확인합니다.
`Cmd+B` 뒤 소비자 프로젝트의 `.cradle/DependencyGraph.mmd`가 생겨요. `.cradle/`은 Git에 올리지 않고 앱과 라이브러리 binary에도 넣지 않아요. Xcode의 build 도구 작업 경로는 공개된 고정 경로가 아니에요. 경고가 나타나면 `CopyCradleMermaid.sh`의 검색 경로를 확인해요.

tag push 뒤 exact version 소비자 검증이나 Release 생성에 실패하면 tag는 그대로 남고 GitHub Release는 생성되지 않습니다. 배포는 tag를 삭제하거나 이동하지 않으므로, 원인을 확인한 뒤 기존 tag를 기준으로 별도 Release 절차를 진행해야 합니다.
</details>

## Mermaid 개발 산출물
<details>
<summary>관리자용 SwiftPM 배포</summary>

`CradlePlugin`은 build 중 Mermaid 원본을 생성합니다. Xcode에서 바로 열 파일이 필요하면 target의 마지막 Run Script 단계가 `.cradle/DependencyGraph.mmd`로 복사하게 설정할 수 있습니다.
관리자는 Actions에서 `Deploy SPM` workflow를 직접 실행해요. 접두사 없는 `version`과 선택 `release_notes`를 입력하면 아래 순서로 배포돼요.

### ExampleApp 설정
1. `version` 형식과 같은 tag가 이미 있는지 확인해요.
2. artifact, 원격 revision 소비자, 전체 test를 검증해요.
3. 검증한 commit에 annotated tag를 만들고 원격 tag revision을 확인해요.
4. exact version 소비자를 검증한 뒤 GitHub Release를 생성하고 게시 상태를 확인해요.

`Examples/ExampleApp`의 `Cradle Mermaid copy` 단계는 [CopyCradleMermaid.sh](Examples/ExampleApp/Scripts/CopyCradleMermaid.sh)를 실행합니다. 같은 설정을 추가할 때는 이 단계를 target의 마지막 Build Phase에 두고 Based on dependency analysis를 선택 해제합니다.
tag를 원격에 push한 뒤 exact version 소비자 검증이 실패하면 tag는 남지만 GitHub Release는 아직 생성되지 않아요. `createRelease` 요청 뒤 응답 확인이 실패한 경우에는 GitHub에서 Release 생성 여부를 직접 확인해요. 배포 과정에서 tag를 삭제하거나 이동하지 않으므로 원인을 고친 뒤 기존 tag를 기준으로 별도 Release 절차를 진행해요.

```sh
/bin/sh "${SRCROOT}/Scripts/CopyCradleMermaid.sh"
```
</details>

`Cmd+B`를 마치면 `Examples/ExampleApp/.cradle/DependencyGraph.mmd`가 생성됩니다. `.cradle/`은 Git에서 제외되며 앱이나 라이브러리 제품에 포함되지 않습니다.
## 더 살펴보기

Xcode의 build 도구 작업 경로는 공개된 고정 경로가 아닙니다. 이 설정은 현재 `CradlePlugin` 산출물을 쉽게 열기 위한 방법이므로, Xcode 구조가 바뀐 뒤 경고가 나오면 [CopyCradleMermaid.sh](Examples/ExampleApp/Scripts/CopyCradleMermaid.sh)의 검색 경로를 확인해야 합니다.
- [ExampleApp](Examples/ExampleApp) — 단일 app target에서 `sources`, `@External`, SwiftUI `@Observable`과 `@State`를 쓰는 상품 상세 예제
- [아키텍처](docs/Architecture.md) — Macro 확장, `CradlePlugin`, Mermaid artifact 제작 경로
- [DependencyGraph 안내](Sources/Cradle/Cradle.docc/DependencyGraph.md) — `sources`, 수명, actor graph, `CradlePlugin`, 선언 조건과 compiler diagnostic
- [CradleTesting 안내](Sources/CradleTesting/CradleTesting.docc/CradleTesting.md) — `overrides: true`, `.replace`, `.mock`, graph별 테스트 대역 구성
70 changes: 70 additions & 0 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Cradle 아키텍처

Cradle은 compiler가 graph 선언을 확장하는 경로와 build 중 Mermaid를 만드는 경로를 나눠요. 둘 다 소비자 Swift source를 보지만, 서로의 결과를 사용하지 않아요.

## 소비자 build 흐름

아래 그림의 실선은 package target 의존성이에요. 점선은 compiler 확장, plugin 실행, 개발용 산출물 생성처럼 build 중에만 일어나는 흐름이에요.

```mermaid
flowchart LR
subgraph Consumer["소비자 target"]
App["앱 또는 library target<br/>Swift source"]
Test["test target"]
end

subgraph Package["Cradle package"]
Cradle["Cradle<br/>공개 Macro 선언과 runtime type"]
Macros["CradleMacros<br/>Macro 확장과 compiler diagnostic"]
Testing["CradleTesting<br/>선택 test support"]
Plugin["CradlePlugin<br/>선택 Build Tool Plugin"]
Maker["CradleDiagramMaker<br/>prebuilt executable artifact"]
Syntax["swift-syntax products"]
end

App -->|import| Cradle
Test -->|import| Cradle
Test -->|선택 import| Testing
Testing --> Cradle
Cradle --> Macros
Macros --> Syntax
App -.->|Macro 확장| Macros
Macros -.->|생성 멤버와 compiler diagnostic| App
App -.->|선택 plugin 연결| Plugin
Plugin --> Maker
Plugin -.->|현재 target Swift source 전달| Maker
Maker -.->|개발용 .mmd 생성| Output["plugin work directory<br/>CradleDiagrams/module/DependencyGraph.mmd<br/>binary 미포함"]
```

소비자 target은 `Cradle`만 import해요. `CradleMacros`는 `Cradle`이 참조하는 Macro 구현 target이므로 소비자가 직접 의존하거나 import하지 않아요. Macro는 `@DependencyGraph`와 `@Provide`를 확장하고, graph 연결이 성립하지 않는 경우 compiler diagnostic을 만들어요.

`CradleTesting`은 test target에서만 선택해요. `CradlePlugin`도 별도 경로예요. plugin은 현재 target의 Swift source를 `CradleDiagramMaker`에 전달해 `.mmd`를 만들 뿐, app이나 library binary에 연결하지 않아요.

## artifact 제작 경로

소비자 build는 prebuilt `CradleDiagramMaker`를 사용해요. 아래 target들은 그 artifact를 제작하고 검증할 때만 쓰며, 소비자 target의 의존성이 아니에요.

```mermaid
flowchart LR
Script["Scripts/build-diagram-artifact.sh"]
Artifact["Artifacts/CradleDiagramMaker.artifactbundle<br/>CradleDiagramMaker binary"]
Syntax["swift-syntax products"]
Parser["SwiftParser"]

subgraph ArtifactBuild["artifact 제작 전용 target"]
MakerSource["CradleDiagramMakerSource<br/>executable target"]
MakerSupport["CradleDiagramMakerSupport"]
Analysis["CradleGraphAnalysis"]
end

MakerSource --> MakerSupport
MakerSupport --> Analysis
MakerSupport --> Parser
Analysis --> Syntax
Script -.->|arm64와 x86_64 build| MakerSource
MakerSource -.->|artifact 제작| Artifact
```

`CradleDiagramMakerSource`는 `CradleDiagramMakerSupport`와 `CradleGraphAnalysis`로 source의 선언을 읽어요. `Scripts/build-diagram-artifact.sh`는 arm64와 x86_64 실행 파일을 만들어 artifact에 넣어요. 소비자 build에서는 이 artifact만 `CradlePlugin`이 실행해요.

Mermaid 분석은 Macro 확장과 별개예요. 따라서 이 문서는 runtime override 선택, graph 수명, `@External` 호출 경로를 설명하지 않아요. 그 계약은 [DependencyGraph 안내](../Sources/Cradle/Cradle.docc/DependencyGraph.md)에서 확인할 수 있어요.