# 메타프로그래밍

메타프로그래밍이란 프로그램 자체를 데이터처럼 다루어 동적으로 코드를 생성하거나 수정할 수 있는 프로그래밍 기법이다. Dart 언어는 메타프로그래밍 기능을 제공하며, 이를 통해 런타임 시점에서 프로그램의 구조를 변경하거나, 새로운 기능을 동적으로 추가할 수 있다. 메타프로그래밍의 주요 개념은 \*\*리플렉션(Reflection)\*\*이며, 이를 통해 코드의 메타정보에 접근할 수 있다.

### 리플렉션의 개념

리플렉션은 실행 중인 프로그램의 구조를 조사하거나 변경할 수 있는 기능이다. 이를 통해 클래스, 메소드, 필드, 생성자 등의 정보를 동적으로 탐색할 수 있다. Dart에서 리플렉션을 사용하면 컴파일 시점에 알 수 없는 정보에도 접근할 수 있으며, 코드의 유연성을 높일 수 있다.

리플렉션의 일반적인 사용 사례는 다음과 같다.

1. 클래스의 인스턴스를 동적으로 생성
2. 클래스에 정의된 메소드 호출
3. 클래스에 선언된 필드 값 읽기/쓰기
4. 주석(annotation) 정보 읽기

리플렉션의 기초가 되는 라이브러리는 `dart:mirrors`이다. 이 라이브러리를 사용하면 프로그램의 구조를 런타임에 탐색하고 수정할 수 있다.

#### 예제: 클래스의 메타정보 탐색

다음은 리플렉션을 이용해 클래스의 메타정보를 동적으로 탐색하는 예제이다.

```dart
import 'dart:mirrors';

class Example {
  int x;
  String y;

  Example(this.x, this.y);

  void display() {
    print('x: $x, y:$y');
  }
}

void main() {
  // Example 클래스의 메타 정보 탐색
  ClassMirror classMirror = reflectClass(Example);

  // 클래스의 필드 정보 출력
  classMirror.declarations.forEach((key, value) {
    if (value is VariableMirror) {
      print('Field: ${MirrorSystem.getName(key)}, Type:${value.type}');
    }
  });

  // 생성자 호출
  InstanceMirror instanceMirror = classMirror.newInstance(Symbol(''), [10, 'hello']);
  instanceMirror.invoke(Symbol('display'), []);
}
```

이 예제에서 `reflectClass` 함수를 사용해 `Example` 클래스의 메타정보를 얻을 수 있다. 이 정보를 바탕으로 클래스의 필드를 탐색하거나 동적으로 인스턴스를 생성할 수 있다.

### 리플렉션의 수학적 표현

리플렉션의 개념을 수학적으로 설명할 때, 프로그램의 실행을 하나의 함수로 표현할 수 있다. 프로그램을 함수 $f$로 정의하고, 이 프로그램이 처리하는 데이터를 $x$라고 할 때, 리플렉션은 함수 $f$가 자기 자신을 입력으로 받을 수 있는 확장된 형태이다.

$$
f : x \to f(x)
$$

여기서, 리플렉션을 적용한 프로그램은 다음과 같이 자기 자신을 참조하는 새로운 함수 $f'$로 표현될 수 있다.

$$
f' : (x, f) \to f'(x, f)
$$

리플렉션을 통해 프로그램이 자기 자신의 구조를 참조하고, 수정할 수 있는 형태를 띠게 되는 것이다.

### 메타프로그래밍의 응용

메타프로그래밍은 여러 상황에서 매우 유용하게 쓰인다. 특히, 코드의 반복을 줄이거나, 런타임에 발생하는 다양한 시나리오를 동적으로 처리할 때 유용하다.

예를 들어, JSON 데이터를 클래스로 매핑하는 코드에서 메타프로그래밍을 활용할 수 있다. 일반적으로 JSON 데이터를 Dart 객체로 변환할 때는 모든 필드에 대해 일일이 매핑하는 코드를 작성해야 하지만, 메타프로그래밍을 사용하면 JSON 키와 클래스의 필드를 자동으로 연결할 수 있다.

