Swift 매크로는 컴파일의 의미 분석 단계에서 확장되며, 구문 분석 후 최종 추상 구문 트리(AST)에 대한 유형 검사 이전에 구체적으로 이루어집니다. 이 시점은 매크로 확장이 전체 유형 체크 및 의미 검증을 거쳐야 하는 코드를 생성할 수 있도록 하므로 매우 중요합니다. Swift는 이러한 단계에서 작업하여 확장된 코드가 언어의 유형 안전성 보증을 위반하거나 접근 제어 수식자를 우회할 수 없도록 합니다.
문제는 매크로가 소스 코드를 변환하여 새로운 구문 노드를 생성함으로써 발생하며, 이로 인해 주변 어휘적 범위의 기존 변수와 충돌하는 식별자를 도입할 가능성이 있습니다. 매크로가 단순히 하드코딩된 변수 이름을 주입하면 호출 контекст의 변수를 우연히 잡거나 가릴 수 있습니다. 이는 생성된 코드가 호출자의 논리에 간섭하여 미세한 버그나 보안 취약점을 초래할 수 있습니다.
이를 해결하기 위해 Swift는 모든 합성 바인딩에 대해 고유한 내부 식별자를 사용하는 위생적 매크로 시스템을 사용합니다. 컴파일러는 원래의 어휘적 컨텍스트를 추적하는 메타데이터를 구문 노드에 붙이며, 생성된 식별자가 사용자 작성 코드와 다르게 취급되도록 합니다. 이 메커니즘은 매크로가 이름 충돌 위험 없이 임시 변수를 안전하게 도입할 수 있게 하며, 필요할 경우 명시적 매개변수 전달을 통해 의도적인 이름 캡처를 허용합니다.
우리 팀은 의존성 주입을 위한 Swift 패키지를 구축하고 있었고, 이에 @Injectable이라는 첨부된 매크로를 사용하여 복잡한 서비스 클래스의 초기화 코드를 자동으로 생성했습니다. 매크로는 생성 중에 중간 의존성을 보유할 임시 변수를 만들어야 했지만, 일반적인 변수 이름인 container나 service가 대상 클래스 범위 내에서 이미 존재할 수도 있다는 위험이 있었습니다. 이는 안전한 초기화 코드를 생성하더라도 이름 충돌로 인해 클라이언트 코드가 깨지거나 미세한 재할당 버그가 발생할 수 있다는 딜레마를 만들었습니다.
우리는 처음에 문자열 템플릿을 사용하여 초기자 구현을 생성하는 단순한 텍스트 기반 코드 생성 접근 방식을 구현할 것을 고려했습니다. 주요 장점은 구현의 단순성이었으며, 생성된 Swift 코드를 즉시 검사하고 직접 디버깅할 수 있었습니다. 그러나 주요 단점은 위생 보증의 부족으로, 임시 변수 이름이 대상 클래스의 기존 속성과 충돌하지 않도록 보장할 메커니즘이 없었으며, 이는 컴파일 실패 또는 매크로가 우연히 기존 인스턴스 변수를 재할당하여 발생하는 논리적 오류의 침묵을 초래할 수 있었습니다.
그 후 우리는 Sourcery라는 성숙한 제3자 코드 생성 도구를 사용하는 것을 평가했습니다. 이 도구는 Swift 컴파일러 외부에서 사전 컴파일 단계로 작동합니다. 장점으로는 방대한 문서, 유연한 스텐실 템플릿, 인라인 코드뿐만 아니라 전체 파일을 생성할 수 있는 능력이 포함되었습니다. 불행히도 단점으로는 Xcode에서 추가 Run Script 단계가 필요한 복잡한 빌드 도구 통합과 외부 프로세스 오버헤드로 인해 크게 느려진 빌드 시간, 생성된 코드에서의 유형 오류가 컴파일 시간에만 드러나고 원래 매크로 호출에 대한 명확한 소스 매핑이 누락되는 문제점이 있었습니다.
결국 우리는 Swift 5.9에서 도입된 Swift의 기본 매크로 시스템을 선택했고, 서비스 클래스 선언에 첨부된 동등한 매크로를 활용했습니다. 이 솔루션은 컴파일러 파이프라인에 직접 통합되어 확장된 코드에 대한 컴파일 타임 유형 검사를 제공하고, SwiftSyntax 라이브러리를 통해 생성된 식별자에 대한 내장 위생을 제공합니다. 결과적으로, @Injectable 매크로는 이름 그림자의 위험 없이 복잡한 초기화 논리를 안전하게 생성할 수 있으며, 약 70%의 보일러플레이트 코드를 줄이면서 완전한 컴파일 타임 안전 보장 및 매크로 사용 위치를 직접 가리키는 명확한 오류 메시지를 유지했습니다.
최종 구현은 이전의 수동 의존성 주입 설정에서 괴롭혔던 이름 관련 버그의 전체 범주를 제거했습니다. 빌드 시간은 Sourcery 접근 방식에 비해 40% 개선되었으며, 개발자들은 매크로가 생성한 초기자가 수동 동기화 없이 새로운 의존성에 자동으로 적응할 것이라는 확신을 가지고 서비스 클래스를 리팩토링할 수 있었습니다.
왜 Swift의 매크로는 기존 코드를 제자리에서 수정할 수 없으며, 유사한 의미를 달성하는 대체 패턴은 무엇인가요?
기존의 구문 노드를 제자리에서 변환할 수 있는 Lisp 또는 Rust 절차적 매크로와는 달리, Swift 매크로는 순수하게 추가적입니다. 즉, 새 코드를 생성할 수만 있으며 원래 소스를 수정할 수는 없습니다. 이 제한은 Swift의 컴파일 모델에서 원래 소스가 디버깅, 소스 매핑 및 점진적 컴파일 목적을 위해 그대로 유지되어야 한다는 요구에 의해 존재합니다. "수정" 의미를 달성하기 위해 개발자는 추가 오버로드나 래퍼 타입을 생성하는 동등 매크로를 사용해야 하며, 원래 선언에 대해 비추천 주석을 결합하여 생성된 대안으로의 마이그레이션을 안내해야 합니다.
매크로 확장이 생성된 표현식에 대한 유형 추론을 어떻게 처리하며, 추론에 실패하면 어떻게 되나요?
매크로가 명시적인 유형 주석 없이 표현이 포함된 코드로 확장될 때, Swift는 매크로 확장이 이루어진 후의 표준 유형 검사 단계에서 생성된 AST에 대해 유형 추론을 수행합니다. 추론에 실패하면, 컴파일러는 확장에서 첨부된 소스 위치 메타데이터를 사용하여 오류 위치를 매크로 호출 위치로 매핑하는 진단 메시지를 발생시킵니다. 후보자들은 종종 매크로가 #file 및 #line 리터럴을 명시적으로 생성하거나 #sourceLocation 지시어를 사용하여 진단이 사용자에게 어떻게 나타나는지를 제어할 수 있다는 점을 놓치며, 이로 인해 오류가 내부 매크로 구현 세부정보가 아닌 의미 있는 위치를 가리킨다는 것을 보장합니다.
자유롭게 사용 가능한 매크로와 첨부된 매크로 간의 구문 확장 컨텍스트 및 사용 가능한 의미 정보의 차이는 무엇인가요?
프리 스탠딩 매크로( #로 접두어가 붙음)는 표현식이나 문장 수준에서 확장되며 주변 타입 정보에 대한 접근이 제한적입니다. 반면에 첨부된 매크로( @로 접두어가 붙음)는 선언에 대해 작동하며, 첨부된 선언의 구문, 접근 제어자 및 상속 관계와 같은 풍부한 의미 정보를 수신합니다. 초보자들은 이러한 경계를 종종 혼동하여 타입 멤버에 접근하거나 특정 타입 범위 내에서 중첩된 선언을 생성하기 위해 필요한 첨부 동등 매크로 대신 프리 스탠딩 매크로를 사용하고자 합니다.