Bylaws: Swift를 위한 아키텍처 린터
요약
Bylaws는 Swift 프로젝트의 아키텍처 규칙을 검사하는 린터 도구입니다. 명명 규칙, 필수 프로토콜 준수 여부, 모듈 간 의존성 등 컴파일러가 강제하지 못하는 프로젝트 전반의 구조적 규칙들을 정의하고 실행할 수 있습니다. 개발자뿐만 아니라 코딩 에이전트가 생성한 코드까지 일관된 아키텍처를 유지하도록 돕습니다.
핵심 포인트
- 컴파일러가 놓치는 아키텍처 규칙 검사 가능
- 명명 규칙, 의존성 순환 등 구조적 문제 감지
- Swift Testing 또는 CLI로 쉽게 통합 및 실행
- 개발자와 AI 에이전트 모두에게 일관된 가이드 제공
Bylaws
Bylaws는 개발자와 코딩 에이전트를 위해 제작된 Swift용 아키텍처 린터입니다.
[![Swift versions][Swift versions badge]][Swift Package Index]
[![Platforms][Platforms badge]][Swift Package Index]
[![CI][CI badge]][CI]
가이드와 API 레퍼런스는 [문서(documentation)]를 읽어보세요.
목차
Bylaws를 사용하는 이유
모든 프로젝트에는 컴파일러가 강제할 수 없는 규칙들이 있습니다. 일부는 명명 규칙(naming conventions), 필수 프로토콜, 또는 코드베이스의 어느 부분이 특정 API를 호출할 수 있는지와 같은 개별 선언이나 API 사용에 관한 것일 수 있습니다. 다른 일부는 폴더 레이아웃이나 코드베이스의 서로 다른 부분 간에 허용되는 의존성 등 프로젝트 전반을 설명합니다.
AI 코딩 에이전트는 팀이 더 빠르게 움직이는 데 도움을 줄 수 있지만, 이들이 생성하는 변경 사항은 프로젝트 규칙을 깨뜨리거나 라인별로 검토하기 어려울 수도 있습니다. 스킬 파일(Skill files)과 AGENTS.md는 유용한 컨텍스트를 제공할 수 있지만, 모든 지침이 일관되게 적용됨을 보장할 수는 없습니다.
Bylaws를 사용하면 익숙한 Swift API를 사용하여 프로젝트 규칙을 작성하고 이를 Swift Testing 또는 CLI를 통해 실행할 수 있습니다. 변경 사항이 규칙을 위반하는 경우, Bylaws는 개발자와 코딩 에이전트 모두가 무엇을 수정해야 하는지 알 수 있도록 관련 파일과 라인을 보고합니다.
규칙이 검사할 수 있는 것
규칙은 선언(declarations), 함수 호출 및 기타 Swift 구문을 검사할 수 있습니다. 파일과 타입이 어디에 속하는지 확인할 수 있고, Package.swift 매니페스트를 소스 코드의 임포트와 비교하거나, 의존성 순환(dependency cycles)을 찾거나, 파일, 폴더 또는 모듈 간의 참조를 제한할 수 있습니다.
대부분의 규칙은 소스 코드와 프로젝트 파일에 대한 접근만 필요하므로, CLI가 빌드 없이 실행할 수 있으며 Swift Testing은 일반적인 테스트 과정의 일부로 테스트 타겟을 컴파일합니다. 만약 어떤 규칙이 컴파일러가 해결한 참조(compiler resolved references)를 필요로 한다면, 최신 빌드에서 컴파일러 인덱스를 읽을 수 있습니다.
Bylaws 사용해 보기
로그 계층 내부에서 콘솔 출력을 유지하는 규칙을 가진 Bylaws.swift 파일을 생성합니다:
import Bylaws
import Testing
...
이 파일을 프로젝트 루트에 저장하여 CLI로 실행하거나, 테스트 타겟에 배치하여 Swift Testing으로 실행할 수 있습니다.
CLI에서 예제 실행하기
bylaws를 설치한 다음, 프로젝트 루트에서 규칙을 실행합니다:
brew install theblixguy/tap/bylaws
bylaws lint
만약 Sources/App.swift가 print를 호출하면, 명령어는 상태 1로 종료되며 해당 호출과 이를 보고한 규칙을 가리킵니다:
/path/to/App/Sources/App.swift:2:3: error: print violates 'Console output goes through the logging layer' [central-logging]
/path/to/App/Bylaws.swift:7:3: note: rule 'central-logging' is declared here
Checked 1 rule: 1 violation.
Swift Testing으로 예제 실행하기
Bylaws 제품을 테스트 타겟에 추가한 다음, 이 테스트를 추가합니다. Swift Testing으로 시작하는 경우, 동일한 파일에서 projectRules를 정의하고 나중에 공유되는 Bylaws.swift로 옮길 수 있습니다. 어느 위치든 .automatic()은 패키지 루트를 찾으므로 Sources/**는 같은 파일을 선택할 것입니다.
import Bylaws
import Testing
...
규칙이 나머지 테스트와 함께 실행됩니다:
swift test
만약 규칙이 위반 사항을 발견하면, 테스트가 실패하고 출력은 영향을 받은 코드를 가리킵니다. [시작하기 가이드]에는 패키지 의존성이 포함되어 있으며 Swift Testing과 CLI 간에 하나의 규칙 파일을 공유하는 방법을 설명합니다.
새로운 규칙이 기존 문제를 보고할 경우, 결과를 검토하거나 기준선(baseline)을 기록하여 새로운 위반 사항이 검사를 실패하도록 유지할 수 있습니다. [규칙 채택 가이드]에는 두 가지 옵션 모두가 포함되어 있습니다.
실용적인 규칙
이 예시들은 작성할 수 있는 몇 가지 검사(check)들을 보여줍니다. [규칙 핸드북 (rule cookbook)]과 다른 가이드에서 API에 대해 더 다루고 있으니, 프로젝트에 맞는 규칙을 선택할 수 있습니다.
레이어 간 종속성 제어
만약 Checkout이 Domain에서 선언된 API를 통해 저장된 데이터를 읽고, Persistence가 그 구현을 제공한다고 가정해 봅시다. Checkout과 Persistence는 Domain을 가져올(import) 수 있지만, Checkout은 스토리지 구현에 독립적으로 유지하기 위해 반드시 Domain의 API를 사용해야 합니다.
만약 Persistence가 Checkout의 타겟 종속성(target dependency)이라면, 이는 디자인을 깨뜨리더라도 임포트가 컴파일됩니다. 다음 규칙은 의도된 경계(boundary)를 강제합니다:
let appLayers = Layering(
Layer("Domain", files: ["Sources/Domain/**"]),
Layer(
...
이 제한 사항은 여기에 나열된 레이어들 사이에서 적용됩니다. Foundation과 같은 다른 모듈의 임포트는 계속 허용됩니다. 만약 Checkout이 Persistence를 가져온다면, bylaws lint는 경계를 넘는 임포트를 지적할 것입니다:
/path/to/App/Sources/Checkout/CheckoutViewModel.swift:2:8: error: import Persistence violates 'Modules follow their declared dependencies' [feature-boundaries]
Checked 1 rule: 1 violation.
만약 이 폴더들이 하나의 앱 타겟 내에 존재한다면, [컴파일러 인덱스 (compiler index)]를 사용하여 모듈 임포트 없이도 동일한 경계를 강제할 수 있습니다.
스코프가 지정된 잠금(locking) 선호하기
프로젝트에서 NSLock.withLock을 사용한다면, SwiftSyntax 방문자(visitor)를 작성하지 않고도 직접적인 lock() 및 unlock() 호출을 보고할 수 있습니다:
let manualLocking = Matcher<SourceNode>("call lock or unlock directly") {
$0.call?.references("lock.lock") == true
|| $0.call?.references("lock.unlock") == true
...
규칙은 노드의 부모(parent)나 조상(ancestors)을 사용하여 다른 컨텍스트에서 동일한 호출을 구별할 수 있습니다.
화면 모델에 공유 기본 클래스 요구하기
앱이 BaseScreenModel에서 공유 화면 동작을 유지한다면, ScreenModel에 준수하는 클래스는 해당 동작을 상속받아야 합니다. 이 규칙은 준수가 확장(extension)이나 중간 클래스(intermediate class)로부터 오는 경우에도 확인할 수 있습니다:
Rule("screen-model-base", "Screen models inherit BaseScreenModel") {
try await app.classes
.where(.conforms(to: "ScreenModel") && !.named("BaseScreenModel"))
...
식별자에는 도메인 타입을 사용하세요
CustomerID와 OrderID 같은 개별 타입은 컴파일러가 고객 ID가 필요한 곳에 주문 ID가 전달되는 것을 포착하도록 합니다. 이 규칙은 Sources/Domain 아래에서 이름이 ID로 끝나고, 타입 주석이 String, Int 또는 UUID를 지정하는 속성들을 보고합니다:
Rule("domain-identifiers", "Identifiers use domain types") {
try await app.properties
.under("Sources/Domain")
...
저장된 enum 케이스에 명시적인 값을 부여하세요
문자열 기반(string-backed) enum 케이스의 이름을 변경하면 암묵적 원시 값(implicit raw value)도 변경됩니다. 앱이 이러한 값을 저장하는 경우, 이름 변경에도 저장 형식이 변하지 않도록 명시적인 원시 값을 요구할 수 있습니다. 이 규칙은 Sources/Storage 아래의 Codable enum을 확인합니다:
Rule("stored-enum-values", "Stored enum cases declare explicit values") {
try await app.enums
.under("Sources/Storage")
...
환경설정 접근은 하나의 타입에 유지하세요
UserDefaults를 호출하는 함수들은 앱이 환경설정 키와 기본값을 관리하는 PreferencesStore 내부에 보관할 수 있습니다:
Rule("preferences-access", "Functions that call UserDefaults belong to PreferencesStore") {
try await app.functions
.where(.calls("UserDefaults"))
...
[선언 가이드(declaration guide)]에서는 속성 초기화자(property initialisers)와 접근자(accessors)의 호출에 대한 별도의 쿼리를 보여줍니다.
각 기능의 파일은 올바른 폴더에 유지하세요
각 기능이 반드시 Models, ViewModels, 그리고 Views를 자식 폴더로 포함하도록 요구할 수 있으며, 누락되거나 추가된 폴더가 있을 경우 위반으로 간주합니다:
별도의 규칙은 타입이 어디에 속하는지 확인합니다:
Rule("view-model-folders", "View 모델은 ViewModels 폴더에 속해야 합니다") {
try await app.types.under("Sources/App").suffixed("ViewModel")
.violations(outsidePaths: "Sources/App/*/ViewModels/**")
...
[폴더 가이드]에는 여러 피처에서 폴더를 공유하는 모듈에 대한 뷰와 모델 검사 및 예시가 포함되어 있습니다.
또한 [허용되는 참조 설정 및 파일 그룹 간의 순환 참조 확인]을 하거나, 각 피처 뷰에 대한 뷰 모델과 각 리포지토리에 대한 테스트 타입을 같은 방식으로 [필수화]할 수 있습니다.
패키지 의존성을 임포트와 일치시키기
동일한 Package.swift 내의 타겟 간에 존재하는 의존성이 해당 모듈의 임포트와 일치하는지 확인할 수 있습니다. 이 규칙은 타겟이 선언하지 않은 모듈을 임포트하거나, 타겟이 더 이상 임포트하지 않는 사용되지 않은 의존성이 있을 때 누락된 의존성을 보고합니다.
애플리케이션 및 테스트 소스를 모두 포함하는 코드베이스를 app 옆에 정의합니다:
let packageCodebase = Codebase(
root: .automatic(),
including: ["Sources/**", "Tests/**"]
...
타겟이 다른 디렉토리를 사용하는 경우, 해당 경로를 including에 추가하여 임포트도 검사되도록 합니다. 그런 다음 이 규칙을 projectRules에 추가합니다:
Rule("package-dependencies", "Package.swift가 소스 임포트를 일치시킵니다") {
try await packageCodebase.checkPackageDependencies()
}
Bylaws는 검사할 수 없는 타겟에 대해 경고를 보고합니다.
[규칙 책]에는 프로토콜 요구 사항, SwiftUI 상태, 생명 주기 호출 및 컴파일러가 해결한 참조도 다루고 있습니다.
시작하기
요구사항
- Swift: 6.2 이상 (Xcode 26 이상에 포함)
- 라이브러리: iOS 13 이상, macOS 14 이상 또는 Linux
- CLI 및 언어 서버: macOS 14 이상 또는 Linux
규칙 실행 방식 선택
| 워크플로우 | 사용 시점 | 결과 |
|---|---|---|
[bylaws CLI] | 빌드 전 또는 CI에서 규칙을 실행해야 할 때 | Xcode, GitHub, JSON 또는 SARIF 형식의 위반 사항 |
| [Swift Testing] | 규칙이 테스트 스위트와 함께 속하거나 전체 Swift 언어가 필요할 때 | Xcode에 소스 진단이 포함된 테스트 케이스 |
[!NOTE]
CLI에서 실행되는 경우와 CLI가 지원하는 Swift 서브셋을 사용하는 테스트 타겟 모두 동일한 규칙을 실행할 수 있습니다. [시작 가이드]에서는 이들을 공유하는 방법을 설명하며, [SwiftPM 플러그인], [Bazel 타겟], 그리고 [에디터 통합] 섹션에서는 해당 워크플로우의 요구 사항을 다룹니다.
명령줄에서 규칙 실행
[CLI 설정 가이드]에는 macOS 및 Linux에서의 설치 방법이 포함되어 있습니다. 프로젝트 루트에 Bylaws.swift를 두고 다음을 실행합니다:
bylaws lint
대신 경고 전용 규칙으로 시작하려면, Bylaws.swift 파일이 없는 프로젝트에서 bylaws init을 실행하세요. 또한 기준선(baseline)을 기록하여 기존 위반 사항은 허용하고 새로운 위반 사항만 거부할 수 있습니다.
CI에서는 --changed-path를 사용하여 이전 실행 이후 변경된 전체 파일 목록을 전달할 수 있습니다. Bylaws는 해당 파일에 영향을 받은 규칙을 재실행하고 나머지는 캐시된 결과를 사용합니다. [CLI에서 규칙 실행]은 이 워크플로우, 디스커버리(discovery), 공유 규칙 패키지, 기준선 및 CI를 다룹니다.
Swift Testing으로 규칙 실행
테스트 타겟에 패키지와 Bylaws 제품을 추가합니다:
dependencies: [
.package(
url: "https://github.com/theblixguy/swift-bylaws.git",
...
macOS에서 Bylaws는 컴파일러와 일치하는 사전 구축된 SwiftSyntax 라이브러리를 사용하므로, SwiftPM은 첫 번째 테스트 빌드 중에 SwiftSyntax를 컴파일하는 것을 건너뛸 수 있습니다. SwiftPM은 Linux 및 다른 컴파일러 버전에서는 SwiftSyntax 소스 패키지를 사용합니다.
Bylaws를 테스트에서만 사용할 경우 traits: []로 설정하거나, 플러그인, CLI 또는 언어 서버도 사용하는 경우에는 이 설정을 생략하세요.
Bylaws 사용해보기의 규칙 파일을 Tests/AppTests/Bylaws.swift에 넣고, 해당 매개변수화된 테스트는 Tests/AppTests/ArchitectureTests.swift에 넣어주세요.
나머지 테스트와 함께 규칙을 실행하려면 다음 명령어를 사용하세요:
swift test
규칙 위반 시 테스트가 실패하며, 영향을 받은 파일과 줄을 보고합니다. 만약 특정 규칙을 .advisory로 지정하면, 해당 위반 사항은 경고(warning)로 표시됩니다.
CLI에서 동일한 규칙을 실행하려면, 규칙 파일을 bylaws lint에 전달하세요:
bylaws lint --rules Tests/AppTests/Bylaws.swift
또한 선언 가이드에 나와 있는 것처럼 일치하는 선언마다 테스트 케이스를 하나씩 만들 수도 있습니다. 플러그인과 에디터용으로 프로젝트 루트에 공유 규칙 파일을 유지하려면, 테스트 타겟의 일부로 규칙을 컴파일하는 대신 [테스트 디스커버리(test discovery)]를 사용하세요. bylaws init으로 생성된 파일도 디스커버리를 사용하는 것이 좋습니다. 왜냐하면 해당 파일들은 테스트 타겟에서 변경 없이 컴파일할 수 없는 CLI 구문을 사용하기 때문입니다.
개발 중 검사 실행
SwiftPM 플러그인 사용
AI 자동 생성 콘텐츠
본 콘텐츠는 GitHub Claude Ecosystem의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기