스칼라 3 매크로 해부: 인용과 스플라이싱을 통한 심층 탐구

발행: (2025년 12월 8일 오전 03:48 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Introduction

컴파일‑타임 메타프로그래밍의 세계에 오신 것을 환영합니다, Scala 3! 매크로는 마법처럼 보일 수 있지만, quotingsplicing이라는 강력한 시스템 위에 구축됩니다. 이 글에서는 임의의 케이스 클래스를 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) 은 매크로 구현을 호출하면서 aquoted 표현('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 노드.
  • ExprTerm 을 감싸는 타입‑안전 래퍼.

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

매크로는 다음과 같이 동작합니다:

  1. 케이스 클래스 인스턴스의 quoted 표현을 받는다.
  2. 컴파일‑타임 리플렉션을 사용해 클래스의 필드를 발견한다.
  3. 그 필드들로부터 Map[String, Any] 를 만들 코드(quoted)를 생성한다.
  4. 생성된 코드를 호출 지점에 splice 한다.

이 예시는 Scala 3 의 quoting과 splicing, 그리고 저수준 리플렉션이 어떻게 강력한 컴파일‑타임 코드 생성을 가능하게 하면서도 사용자에게는 깔끔하고 관용적인 API 를 제공하는지를 보여줍니다.

Back to Blog

관련 글

더 보기 »