#### 예제: JSON 자동 매핑

다음은 메타프로그래밍을 활용하여 JSON 데이터를 Dart 객체로 자동으로 변환하는 방법이다.

```dart
import 'dart:convert';
import 'dart:mirrors';

class User {
  String name;
  int age;

  User(this.name, this.age);
}

dynamic fromJson(Type type, Map<String, dynamic> json) {
  ClassMirror classMirror = reflectClass(type);
  InstanceMirror instanceMirror = classMirror.newInstance(Symbol(''), []);
  json.forEach((key, value) {
    var symbol = Symbol(key);
    if (classMirror.declarations.containsKey(symbol)) {
      instanceMirror.setField(symbol, value);
    }
  });
  return instanceMirror.reflectee;
}

void main() {
  var jsonStr = '{"name": "John", "age": 30}';
  var jsonMap = jsonDecode(jsonStr);

  User user = fromJson(User, jsonMap);
  print('Name: ${user.name}, Age:${user.age}');
}
```

이 코드에서는 `fromJson` 함수가 주어진 클래스 타입과 JSON 데이터를 바탕으로 클래스를 자동으로 매핑하여 객체를 생성한다. 리플렉션을 통해 클래스의 필드 정보를 확인하고, JSON 데이터를 해당 필드에 할당한다.

### 리플렉션의 성능 문제

메타프로그래밍은 강력한 도구이지만, 성능 측면에서 주의해야 할 점이 있다. Dart에서 리플렉션을 사용하면 런타임 시점에 프로그램의 구조를 탐색하게 되므로, 컴파일 타임에 미리 알 수 있는 정보를 런타임에 다시 확인하는 과정이 발생하게 된다. 이로 인해 성능 저하가 발생할 수 있다.

리플렉션을 사용할 때 주의해야 할 성능 문제는 다음과 같다.

1. **동적 호출 비용**: 리플렉션을 통해 동적으로 메소드를 호출하면, 컴파일 시점에 메소드를 직접 호출하는 것보다 더 많은 비용이 발생한다.
2. **메타정보 탐색 비용**: 리플렉션으로 클래스, 메소드, 필드 등의 메타정보를 탐색하는 과정에서 추가적인 연산이 필요하다.
3. **캐싱 필요성**: 리플렉션으로 얻은 메타정보는 매번 새롭게 탐색하지 않고 캐싱하여 성능을 향상시킬 수 있다.

리플렉션을 사용하는 코드에서는 이러한 성능 문제를 염두에 두고, 필요한 경우에만 사용하는 것이 바람직하다. 또한, 캐싱이나 적절한 최적화 기법을 활용하여 성능을 향상시킬 수 있다.

#### 성능 최적화를 위한 캐싱

리플렉션의 성능 문제를 해결하기 위한 한 가지 방법은 클래스 메타정보를 한 번만 탐색하고, 이후 호출 시 캐싱된 정보를 사용하는 것이다. 다음은 캐싱을 적용한 예제이다.

```dart
import 'dart:mirrors';

class Cache {
  static final Map<Type, ClassMirror> _cache = {};

  static ClassMirror getClassMirror(Type type) {
    if (!_cache.containsKey(type)) {
      _cache[type] = reflectClass(type);
    }
    return _cache[type]!;
  }
}

class Example {
  int x;
  String y;

  Example(this.x, this.y);

  void display() {
    print('x: $x, y:$y');
  }
}

void main() {
  // 캐시를 통해 메타정보 획득
  ClassMirror classMirror = Cache.getClassMirror(Example);

  // 생성자 호출
  InstanceMirror instanceMirror = classMirror.newInstance(Symbol(''), [10, 'hello']);
  instanceMirror.invoke(Symbol('display'), []);
}
```

