BookaMac 엔지니어링 기록

클라우드 Mac에서 Swift 매크로 전개를 재현 가능하게 테스트하기

클라우드 Mac에서 Swift 매크로 전개를 재현 가능하게 테스트하기

한 매크로 구현을 병합한 뒤 로컬 테스트는 모두 통과했지만, 클라우드 Mac에서 실행한 다음 빌드에서는 멤버 순서가 달라졌고 진단 위치도 한 줄 어긋났다. 최종 결과물은 여전히 컴파일됐지만, 코드 리뷰에서 확인한 API는 더 이상 같은 형태가 아니었다. Swift 매크로는 컴파일 단계에서 실행되며, 그 결과는 매크로 구현뿐 아니라 SwiftSyntax 의존성과 현재 도구 체인의 영향도 받는다. 따라서 일반 함수처럼만 테스트해서는 안 된다.

먼저 고정할 결과 정의하기

회귀 테스트는 세 계층을 다뤄야 한다. 첫 번째 계층에서는 매크로 전개 후의 전체 소스를 비교해 멤버, 접근 수준, 속성, 형식의 변화를 포착한다. 두 번째 계층에서는 오류, 경고, 수정 제안을 검증하며, 특히 진단이 가리키는 토큰을 확인한다. 세 번째 계층에서야 컴파일 및 실행 테스트를 수행해 생성된 코드를 실제로 호출할 수 있는지 확인한다.

계층 검사 대상 대표적인 실패
전개 계층 생성된 소스와 선언 순서 속성 누락, 접근 수준 변경
진단 계층 메시지, 위치, 수정 제안 오류가 잘못된 노드에 표시됨
동작 계층 컴파일 및 실행 결과 생성된 구현은 컴파일되지만 동작이 잘못됨

“테스트가 컴파일된다”는 사실을 “매크로 출력이 바뀌지 않았다”는 뜻으로 받아들이지 말아야 한다. 매크로가 생성하는 소스의 형태 자체가 공개 계약인 경우가 많다.

먼저 최소 입력을 골라 기준선을 만든 다음, 빈 선언, 제네릭, 중첩 타입, 이미 존재하는 동명 멤버, 잘못된 인수에 대한 사례를 추가한다. 각 테스트 사례에서는 하나의 규칙만 검증해야 실패 시 구현 회귀인지 도구 체인 변경인지 빠르게 판단할 수 있다.

전개 테스트로 생성 소스 고정하기

매크로 패키지의 테스트 대상에 SwiftSyntaxMacrosTestSupport를 추가하면 assertMacroExpansion으로 입력과 예상 소스를 함께 지정할 수 있다. 다음 사례에서는 TraceIDMacro가 구조체에 문자열 속성을 추가한다고 가정한다.

import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import TraceIDMacros

private let macros: [String: Macro.Type] = [
    "TraceID": TraceIDMacro.self
]

final class TraceIDMacroTests: XCTestCase {
    func testAddsTraceID() {
        assertMacroExpansion(
            """
            @TraceID
            struct Job {}
            """,
            expandedSource:
            """
            struct Job {
                let traceID: String
            }
            """,
            macros: macros
        )
    }
}

예상 텍스트는 전체 형태로 유지해야 하며, traceID가 포함됐는지만 검색해서는 안 된다. 부분 문자열 단언만으로는 중복 멤버, 잘못된 들여쓰기, 의도치 않게 추가된 인터페이스를 놓칠 수 있다. 매크로가 인수를 허용한다면 기본값, 명시적 값, 잘못된 표현식에 대한 사례도 각각 작성해야 한다.

오류 경로를 별도 테스트로 분리하기

잘못된 입력을 성공적인 전개와 같은 테스트에 섞지 않는다. 진단 텍스트, 행과 열 위치, 수정 제안을 각각 단언하고, 테스트 이름에는 어떤 조건이 오류를 유발하는지 명확히 적는다. 이렇게 하면 오류 메시지 하나를 조정할 때 성공한 전체 스냅샷까지 다시 승인할 필요가 없다.

도구 체인 지문 기록하기

BookaMac의 클라우드 Mac에서는 같은 파이프라인을 장기간 실행할 수 있지만, 노드가 안정적이라고 해서 도구 체인이 영원히 바뀌지 않는 것은 아니다. 테스트할 때마다 선택된 Xcode 경로, Swift 버전, 호스트 아키텍처, 의존성 잠금 파일을 함께 보관해야 한다.

set -euo pipefail
export LANG=C
export LC_ALL=C

mkdir -p artifacts
xcode-select -p | tee artifacts/xcode-path.txt
xcrun swift --version | tee artifacts/swift-version.txt
uname -m | tee artifacts/host-arch.txt
cp Package.resolved artifacts/Package.resolved
xcrun swift test 2>&1 | tee artifacts/swift-test.log

