스칼라 3 매크로 해부: 인용과 스플라이싱을 통한 심층 탐구
발행: (2025년 12월 8일 오전 03:48 GMT+9)
6 min read
원문: Dev.to
Source: Dev.to
Introduction
컴파일‑타임 메타프로그래밍의 세계에 오신 것을 환영합니다, Scala 3! 매크로는 마법처럼 보일 수 있지만, quoting과 splicing이라는 강력한 시스템 위에 구축됩니다. 이 글에서는 임의의 케이스 클래스를 Map[String, Any] 로 변환하는 실용적인 매크로를 분석합니다. 마지막까지 읽으면 매크로가 무엇을 하는지, 어떻게 동작하는지, 그리고 이를 구동하는 저수준 리플렉션 개념을 이해하게 될 것입니다.
Macro Code
package com.dev24.macros
import scala.quoted.*
object ToMapMacro {
// The user‑facing, inline method
extension [A](inline a: A) inline def toMap: Map[String, Any] = ${ toMapImpl('a) }
// The implementation that runs at compile time
def toMapImpl[A: Type](instance: Expr[A])(using Quotes): Expr[Map[String, Any]] = {
import quotes.reflect.*
// 1. Get the type and symbol of A
val tpe = TypeRepr.of[A]
val sym = tpe.typeSymbol
// 2. Get the list of case class fields
val fields: List[Symbol] =
if (sym.isClassDef) sym.caseFields.toList
else Nil
// 3. For each field, generate a quoted (String, Any) tuple
val mapEntries: List[Expr[(String, Any)]] = fields.map { field =>
val fieldNameExpr = Expr(field.name)
val fieldAccess = Select(instance.asTerm, field)
val fieldValueExpr = fieldAccess.asExprOf[Any]
'{ ($fieldNameExpr, $fieldValueExpr) }
}
// 4. Assemble the final Map from the list of tuples
'{ Map(${Varargs(mapEntries)}*) }
}
}
Part 1 – The Gateway to Metaprogramming: The inline Method
extension [A](inline a: A) inline def toMap: Map[String, Any] = ${ toMapImpl('a) }
extension [A]은 모든 타입A에 메서드를 추가해myCaseClass.toMap과 같은 호출을 가능하게 합니다.inline a: A는 컴파일러에게 런타임 메서드를 실행하는 대신 매크로 결과로 호출 지점을 교체하도록 지시합니다.${ … }은 splice 연산자로, 일반 코드를 매크로 코드와 연결합니다. 내부의 표현식은 컴파일러에 의해 실행됩니다.toMapImpl('a)은 매크로 구현을 호출하면서a의 quoted 표현('a)을 전달합니다. 이 인용은 값을 AST(Expr[A]) 로 올려주는 것이지 런타임 값을 전달하는 것이 아닙니다.
컴파일러가 person.toMap 을 만나면 일시 중지하고, person 의 quoted AST 로 toMapImpl 을 호출한 뒤, 새로 만든 quoted Map 을 받아 다시 프로그램에 splice 합니다.
Part 2 – The Engine Room: Macro Implementation
def toMapImpl[A: Type](instance: Expr[A])(using Quotes): Expr[Map[String, Any]]
A: Type은 컨텍스트 바인드로, 매크로가A의 구조를 검사할 수 있게 하는Type[A](컴파일‑타임 타입 태그)를 제공합니다.instance: Expr[A]은 변환 대상 값의 quoted AST 입니다.using Quotes는 매크로의 도구 상자를 제공하며, 리플렉션 API 에 접근할 수 있게 합니다.- 이 메서드는 실제 맵이 아니라 quoted
Map[String, Any](Expr[Map[String, Any]]) 를 반환합니다.
Low‑Level Reflection
import quotes.reflect.* 은 핵심 리플렉션 타입들을 가져옵니다:
TypeRepr– 타입의 풍부한 내부 표현.Symbol– 선언(클래스, 필드, 메서드 등)의 고유 식별자.Term– 표현식이나 문장의 AST 노드.Expr–Term을 감싸는 타입‑안전 래퍼.
Step 1 – Inspecting the Type
val tpe = TypeRepr.of[A]
val sym = tpe.typeSymbol
TypeRepr.of[A]는A의 전체 컴파일‑타임 설명을 얻습니다.tpe.typeSymbol은 해당 타입의 기본Symbol(예: 케이스 클래스 정의)을 추출합니다.
Step 2 – Finding the Fields
val fields: List[Symbol] =
if (sym.isClassDef) sym.caseFields.toList
else Nil
sym.isClassDef은 심볼이 클래스를 나타내는지 확인합니다.sym.caseFields는 케이스 클래스의 필드 심볼 리스트를 반환하고, 그렇지 않으면 빈 리스트를 사용합니다.
Step 3 – Generating Map Entries
val mapEntries: List[Expr[(String, Any)]] = fields.map { field =>
val fieldNameExpr = Expr(field.name)
val fieldAccess = Select(instance.asTerm, field)
val fieldValueExpr = fieldAccess.asExprOf[Any]
'{ ($fieldNameExpr, $fieldValueExpr) }
}
- 각 필드마다 quoted 튜플
(fieldName, fieldValue)을 생성합니다. Select(instance.asTerm, field)은 주어진 인스턴스에서 해당 필드에 접근하는 term 을 만듭니다.- 튜플은
'{ … }로 quoted 표현식으로 올려집니다.
Step 4 – Assembling the Final Map
'{ Map(${Varargs(mapEntries)}*) }
Varargs(mapEntries)은 튜플 표현식 리스트를Map.apply의 가변 인자로 splice 합니다.- 전체 표현식은 quoted 되어, 런타임에 맵을 구성하는 코드를 생성합니다.
Conclusion
매크로는 다음과 같이 동작합니다:
- 케이스 클래스 인스턴스의 quoted 표현을 받는다.
- 컴파일‑타임 리플렉션을 사용해 클래스의 필드를 발견한다.
- 그 필드들로부터
Map[String, Any]를 만들 코드(quoted)를 생성한다. - 생성된 코드를 호출 지점에 splice 한다.
이 예시는 Scala 3 의 quoting과 splicing, 그리고 저수준 리플렉션이 어떻게 강력한 컴파일‑타임 코드 생성을 가능하게 하면서도 사용자에게는 깔끔하고 관용적인 API 를 제공하는지를 보여줍니다.