Dart에서 주석과 애노테이션은 코드의 가독성을 높이고, 메타데이터를 활용하는 중요한 도구로 사용된다. 특히 애노테이션은 리플렉션을 통해 런타임 시점에서 코드에 추가적인 의미를 부여하거나 동작을 변경할 수 있게 해준다. 이 장에서는 주석과 애노테이션의 사용법과 적용 사례를 상세히 다룬다.

주석의 기본 개념

주석은 코드 내에서 설명을 추가하기 위한 텍스트이다. Dart에서는 주석을 다음과 같은 방식으로 작성할 수 있다.

한 줄 주석

// 이 코드는 변수 a에 5를 할당한다.
int a = 5;

블록 주석

/*
이 함수는 두 개의 정수를 더한 결과를 반환한다.
매개변수:
- x: 첫 번째 정수
- y: 두 번째 정수
*/
int add(int x, int y) {
  return x + y;
}

문서 주석

문서 주석은 dartdoc 도구를 사용하여 자동으로 API 문서를 생성할 수 있다. 이는 코드에 대한 설명을 표준화된 형식으로 제공하며, 클래스나 함수, 변수에 대한 자세한 정보를 포함할 수 있다.

/// 두 숫자를 더하는 함수이다.
/// 
/// [x]와 [y]는 더할 두 정수이다.
/// 반환값은 두 숫자의 합이다.
int add(int x, int y) {
  return x + y;
}

애노테이션의 기본 개념

애노테이션(Annotation)은 코드에 메타데이터를 추가하는 방법이다. Dart에서는 @ 기호를 사용하여 애노테이션을 적용하며, 주로 클래스, 함수, 변수 등에 메타데이터를 추가하는 데 사용된다.

애노테이션의 주요 역할은 다음과 같다: - 코드에 의미 부여 - 특정 런타임 동작을 트리거 - 테스트, 디버깅, 성능 최적화 시 중요한 정보 전달

기본 애노테이션 사용법

가장 기본적인 Dart의 애노테이션은 @override이다. 이는 상속된 클래스에서 메소드를 재정의할 때 사용된다. 예를 들어, 부모 클래스의 메소드를 자식 클래스에서 재정의할 때, @override 애노테이션을 사용하여 의도적으로 재정의한 것임을 나타낸다.

class Parent {
  void sayHello() {
    print('Hello from Parent');
  }
}

class Child extends Parent {
  @override
  void sayHello() {
    print('Hello from Child');
  }
}

위 코드에서 @overridesayHello 메소드가 부모 클래스의 동일한 메소드를 재정의했음을 나타낸다. 만약 이 애노테이션을 생략할 경우, Dart 컴파일러는 실수로 인해 재정의가 잘못되었는지 확인할 수 없기 때문에, 이를 명시적으로 사용함으로써 코드의 가독성과 안전성을 높일 수 있다.

사용자 정의 애노테이션

Dart에서는 직접 애노테이션을 정의하여 사용할 수도 있다. 이를 통해 특정 클래스나 메소드에 추가적인 정보를 제공하거나, 리플렉션을 사용하여 런타임에서 동작을 결정할 수 있다. 애노테이션은 일반적으로 클래스 형태로 정의되며, 클래스 이름 앞에 @를 붙여 사용한다.

다음은 애노테이션을 정의하고 사용하는 예시이다.

// 애노테이션 정의
class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

// 애노테이션 사용
@Todo('John Doe', 'Implement the login feature')
void login() {
  // 로그인 기능 구현
}

이 코드에서 @Todo 애노테이션은 login 함수에 메타데이터를 추가하여 누가 어떤 작업을 해야 하는지 나타낸다. Dart의 const 키워드를 사용하여 애노테이션 클래스를 정의하면, 해당 애노테이션은 컴파일 타임에 적용된다.

애노테이션의 활용 사례

애노테이션은 다양한 방식으로 활용될 수 있으며, 주로 다음과 같은 상황에서 사용된다.

리플렉션을 통한 애노테이션 활용

리플렉션은 런타임에 코드 구조를 탐색하고 수정할 수 있는 기능이다. Dart에서 리플렉션은 dart:mirrors 패키지를 통해 제공되며, 이를 사용하여 애노테이션이 적용된 요소를 확인하거나 동적으로 처리할 수 있다.

