확장 메서드
확장 메서드 (Extension methods)
확장 메서드(extension methods)는 기존 라이브러리에 기능을 추가해요. IDE에서 코드 완성을 쓰다 보면 일반 메서드 옆에 확장 메서드가 함께 제안되는 걸 본 적이 있을 거예요. 모르는 사이에 이미 확장 메서드를 쓰고 있는 셈이죠.
본문
개요 (Overview)
남이 만든 API를 쓰고 있거나, 널리 쓰이는 라이브러리를 구현 중이라면 API 자체를 바꾸는 게 현실적으로 어렵거나 불가능할 때가 많아요. 그래도 기능을 추가하고 싶을 때가 있죠.
다음처럼 문자열을 정수로 파싱하는 코드를 생각해 볼게요.
int.parse('42')
이 기능이 보다 짧고 도구로 쓰기 쉬운 형태로 String 위에 있으면 좋겠다 싶은 거죠.
'42'.parseInt()
이 코드를 가능하게 하려면 String 클래스의 확장을 담은 라이브러리를 import하면 돼요.
import 'string_apis.dart';
void main() {
print('42'.parseInt()); // Use an extension method.
}
확장은 메서드뿐 아니라 getter, setter, 연산자(operator) 같은 다른 멤버도 정의할 수 있어요. 그리고 확장은 이름을 가질 수 있는데, API 충돌이 생길 때 유용하죠. String을 대상으로 하는 NumberParsing이라는 확장으로 parseInt()를 구현하는 방법을 볼게요.
// lib/string_apis.dart
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
다음 섹션부터는 확장 메서드를 사용하는 방법을, 그 다음에는 구현하는 방법을 다룰게요.
확장 메서드 사용하기 (Using extension methods)
모든 Dart 코드가 그렇듯 확장 메서드도 라이브러리에 있어요. 확장 메서드를 사용하는 방법은 이미 봤죠. 그 확장이 담긴 라이브러리를 import해서 평범한 메서드처럼 쓰면 돼요.
// Import a library that contains an extension on String.
import 'string_apis.dart';
void main() {
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.
}
보통 확장 메서드를 쓰기 위해 필요한 건 이게 전부예요. 코드를 쓰다 보면 확장 메서드가 static 타입에 어떻게 의존하는지(dynamic이 아니라), 그리고 API 충돌을 어떻게 해결하는지도 알면 도움이 돼요.
Static 타입과 dynamic
dynamic 타입의 변수에서는 확장 메서드를 호출할 수 없어요. 예를 들어 다음 코드는 런타임 예외를 일으켜요.
dynamic d = '2';
print(d.parseInt()); // Runtime exception: NoSuchMethodError
확장 메서드는 Dart의 타입 추론과는 잘 동작해요. 다음 코드는 변수 v가 String 타입으로 추론되기 때문에 문제없이 동작하죠.
var v = '2';
print(v.parseInt()); // Output: 2
dynamic에서 안 되는 이유는, 확장 메서드가 수신자(receiver)의 static 타입을 기준으로 해석되기 때문이에요. 확장 메서드는 정적으로 해석되므로, static 함수를 호출하는 것만큼 빠르죠.
API 충돌 (API conflicts)
확장 멤버가 인터페이스나 다른 확장 멤버와 충돌하면, 몇 가지 선택지가 있어요.
한 가지 방법은 충돌하는 확장을 가져오는 방식을 show나 hide로 바꿔서 노출되는 API를 제한하는 거예요.
// Defines the String extension method parseInt().
import 'string_apis.dart';
// Also defines parseInt(), but hiding NumberParsing2
// hides that extension method.
import 'string_apis_2.dart' hide NumberParsing2;
void main() {
// Uses the parseInt() defined in 'string_apis.dart'.
print('42'.parseInt());
}
또 다른 방법은 확장을 명시적으로 적용하는 거예요. 코드가 마치 래퍼 클래스처럼 보이게 되죠.
// Both libraries define extensions on String that contain parseInt(),
// and the extensions have different names.
import 'string_apis.dart'; // Contains NumberParsing extension.
import 'string_apis_2.dart'; // Contains NumberParsing2 extension.
void main() {
// print('42'.parseInt()); // Doesn't work.
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());
}
두 확장의 이름이 같다면, 접두어(prefix)를 붙여 import해야 할 수도 있어요.
// Both libraries define extensions named NumberParsing
// that contain the extension method parseInt(). One NumberParsing
// extension (in 'string_apis_3.dart') also defines parseNum().
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;
void main() {
// print('42'.parseInt()); // Doesn't work.
// Use the ParseNumbers extension from string_apis.dart.
print(NumberParsing('42').parseInt());
// Use the ParseNumbers extension from string_apis_3.dart.
print(rad.NumberParsing('42').parseInt());
// Only string_apis_3.dart has parseNum().
print('42'.parseNum());
}
예시에서 보듯, 접두어를 붙여 import해도 확장 메서드를 암시적으로 호출할 수 있어요. 접두어를 써야 하는 경우는 확장을 명시적으로 호출할 때 이름 충돌을 피하려고 할 때뿐이에요.
확장 메서드 구현하기 (Implementing extension methods)
확장을 만들 때는 다음 문법을 사용해요.
extension <extension name>? on <type> { // <extension-name> is optional
(<member definition>)* // Can provide one or more <member definition>.
}
예를 들어 String 클래스의 확장을 이렇게 구현할 수 있어요.
// lib/string_apis.dart
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
double parseDouble() {
return double.parse(this);
}
}
확장의 멤버는 메서드, getter, setter, 연산자일 수 있어요. 확장은 static 필드와 static 헬퍼 메서드도 가질 수 있어요. 확장 선언 밖에서 static 멤버에 접근하려면, 클래스 변수와 메서드처럼 선언 이름을 통해 호출하면 돼요.
이름 없는 확장 (Unnamed extensions)
확장을 선언할 때 이름을 생략할 수도 있어요. 이름 없는 확장은 선언된 라이브러리 안에서만 보여요. 이름이 없으므로 API 충돌을 해결하기 위해 명시적으로 적용할 수도 없죠.
extension on String {
bool get isBlank => trim().isEmpty;
}
제네릭 확장 구현하기 (Implementing generic extensions)
확장은 제네릭 타입 매개변수를 가질 수 있어요. 예를 들어 내장 List<T> 타입에 getter, 연산자, 메서드를 추가하는 코드를 볼게요.
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)];
}
여기서 타입 T는 메서드가 호출되는 리스트의 static 타입을 기준으로 결정돼요.
더 알아보기 (Learn more)
- Dart 공식 문서 - Extension methods 원문 살펴보기
- 관련 자료: "Dart Extension Methods Fundamentals" 아티클, 기능 명세(feature specification), 확장 메서드 샘플
- Dart의 static 타입과 dynamic의 차이는 'The Dart type system' 문서에서 더 자세히 볼 수 있어요.