LANGLC_ALL을 고정하면 실행 계정의 언어 환경 때문에 진단 텍스트가 달라질 가능성이 줄어든다. xcrun은 셸 경로에 우연히 포함된 다른 swift가 아니라 현재 선택된 Xcode 도구 체인으로 테스트하도록 보장한다. 저장소에 Package.resolved가 없다면 빈 파일을 만들어 잠금 결과인 것처럼 처리하지 말고, 스크립트에서 복사를 명시적으로 건너뛰어야 한다.

동시 실행과 공유 상태 격리하기

매크로 구현이 환경 변수, 현재 디렉터리, 시간, 임시 파일을 읽으면 병렬 실행 시 테스트 결과가 쉽게 흔들린다. 더 안정적인 원칙은 매크로가 구문 트리와 명시적 인수만으로 결과를 생성하도록 만드는 것이다. 외부 데이터가 반드시 필요하다면 읽기 로직을 별도 타입으로 분리하고 테스트에서는 고정된 값을 주입한다.

먼저 최소 테스트 집합을 직렬 모드로 통과시킨 뒤 병렬 테스트를 활성화한다. 병렬 실행에서만 실패한다면 재시도 횟수부터 늘리지 말고 고정 파일명, 공유 캐시 디렉터리, 전역 가변 변수, 테스트 정리 순서를 우선 확인한다. 재시도는 경쟁 상태를 간헐적인 성공으로 감출 뿐이다.

매크로 테스트가 실제 프로젝트 소스를 수정해서도 안 된다. 임시 입력은 테스트 사례마다 독립된 디렉터리에 쓰고 완료 후 정리한다. 실패한 경우에는 민감 정보를 제거한 입력, 전개 결과, 로그만 보관한다. 이렇게 해야 문제를 재현하면서도 이전 실행의 잔여물이 다음 실행에 영향을 주지 않는다.

도구 체인을 안전하게 업그레이드하기

Xcode 또는 SwiftSyntax를 업그레이드하기 전에 기존 도구 체인으로 전체 테스트를 실행해 기준선을 저장한 다음, 도구 체인을 전환하고 같은 명령을 실행한다. 차이는 다음 순서로 검토한다.

  1. 실제로 선택된 Xcode 경로와 Swift 버전을 확인한다.
  2. Package.resolved가 예상한 범위에서 변경됐는지 확인한다.
  3. 공백이나 들여쓰기 같은 형식 변화와 멤버, 타입, 접근 수준 같은 의미 변화를 구분한다.
  4. 동작 테스트를 다시 실행해 생성된 인터페이스를 호출할 수 있는지 확인한다.
  5. 검토를 모두 마친 뒤에만 예상 전개 결과를 업데이트한다.

모든 사례에서 비슷한 형식 차이가 동시에 나타난다면 먼저 출력 규칙이나 도구 체인의 변화를 조사해야 한다. 특정 경계 사례 하나만 바뀌었다면 매크로 구현 자체의 문제일 가능성이 더 크다. 일괄 치환으로 스냅샷을 바로 갱신하면 실제 API 회귀가 대량의 텍스트 변경에 묻힐 수 있으므로 피해야 한다.

최종적으로 세 종류의 산출물을 보관해야 한다. 기계가 읽을 수 있는 테스트 결과, 전개 실패 시 실제 결과와 예상 결과의 차이, 도구 체인 지문이다. 다음에 클라우드 Mac 빌드가 실패하면 더 이상 “환경이 달랐을 수 있다”는 추측에서 시작할 필요가 없다. 대신 어느 계층의 계약이 어떤 버전부터 달라졌는지 명확히 답할 수 있다.

자주 묻는 질문

최종 빌드가 성공하면 매크로 전개 테스트를 생략해도 되나요?

생략하면 안 됩니다. 빌드 성공은 생성 코드가 컴파일된다는 뜻일 뿐이며, 공개 API 형태와 진단 위치가 의도대로 유지되는지는 전개 소스와 진단을 별도로 비교해야 확인할 수 있습니다.

도구 체인 업데이트로 스냅샷이 한꺼번에 바뀌면 어떻게 처리하나요?

Xcode 경로, Swift 버전, 잠금 파일을 먼저 비교한 뒤 형식 변경과 의미 변경을 분리합니다. API와 실행 동작을 검토하기 전에는 기준 파일을 일괄 갱신하지 않는 것이 안전합니다.

전용 물리 Mac mini

검증된 워크플로를 365일 지속 운영되는 클라우드 Mac으로 옮기세요

용도에 맞춰 BookaMac M4 또는 BookaMac M4 Pro를 선택하고, 주문 시 지역·대여 기간·스토리지 추가 옵션을 확인하세요.

대여 요금제 선택