다음은 리플렉션을 통해 애노테이션이 적용된 메소드를 확인하는 예시이다.

import 'dart:mirrors';

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

@Todo('Alice', 'Implement the logout feature')
void logout() {
  print('Logging out...');
}

void main() {
  // 리플렉션을 사용하여 애노테이션 정보 확인
  var mirror = reflect(logout);
  var metadata = mirror.function.metadata;

  for (var annotation in metadata) {
    if (annotation.reflectee is Todo) {
      var todo = annotation.reflectee as Todo;
      print('Todo: ${todo.who}, Task:${todo.what}');
    }
  }
}

이 코드에서 reflect() 메소드를 사용하여 함수 logout에 적용된 애노테이션을 탐색하고, 애노테이션의 값인 whowhat을 출력한다. 리플렉션을 통해 이러한 애노테이션을 런타임에 동적으로 처리할 수 있다.

애노테이션의 활용 범위

Dart에서 애노테이션은 클래스, 함수, 변수, 매개변수 등 다양한 요소에 적용될 수 있다. 애노테이션의 적용 범위는 코드를 설계하는 데 있어 매우 유연하게 활용될 수 있다. 주석과는 달리, 애노테이션은 실제로 코드 실행에 영향을 미칠 수 있기 때문에, 적절한 상황에서 이를 사용함으로써 코드의 유지보수성과 확장성을 높일 수 있다.

클래스에 애노테이션 적용

클래스에 애노테이션을 적용하여, 해당 클래스가 어떤 역할을 수행하는지 메타데이터를 추가할 수 있다.

@deprecated
class OldClass {
  // 오래된 클래스 로직
}

위 코드에서 @deprecated 애노테이션은 OldClass가 더 이상 사용되지 않는 클래스임을 나타낸다. 이 애노테이션은 IDE나 개발 도구에서 경고 메시지를 출력하여, 개발자에게 해당 클래스의 사용을 피할 것을 권장한다.

매개변수에 애노테이션 적용

Dart에서는 함수의 매개변수에도 애노테이션을 적용할 수 있다. 이를 통해 함수의 특정 매개변수가 어떤 역할을 하는지 명확하게 설명하거나, 추가적인 처리를 할 수 있다.

void printMessage(@required String message) {
  print(message);
}

이 코드에서 @required 애노테이션은 message 매개변수가 필수임을 나타내며, 이 함수가 호출될 때 반드시 해당 매개변수가 전달되어야 함을 명시한다.

변수에 애노테이션 적용

변수에도 애노테이션을 추가하여, 해당 변수의 특성을 설명하거나 특정 규칙을 적용할 수 있다.

class Person {
  @nonVirtual
  final String name;

  Person(this.name);
}

이 예시에서 @nonVirtual 애노테이션은 name 변수가 더 이상 재정의되지 않도록 보호한다.

애노테이션으로 메타데이터 처리

메타데이터는 코드 실행과는 직접적인 관련이 없지만, 코드가 더 명확하게 이해될 수 있도록 돕는 중요한 정보를 제공한다. 애노테이션은 이러한 메타데이터를 코드에 부여하여, 다른 개발자나 도구들이 코드를 해석하는 데 도움이 된다.

사용자 정의 애노테이션의 고급 활용

사용자 정의 애노테이션을 이용하면 특정 코드에 대한 정보를 구조적으로 저장하고 런타임에서 그 정보를 이용하여 특정 동작을 수행할 수 있다. Dart에서 애노테이션은 상수로 정의되기 때문에 컴파일 타임에 해당 애노테이션이 어떻게 사용될지를 결정할 수 있다. 사용자 정의 애노테이션을 통해 런타임에서 동적 분석을 하거나 특정 메타데이터를 참조하여 동작을 제어할 수 있다.

사용자 정의 애노테이션의 예

아래 예제는 애노테이션을 사용하여 특정 API 메소드가 어떤 HTTP 요청 메소드에 해당하는지를 명시하는 경우이다.

class HttpMethod {
  final String method;
  const HttpMethod(this.method);
}

@HttpMethod('GET')
void fetchUser() {
  print('Fetching user data...');
}