이 코드에서는 `Cache` 클래스를 사용하여 메타정보를 한 번만 탐색하고, 이후에는 캐싱된 정보를 사용함으로써 성능을 최적화하고 있다. 이와 같은 방식으로 메타프로그래밍을 사용하는 경우 성능 문제를 줄일 수 있다.

### 주석(Annotation)과 메타프로그래밍

Dart에서는 주석(annotation)을 통해 추가적인 메타정보를 제공할 수 있다. 주석은 클래스, 메소드, 필드 등에 메타데이터를 추가하는 데 사용되며, 메타프로그래밍에서 자주 활용된다. 주석은 런타임 시 리플렉션을 통해 읽어올 수 있으며, 프로그램 동작을 동적으로 변경하는 데 유용하다.

주석을 사용하는 예제는 다음과 같다.

```dart
class JsonSerializable {
  const JsonSerializable();
}

@JsonSerializable()
class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

void main() {
  ClassMirror classMirror = reflectClass(Person);
  classMirror.metadata.forEach((metadata) {
    if (metadata.reflectee is JsonSerializable) {
      print('Person 클래스는 JsonSerializable로 표시되었다.');
    }
  });
}
```

이 예제에서는 `@JsonSerializable` 주석을 통해 `Person` 클래스가 JSON 직렬화 가능하다는 메타정보를 제공하고 있다. 리플렉션을 통해 이 정보를 읽어와 특정 동작을 수행할 수 있다.

### 주석과 수학적 해석

주석(Annotation)은 프로그래밍에서 일종의 \*\*특성값(attribute)\*\*을 정의하는 방식으로 이해할 수 있다. 이는 특정 속성 집합 $A$를 클래스나 함수에 할당하여 런타임 시점에 해당 속성 정보를 읽어오는 과정으로 해석할 수 있다.

이러한 특성값 $A$는 다음과 같은 집합의 특성을 지닐 수 있다:

$$
A = {a\_1, a\_2, \ldots, a\_n}
$$

여기서 각각의 특성 $a\_i$는 클래스나 함수에 대해 적용될 수 있는 속성을 나타내며, 주석은 이러한 속성 집합의 동적 관찰과 연관된다. 이를 통해 특정 조건에 맞는 속성을 가진 요소들만 동적으로 처리하거나 변환할 수 있다.

### 메타프로그래밍의 한계

메타프로그래밍은 매우 강력한 도구이지만, 몇 가지 한계와 주의해야 할 점이 있다. 특히, Dart 언어에서의 메타프로그래밍은 일부 제약을 가진다.

#### 컴파일 타임과 런타임의 차이

리플렉션은 런타임에 작동하는 기술이기 때문에, 컴파일 타임에 확인되지 않는 문제들이 발생할 수 있다. Dart는 JIT(Just-In-Time) 컴파일러를 사용하는 언어이기 때문에, 리플렉션에 의존하는 코드는 컴파일 시점에 성능 최적화를 적용하기 어렵다. 이에 따라, 런타임 시점에서 예기치 않은 동작이나 성능 저하가 발생할 수 있다.

다음은 Dart에서 메타프로그래밍과 관련한 주요 한계점이다:

1. **미리보기 불가**: 컴파일 시점에 클래스, 메소드, 필드 등의 구조를 확인할 수 없으므로, 리플렉션을 사용한 코드는 런타임 에러의 위험이 크다.
2. **Tree Shaking의 문제**: Dart는 트리 쉐이킹(Tree Shaking) 기법을 사용하여 사용되지 않는 코드를 제거하는데, 리플렉션을 사용하면 이 최적화가 제대로 작동하지 않는다. 즉, 프로그램에서 사용하지 않은 코드가 여전히 남아 있을 수 있다.
3. **구조화된 코드의 필요성**: 리플렉션과 메타프로그래밍은 동적인 구조를 제공하지만, 너무 과도하게 사용하면 코드의 가독성과 유지보수성이 떨어질 수 있다.

