Harmonize로 Swift 아키텍처 규칙을 유닛 테스트로 강제하기
핵심 요약 Harmonize는 Swift 아키텍처 규칙을 정규식 대신 유닛 테스트로 강제하는 오픈소스 린터다. Perry Street Software가 공개했고 Apache-2.0, 최신 1.2.0. 설치·규칙 작성·SwiftLint와의 차이를 실무 관점에서 정리했다.
Harmonize는 Swift 코드베이스의 아키텍처 규칙을 정규식이 아니라 유닛 테스트로 강제하는 오픈소스 린터다. iOS·서버 등 Swift 프로젝트에서 "이 계층은 저 모듈을 import 하면 안 된다" 같은 팀 규약을 테스트 코드로 적어 두면, 규칙을 어긴 코드가 들어올 때 테스트가 빨간불을 낸다. Perry Street Software가 공개했고, 2026년 7월 기준 GitHub 태그의 최신 버전은 1.2.0이다. 이 글은 Harmonize를 처음 도입하려는 개발자를 대상으로 설치와 규칙 작성, SwiftLint와의 차이를 실무 관점에서 정리한다. DSL의 모든 셀렉터를 나열하지는 않으며, 세부 API는 공식 문서를 함께 보는 편이 좋다.
정규식 린터가 멈추는 지점
SwiftLint 같은 기존 린터는 정규식(문자 패턴으로 텍스트를 찾는 규칙)으로 코드를 검사한다. 들여쓰기나 네이밍 컨벤션처럼 한 줄 안에서 판단되는 스타일 규칙에는 잘 맞는다. 하지만 "프레젠테이션 계층은 도메인 계층만 import 한다" 같은 구조 규칙은 정규식으로 표현하기가 까다롭다. 파일 경계를 넘는 관계나 상속 구조를 텍스트 매칭만으로 확인하기 어렵기 때문이다.
Harmonize는 이 지점을 다르게 잡는다. 코드를 텍스트가 아니라 AST(추상 구문 트리, 컴파일러가 코드를 이해하는 구조화된 표현)로 분석해서 클래스·함수·프로퍼티·import 같은 요소를 직접 질의하고 단언한다. Perry Street Software 공식 저장소에 따르면 이 도구는 Kotlin의 Konsist와 Java의 ArchUnit에서 영감을 받았고, Apache-2.0 라이선스로 공개돼 있다. AI가 만든 코드가 코드베이스에 빠르게 섞여 드는 흐름에서 사람 리뷰만으로 구조를 지키기 어려워졌다는 문제의식이 배경이다. 코드를 이해하는 일 자체가 병목이 된 상황에서, 아키텍처 규칙을 기계가 반복 검증하도록 넘기려는 시도로 볼 수 있다.
설치와 설정은 어떻게 하나
Swift Package Manager로 테스트 타깃에 의존성을 추가하면 된다. GitHub 태그 기준 2026년 7월 8일 공개된 1.2.0이 최신 버전이라, Package.swift에는 아래처럼 적는다.
dependencies: [
.package(url: "https://github.com/perrystreetsoftware/Harmonize.git", from: "1.2.0"),
]그다음 이 패키지를 테스트 타깃(테스트 코드가 들어가는 빌드 단위)의 의존성 목록에 넣는다. 규칙을 프로덕션 타깃이 아니라 테스트 타깃에 두는 이유는, 규칙 자체를 테스트로 돌려 CI에서 실패를 잡기 위해서다. 마지막으로 프로젝트 루트에 .harmonize.yaml 파일을 둔다. 공식 저장소 설명에 따르면 이 파일은 비어 있어도 되고, excludes 키로 특정 폴더나 파일을 검사에서 뺄 수 있다. 자동 생성 코드나 서드파티 폴더를 규칙 대상에서 제외할 때 쓴다.
규칙 하나를 유닛 테스트로 작성하기
규칙은 평범한 테스트 함수 안에 쓴다. 아래는 이름이 "ViewModel"로 끝나는 모든 클래스가 BaseViewModel을 상속하는지 검사하는 규칙이다. Perry Street Software가 공식 README에서 소개하는 대표 예제와 같은 형태다.
import Harmonize
import XCTest
final class ArchitectureTests: XCTestCase {
func testViewModelsInheritBaseViewModel() {
Harmonize.productionCode()
.classes()
.withNameEndingWith("ViewModel")
.assertTrue(message: "ViewModel은 BaseViewModel을 상속해야 한다") {
$0.inherits(from: "BaseViewModel")
}
}
}체인을 읽으면 규칙이 그대로 문장이 된다. productionCode()로 프로덕션 코드를 잡고, classes()로 클래스만 추린 다음, withNameEndingWith("ViewModel")로 대상 범위를 좁힌다. assertTrue의 클로저 안에서 각 클래스가 조건을 만족하는지 단언하고, 어긋나면 message에 적은 문구와 함께 테스트가 실패한다. 예외를 두고 싶으면 withoutName 같은 필터로 특정 타입을 규칙에서 빼면 된다. 실행은 XCTest뿐 아니라 Quick, Swift Testing에서도 가능하다.
SwiftLint와 무엇이 다른가
둘은 경쟁 관계라기보다 역할이 갈린다. SwiftLint는 코드 스타일과 단순 컨벤션을, Harmonize는 파일 경계를 넘는 아키텍처 규칙을 맡는다. 핵심 차이를 정리하면 다음과 같다.
| 구분 | SwiftLint | Harmonize |
|---|---|---|
| 규칙 표현 | 정규식·설정 파일 | 유닛 테스트 코드 |
| 분석 수준 | 텍스트·라인 단위 | AST 시맨틱 단위 |
| 주 용도 | 스타일·컨벤션 | 계층·의존성 구조 |
| 실행 방식 | 전용 실행 명령 | 테스트 스위트 |
그래서 실무에서는 둘을 함께 쓰는 편이 자연스럽다. 줄 길이나 강제 언랩 경고는 SwiftLint에 맡기고, 계층 간 import 방향처럼 팀이 합의한 구조 규약은 Harmonize로 못박는 식이다.
실무에서 강제할 만한 규칙들
구조 규약을 코드로 옮겨 두면 리뷰에서 매번 지적하던 항목이 자동 검사로 바뀐다. Perry Street Software가 블로그와 예제에서 든 규칙들을 실무 관점에서 추리면 다음과 같다.
- 의존성 방향 고정: 프레젠테이션 계층 파일이 UseCase 같은 특정 모듈을 import 하지 못하게 막아, 계층이 역방향으로 얽히는 것을 차단한다. on("경로")로 검사 범위를 특정 디렉터리로 좁힐 수 있다.
- 메모리 누수 방지: ViewModel 함수 안의 클로저가 self를 약한 참조(weak)로 캡처하는지 검사해, 순환 참조로 생기는 누수를 초기에 잡는다.
- 네이밍 규약: 특정 접미어로 끝나는 타입이 정해진 프로토콜을 따르거나 지정한 폴더에만 존재하도록 강제한다.
도입은 한 번에 모든 규칙을 켜기보다, 팀이 이미 합의했지만 리뷰에서 자꾸 새는 규약 한두 개부터 테스트로 옮기는 편이 부담이 적다. 규칙이 늘수록 새 코드가 구조를 벗어날 때 CI가 먼저 알려 주므로, AI가 초안을 써 주더라도 사람이 구조를 판단해야 하는 몫이 리뷰 코멘트에서 실행되는 테스트로 옮겨 간다. 버전에 따라 API 세부가 달라질 수 있으니 도입 전 최신 릴리스 노트를 확인하는 편이 안전하다. 1.2.0 릴리스에서는 파싱을 병렬화하고 위반 리포팅을 XCTest에서 분리했다고 저장소가 밝히고 있다.