@HttpMethod('POST')
void createUser() {
  print('Creating new user...');
}

위 예제에서 @HttpMethod('GET')와 같은 사용자 정의 애노테이션을 통해 fetchUser 함수가 GET 요청에 대응한다는 것을 명시할 수 있다. 이와 같이 API를 개발할 때 애노테이션을 이용하면 각 함수가 담당하는 HTTP 메소드를 명확하게 구분할 수 있다.

리플렉션을 사용한 API 요청 처리

위에서 정의한 HttpMethod 애노테이션을 기반으로 리플렉션을 통해 API 요청을 처리하는 코드를 작성할 수 있다. 런타임에서 각 메소드에 어떤 HTTP 메소드가 적용되었는지 동적으로 확인하고, 그에 따라 적절한 로직을 수행하게 할 수 있다.

import 'dart:mirrors';

void handleApiCall(Symbol methodName) {
  var mirror = reflectClass(ApiHandler);
  var method = mirror.declarations[methodName] as MethodMirror;

  if (method.metadata.isNotEmpty) {
    for (var meta in method.metadata) {
      if (meta.reflectee is HttpMethod) {
        var httpMethod = meta.reflectee as HttpMethod;
        print('Handling API call with HTTP method: ${httpMethod.method}');
      }
    }
  }
}

class ApiHandler {
  @HttpMethod('GET')
  void getUser() {
    print('User data retrieved');
  }

  @HttpMethod('POST')
  void addUser() {
    print('New user added');
  }
}

void main() {
  handleApiCall(#getUser);  // Handling API call with HTTP method: GET
  handleApiCall(#addUser);  // Handling API call with HTTP method: POST
}

위 코드에서 handleApiCall 함수는 리플렉션을 통해 getUseraddUser 메소드에 적용된 애노테이션을 확인하고, 해당 메소드가 어떤 HTTP 메소드를 처리하는지 동적으로 출력한다. 이처럼 애노테이션을 활용하면 API의 메타데이터를 간결하게 관리하고, 코드의 유지보수성을 높일 수 있다.

애노테이션과 데이터 검증

애노테이션은 코드에서 데이터 검증 규칙을 정의하는 데에도 사용할 수 있다. 예를 들어, Dart에서 폼 입력을 처리할 때 애노테이션을 사용하여 특정 필드가 필수 입력인지, 또는 특정 패턴을 만족해야 하는지 등을 지정할 수 있다.

데이터 검증 애노테이션 예시

다음 예시는 데이터 검증을 위한 애노테이션을 정의하고, 이를 사용하는 방법을 보여준다.

class Required {
  const Required();
}

class Email {
  const Email();
}

class UserForm {
  @Required
  String name;

  @Email
  String email;

  UserForm(this.name, this.email);
}

void validate(Object form) {
  var mirror = reflect(form);

  mirror.type.declarations.forEach((key, declaration) {
    var field = mirror.getField(key);
    for (var meta in declaration.metadata) {
      if (meta.reflectee is Required && field.reflectee == null) {
        print('${MirrorSystem.getName(key)} is required.');
      }
      if (meta.reflectee is Email && !RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(field.reflectee)) {
        print('${MirrorSystem.getName(key)} is not a valid email.');
      }
    }
  });
}

void main() {
  var form = UserForm(null, 'not_an_email');
  validate(form);  // Output: name is required. email is not a valid email.
}

이 코드에서 @Required 애노테이션은 필수 입력 필드를 나타내며, @Email 애노테이션은 이메일 형식의 유효성을 검사하는 역할을 한다. validate 함수는 리플렉션을 통해 해당 필드가 적절한 값을 가지고 있는지 확인하고, 규칙을 위반할 경우 오류 메시지를 출력한다.


Dart의 주석과 애노테이션은 코드의 가독성을 높이는 동시에 런타임 동작에 중요한 영향을 미칠 수 있는 강력한 도구이다. 주석은 주로 코드의 설명이나 문서화를 위한 용도로 사용되지만, 애노테이션은 메타데이터를 코드에 부여하여 더욱 유연한 구조를 만들 수 있다. 애노테이션은 리플렉션과 결합하여 다양한 런타임 동작을 제어하고, 코드의 확장성과 유지보수성을 높이는 데 중요한 역할을 한다.