#### 리플렉션의 수학적 관점에서의 한계

리플렉션을 수학적 관점에서 표현하면, 함수 $f$가 자기 자신의 메타정보에 접근하여 다시 자신의 동작을 바꾸는 과정으로 설명될 수 있다. 하지만 모든 프로그램이 리플렉션을 사용하여 자기 자신을 동적으로 수정하는 것이 항상 가능하지는 않다. 이 문제를 수학적으로 표현하면 다음과 같다.

주어진 프로그램의 함수 $f$와 그 프로그램이 리플렉션을 통해 참조할 수 있는 메타정보 $M\_f$가 있다고 가정하자.

$$
M\_f = {m\_1, m\_2, \ldots, m\_n}
$$

이때, 함수 $f$는 이 메타정보 집합 $M\_f$에 따라 동작을 수정하는 과정을 거친다. 하지만 모든 $M\_f$가 함수 $f$에 의해 다룰 수 있는 것이 아니며, 경우에 따라서는 $M\_f$의 일부만이 함수 $f$에 의해 참조되거나 수정될 수 있다. 이러한 한계를 극복하기 위해서는 프로그램 구조 자체가 잘 정의되어 있어야 한다.

### 리플렉션 대체 기법

메타프로그래밍을 대체할 수 있는 몇 가지 기법들이 있다. 특히 Dart에서는 **코드 생성(code generation)** 기법을 사용하여 메타프로그래밍과 유사한 효과를 낼 수 있다. 코드 생성은 런타임이 아니라 컴파일 타임에 동적으로 코드를 생성하는 방식으로, 리플렉션보다 성능상 이점이 있다.

#### 코드 생성

코드 생성은 컴파일 타임에 미리 프로그램의 구조에 따라 코드를 생성하는 방식이다. Dart에서는 `build_runner`와 같은 패키지를 사용하여 코드 생성을 자동화할 수 있다. 코드 생성은 특히, JSON 직렬화와 같은 반복 작업을 자동화하는 데 유용하다.

다음은 코드 생성 기법을 사용한 JSON 직렬화 예제이다.

```dart
import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  String name;
  int age;

  User(this.name, this.age);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
```

이 예제에서 `@JsonSerializable` 주석을 사용해 JSON 직렬화 코드를 자동으로 생성할 수 있다. 코드 생성을 통해 리플렉션 없이도 유사한 기능을 구현할 수 있으며, 성능을 크게 향상시킬 수 있다.

코드 생성 기법은 메타프로그래밍과 비교해 다음과 같은 장점을 가진다:

1. **컴파일 타임에 코드 생성**: 코드 생성은 런타임이 아닌 컴파일 타임에 이루어지므로, 성능 최적화가 가능하다.
2. **트리 쉐이킹과 호환**: 코드 생성은 불필요한 코드 제거에 유리하며, 트리 쉐이킹이 원활하게 적용된다.
3. **명시적 코드**: 자동 생성된 코드가 명시적이기 때문에 디버깅과 유지보수가 쉬워진다.

### 메타프로그래밍과 코드 생성의 비교

메타프로그래밍과 코드 생성은 유사한 기능을 제공하지만, 각각의 장단점이 있다. 아래 표는 두 가지 기법을 비교한 것이다.

| **기법**      | **장점**                    | **단점**                   |
| ----------- | ------------------------- | ------------------------ |
| **메타프로그래밍** | 동적인 코드 수정 가능, 런타임의 유연성 제공 | 성능 저하, 트리 쉐이킹 문제, 복잡성 증가 |
| **코드 생성**   | 컴파일 타임에 코드 최적화 가능, 성능 우수  | 런타임의 유연성 부족, 초기 설정 복잡    |

이처럼, 상황에 따라 두 기법 중 적합한 방법을 선택하여 사용할 수 있다.
