🔥 매크로 확장

573자
9분

Swift 프로그래밍 언어에는 강력한 기능 중 하나인 매크로(Macro)라는 것이 있답니다. 매크로를 사용하면 코드를 더욱 간결하고 표현력 있게 작성할 수 있죠. 그런데 이 매크로라는 것이 어떻게 동작하는 걸까요? 바로 매크로 확장(Macro Expansion)이라는 과정을 통해서랍니다. 지금부터 Swift 컴파일러가 어떻게 매크로를 확장하는지 자세히 알아보겠습니다.

lecture image

Swift에서 매크로를 사용한 코드를 빌드할 때, 컴파일러는 다음과 같은 단계를 거쳐 매크로를 확장한답니다.

  1. 컴파일러가 코드를 읽고, 코드의 구조를 나타내는 메모리 내 표현을 생성해요.
  2. 컴파일러는 이 메모리 내 표현의 일부를 매크로 구현부로 전달하고, 매크로를 확장하죠.
  3. 컴파일러는 매크로 호출 부분을 확장된 형태로 대체합니다.
  4. 컴파일러는 확장된 소스 코드를 사용하여 컴파일을 계속 진행해요.

이 과정을 좀 더 구체적으로 살펴볼까요? 다음 코드를 보시죠.

let magicNumber = #fourCharacterCode("ABCD")
swift

여기서 #fourCharacterCode 매크로는 4글자로 이루어진 문자열을 받아서, 해당 문자열의 ASCII 값들을 이어붙인 32비트 부호 없는 정수를 반환한답니다. 이런 식으로 정수를 사용하면 디버거에서도 읽기 쉬우면서도 컴팩트한 데이터 식별자를 만들 수 있죠. 이 매크로를 어떻게 구현하는지는 아래에서 살펴볼 거예요.

자, 이제 위 코드에 있는 매크로를 확장하는 과정을 단계별로 알아보겠습니다. 먼저 컴파일러는 Swift 파일을 읽고, 추상 구문 트리(Abstract Syntax Tree, AST)라고 하는 메모리 내 코드 표현을 생성해요. AST는 코드의 구조를 명시적으로 나타내기 때문에, 컴파일러나 매크로 구현처럼 코드 구조와 상호작용하는 코드를 작성하기 쉽답니다. 아래는 위 코드에 대한 AST를 나타낸 것인데요, 약간 단순화된 버전이에요.

lecture image

위 다이어그램은 이 코드의 구조가 메모리에 어떻게 표현되는지 보여주고 있어요. AST의 각 요소는 소스 코드의 일부에 대응되죠. "Constant declaration" AST 요소는 그 아래 두 개의 하위 요소를 갖는데, 이는 상수 선언의 두 부분인 이름과 값을 나타냅니다. "Macro call" 요소는 매크로의 이름과 매크로에 전달되는 인수 목록을 나타내는 하위 요소를 갖고 있네요.

AST를 구성하는 과정에서 컴파일러는 소스 코드가 유효한 Swift 코드인지 확인해요. 예를 들어, #fourCharacterCode는 단일 문자열 인수를 받아야 합니다. 만약 정수 인수를 전달하거나 문자열 리터럴 끝에 따옴표(")를 빠뜨렸다면, 이 시점에서 오류가 발생할 거예요.

컴파일러는 코드에서 매크로를 호출하는 위치를 찾아내고, 해당 매크로를 구현하는 외부 바이너리를 로드합니다. 그리고 각 매크로 호출에 대해, 컴파일러는 AST의 일부를 해당 매크로의 구현부로 전달하죠. 아래는 이렇게 전달되는 부분 AST를 나타낸 것입니다.

lecture image

#fourCharacterCode 매크로의 구현부는 매크로를 확장할 때 이 부분 AST를 입력으로 받아요. 매크로 구현부는 입력으로 받은 부분 AST에 대해서만 동작하기 때문에, 매크로는 항상 동일한 방식으로 확장된답니다. 이런 제한은 매크로 확장을 이해하기 쉽게 만들어주고, 변경되지 않은 매크로의 확장을 피할 수 있게 해줘서 코드 빌드 속도를 높여준답니다.

Swift는 매크로 작성자가 실수로 다른 입력을 읽는 것을 방지하기 위해 매크로를 구현하는 코드를 제한해요.

  • 매크로 구현부에 전달되는 AST는 매크로를 나타내는 AST 요소만 포함하고, 그 앞뒤에 오는 코드는 포함하지 않아요.
  • 매크로 구현부는 파일 시스템이나 네트워크에 접근할 수 없는 샌드박스 환경에서 실행됩니다.

이러한 안전장치 외에도, 매크로 작성자는 매크로 입력 외부의 것을 읽거나 수정하지 않도록 해야 해요. 예를 들어, 매크로의 확장은 현재 시간에 의존해서는 안 됩니다.

#fourCharacterCode의 구현부는 확장된 코드를 포함하는 새로운 AST를 생성하죠. 아래는 컴파일러에 반환되는 코드예요.

lecture image

컴파일러가 이 확장된 결과를 받으면, 매크로 호출을 포함하는 AST 요소를 매크로의 확장 결과를 포함하는 요소로 교체해요. 매크로 확장 후에 컴파일러는 프로그램이 여전히 구문적으로 유효한 Swift이고 모든 타입이 올바른지 다시 확인합니다. 그러면 평소처럼 컴파일할 수 있는 최종 AST가 생성되죠.

lecture image

이 AST는 다음과 같은 Swift 코드에 해당합니다.

let magicNumber = 1145258561 as UInt32
swift

이 예제에서는 입력 소스 코드에 매크로가 하나뿐이지만, 실제 프로그램에서는 같은 매크로의 여러 인스턴스와 여러 매크로가 있을 수 있어요. 컴파일러는 한 번에 하나씩 매크로를 확장하죠.

한 매크로가 다른 매크로 내부에 나타나면, 외부 매크로가 먼저 확장됩니다. 이렇게 하면 외부 매크로가 내부 매크로를 확장하기 전에 수정할 수 있게 되죠.

이렇게 매크로 확장 과정을 살펴보니 좀 더 명확해졌나요? Swift의 강력한 매크로 기능을 활용하려면 이 과정을 이해하는 게 중요하답니다. 한 번 직접 매크로를 만들어보면서 이 과정을 체험해보는 것도 좋겠죠? 이어지는 장에서 fourCharacterCode 매크로를 직접 구현해 보겠습니다.