확장 메서드 (Extension methods)

확장 메서드는 기존 타입, 서드파티 라이브러리 타입에 새 기능을 추가할 수 있으며 원본 클래스 소스 코드를 수정할 필요가 없습니다. IDE 코드 자동 완성에서 네이티브 메서드와 함께 확장 메서드가 표시됩니다.

다른 사람이 제공한 API는 수정할 수 없지만 편의용 메서드를 추가하고 싶을 때 활용합니다.

요구사항 예시: int.parse('42') 대신 '42'.parseInt()를 사용하고 싶은 경우

문법 구조

extension 확장이름? on 대상타입 {
  // 메서드, getter, setter, 연산자, 정적 멤버
}Code language: JavaScript (javascript)

예제

// 모든 코드를 한 파일에 작성, 분리할 필요 없고 main이 바로 실행됨
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}

void main() {
  print('42'.parseInt());       // 42
  print('3.14'.parseDouble());  // 3.14
  print('100'.padLeft(6));      // 기본 String 메서드와 비교
}Code language: Dart (dart)

실행 결과

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
42
3.14
   100Code language: CSS (css)

확장 이름은 선택 사항입니다. this는 확장을 호출한 인스턴스 객체를 의미합니다.

확장에서 지원하는 멤버 종류

확장 내부에 인스턴스 메서드, getter, setter, 연산자, 정적 멤버를 정의할 수 있습니다.

extension StringExt on String {
  // getter
  bool get isEmptyOrBlank => trim().isEmpty;

  // 인스턴스 메서드
  String repeat(int count) => List.filled(count, this).join();

  // 연산자 오버로딩
  String operator *(int count) => repeat(count);

  // 정적 멤버 (확장 이름으로 접근)
  static String emptyStr = '';
  static void printTip() => print("문자열 확장 정적 메서드");
}

void main() {
  var str = " dart ";
  print(str.isEmptyOrBlank);
  print("hi" * 3);
  print("hello".repeat(2));

  // 정적 멤버 접근
  print(StringExt.emptyStr);
  StringExt.printTip();
}Code language: PHP (php)

실행 결과

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
false
hihihi
hellohello

문자열 확장 정적 메서드Code language: CSS (css)

이름 없는 확장(익명 확장)

확장 이름을 적지 않으면 현재 라이브러리 내에서만 접근 가능하며 명시적 호출로 이름 충돌을 해결할 수 없습니다.

외부에서 확장이름(인스턴스) 구문으로 호출할 수 없습니다.

extension on String {
  bool get isBlank => trim().isEmpty;
}

void main() {
  print("   ".isBlank); // true
}Code language: Dart (dart)

익명 확장 안의 정적 멤버는 확장 내부에서만 사용 가능합니다.

제네릭 확장(Generic extensions)

List<T>와 같은 제네릭 타입에 확장을 구현할 수 있습니다.

extension MyFancyList<T> on List<T> {
  int get doubleLength => length * 2;
  List<T> operator -() => reversed.toList();
  List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

void main() {
  List<int> list = [1, 2, 3, 4];
  print(list.doubleLength);          // 8
  print(-list);                      // [4,3,2,1]
  print(list.split(2));              // [[1,2],[3,4]]
}Code language: Dart (dart)

실행 결과

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
8
[4, 3, 2, 1]
[[1, 2], [3, 4]]Code language: CSS (css)

정적 타입(static) vs dynamic

dynamic 변수에서는 확장 메서드를 호출할 수 없음

확장 메서드는 정적 해석 방식으로 처리됩니다. 런타임에 타입이 결정되는 dynamic 변수는 확장을 찾지 못합니다:

void main() {
  dynamic d = '2';
  // print(d.parseInt()); // 런타임에서 NoSuchMethodError 발생

  var v = '2';
  print(v.parseInt()); // 타입 추론으로 String으로 인식되어 정상 실행
}

extension NumberParsing on String {
  int parseInt() => int.parse(this);
}Code language: JavaScript (javascript)

원인: 확장 메서드는 컴파일 시점에 변수의 정적 타입 기준으로 바인딩되며 런타임에 탐색하지 않기 때문입니다.

API 이름 충돌 해결 방안

여러 확장에 동일한 이름의 멤버가 존재할 때 사용하는 세 가지 방법입니다.

1: import 시 show / hide로 충돌하는 확장 가리기

// string_apis.dart에 NumberParsing 존재
// string_apis_2.dart에 NumberParsing2, 동일하게 parseInt 구현
import 'string_apis.dart';
import 'string_apis_2.dart' hide NumberParsing2;

void main() {
  print('42'.parseInt()); // string_apis.dart의 구현 사용
}Code language: Dart (dart)

2: 확장을 명시적으로 호출(래퍼 구문)

import 'string_apis.dart';    // NumberParsing
import 'string_apis_2.dart';  // NumberParsing2

void main() {
  // print('42'.parseInt()); // 충돌로 컴파일 오류
  print(NumberParsing('42').parseInt());
  print(NumberParsing2('42').parseInt());
}Code language: Dart (dart)

3: 라이브러리 가져올 때 접두사 사용

두 확장 이름이 완전히 같을 경우 접두사로 구분합니다:

import 'string_apis.dart';
import 'string_apis_3.dart' as rad;

void main() {
  print(NumberParsing('42').parseInt());
  print(rad.NumberParsing('42').parseInt());

  // 충돌 없는 확장 메서드는 암시적 호출 가능
  print('42'.parseNum());
}Code language: Dart (dart)

확장 메서드 (Extension methods)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다