학습
Dart 메타 프로그래밍 기초
Dart 메타 프로그래밍 기초 — 중급
메타 프로그래밍 기초
1. 도입 (왜 배워야 하는가)
메타 프로그래밍은 프로그래밍 언어의 구조와 동작을 이해하고, 이를 활용하여 더 효율적인 코드를 작성할 수 있게 해주는 강력한 기법입니다. 실무에서는 코드의 재사용성을 높이고, 반복되는 작업을 자동화하며, 동적으로 객체를 생성하는 등의 장점이 있습니다. 만약 메타 프로그래밍을 이해하지 못한다면, 중복된 코드로 인해 유지보수가 어려워지거나, 동적 프로그래밍의 이점을 놓치는 경우가 발생할 수 있습니다. 결과적으로 소프트웨어의 품질과 생산성이 저하될 수 있습니다.
2. 핵심 개념 설명
메타 프로그래밍에서는 코드가 자신의 구조를 변경하거나, 새로운 클래스를 생성할 수 있도록 합니다. 이를 이해하기 위해 몇 가지 핵심 용어를 정의하겠습니다.
| 용어 | 설명 |
|---|---|
| Reflection | 프로그램이 실행 중 자신의 구조를 알아보고 수정하는 능력 |
| Code Generation | 프로그램이 실행 중 코드를 동적으로 생성하는 과정 |
| Annotations | 메타데이터를 제공하여 코드의 의미를 설명하는 방법 |
이 개념을 이해하기 쉽게 비유하자면, 메타 프로그래밍은 '자기 자신을 거울로 보는 것'과 같습니다. 코드가 자신의 속성과 상태를 인식하고, 이를 바탕으로 행동을 결정할 수 있습니다.
3. 기본 코드 예제
다음은 Dart에서 메타 프로그래밍을 사용하는 기본적인 코드 예제입니다. 이 예제에서는 ClassMirror를 사용하여 클래스의 정보를 출력합니다.
import 'dart:mirrors';
class Person {
String name;
int age;
Person(this.name, this.age);
}
void main() {
Person person = Person('Alice', 30);
InstanceMirror im = reflect(person);
// 클래스 정보 출력
print('Class Name: ${im.type.simpleName}');
print('Fields:');
// 필드 정보 출력
im.type.declarations.forEach((key, value) {
if (value is VariableMirror) {
print(' - ${MirrorSystem.getName(key)}: ${im.getField(key).reflectee}');
}
});
}
실행 결과
Class Name: Person
Fields:
- name: Alice
- age: 30
코드 한 줄씩 동작 설명
import 'dart:mirrors';: 메타 프로그래밍을 위한mirrors라이브러리를 가져옵니다.class Person {...}:Person이라는 클래스를 정의합니다.void main() {...}: 프로그램의 시작점인main함수를 정의합니다.Person person = Person('Alice', 30);:Person클래스를 인스턴스화하여person객체를 생성합니다.InstanceMirror im = reflect(person);:person객체를 반영(reflect)하여 인스턴스 미러를 생성합니다.print('Class Name: ${im.type.simpleName}');: 클래스의 이름을 출력합니다.im.type.declarations.forEach(...): 클래스의 모든 필드를 순회하며 출력합니다.
4. 실전 활용 예제
예제 1: 동적 객체 생성
쇼핑몰에서 장바구니를 구현할 때, 사용자가 선택한 상품을 동적으로 추가할 수 있습니다. 다음은 동적으로 상품 객체를 생성하는 예제입니다.
class Product {
String name;
double price;
Product(this.name, this.price);
}
void main() {
var productData = [
{'name': 'Apple', 'price': 1.0},
{'name': 'Banana', 'price': 0.5}
];
var products = productData.map((data) {
return reflectClass(Product).newInstance(Symbol(''), [data['name'], data['price']]).reflectee;
}).toList();
products.forEach((product) {
print('Product: ${product.name}, Price: ${product.price}');
});
}
예제 2: 애노테이션 사용
API의 메타데이터를 추가하여 클라이언트와 서버 간의 통신을 정의할 수 있습니다. 다음은 애노테이션을 사용하는 예제입니다.
class ApiEndpoint {
final String path;
const ApiEndpoint(this.path);
}
@ApiEndpoint('/users')
class UserService {
// 사용자 관련 서비스 메소드
}
void main() {
var endpoint = reflectClass(UserService).metadata
.firstWhere((meta) => meta is ApiEndpoint) as ApiEndpoint;
print('API Path: ${endpoint.path}');
}
5. 흔한 실수와 주의사항
실수 1: 미러 사용 시 잘못된 타입 캐스팅
// 잘못된 코드
var fieldValue = im.getField(key) as String;
// 올바른 코드
var fieldValue = im.getField(key).reflectee as String;
잘못된 코드에서는 미러 객체를 직접 문자열로 캐스팅하려고 하여 오류가 발생할 수 있습니다.
실수 2: Reflection을 사용하지 않은 접근
// 잘못된 코드
print(person.name); // 고정된 접근 방식
// 올바른 코드
print(im.getField(Symbol('name')).reflectee); // 메타 프로그래밍 방식
고정된 접근 방식은 코드의 유연성을 저하시키고, 메타 프로그래밍의 장점을 활용하지 못합니다.