Dart 2.15 발표

Dart 2.15 발표

오늘 Dart SDK 버전 2.15를 출시해요. 워커 아이솔레이트(worker isolates)를 이용한 빠른 동시성, 새 생성자 tear-off 언어 기능, dart:core 라이브러리의 개선된 enum 지원, 패키지 게시자용 새 기능 등이 담겨 있어요.

출처: Announcing Dart 2.15

본문

워커 아이솔레이트로 빠른 동시성

요즘 거의 모든 현대 기기의 CPU에는 여러 코어가 있어서 여러 작업을 병렬로 실행할 수 있어요. 대부분의 Dart 프로그램에서 이 코어들이 어떻게 쓰이는지는 개발자로서는 투명해요. Dart 런타임 시스템은 기본적으로 모든 Dart 코드를 단일 코어에서 실행하지만, 파일을 쓰거나 네트워크 호출을 하는 것 같은 시스템 수준 작업을 실행하는 데 추가 코어를 사용해요.

하지만 여러분의 Dart 코드 자체가 동시에 실행돼야 할 수도 있어요. 예를 들어 연속적인 애니메이션과 함께 대용량 JSON 파일 파싱 같은 오래 걸리는 작업이 있을 수 있죠. 추가 작업이 너무 오래 걸리면 UI에 스터터(stutter)나 랙(lag)이 생길 수 있어요. 그 추가 작업을 별도의 코어로 옮기면 애니메이션은 실행의 메인 스레드에서 끊김 없이 계속 실행될 수 있어요.

Dart의 동시성 모델은 아이솔레이트(isolates)에 기반해요. 아이솔레이트는 서로 격리된 독립적인 실행 단위로, 공유 메모리와 관련된 대규모 동시성 프로그래밍 버그 — data race 같은 경쟁 조건 — 를 막아줘요. Dart는 아이솔레이트 사이에서 가변(mutable) 객체를 공유하지 못하게 해서 이런 버그를 막고, 대신 아이솔레이트가 메시지 전달(message passing)로 상태를 교환하는 모델을 사용해요. Dart 2.15에서 우리는 아이솔레이트에 여러 가지 상당한 개선을 했어요.

먼저 아이솔레이트가 동작하는 방식을 재설계하고 재구현하면서, 아이솔레이트 그룹(isolate groups) 이라는 새 개념을 도입했어요. 아이솔레이트 그룹 안의 아이솔레이트들은 실행 중인 프로그램을 나타내는 여러 내부 데이터 구조를 공유해요. 이 덕분에 그룹 안의 개별 아이솔레이트가 훨씬 더 가벼워져요. 기존 아이솔레이트 그룹에서 추가 아이솔레이트를 시작하는 것은 이제 100배 이상 빨라졌어요. 프로그램 구조를 초기화할 필요가 없으니까요. 그리고 그렇게 생성된 아이솔레이트는 메모리를 10~100배 덜 소비해요.

아이솔레이트 그룹이 여전히 아이솔레이트 사이의 가변 객체 공유 접근을 막지만, 그룹은 공유 힙(shared heap)으로 구현돼서 더 많은 기능을 열어줘요. 한 아이솔레이트에서 다른 아이솔레이트로 객체를 전달할 수 있는데, 이는 큰 메모리 조각을 반환하는 작업을 수행하는 워커 아이솔레이트에 쓸 수 있어요. 예를 들어 데이터를 얻기 위해 네트워크 호출을 하고, 그 데이터를 큰 JSON 객체 그래프로 파싱한 다음, 그 JSON 그래프를 메인 아이솔레이트로 반환하는 워커 아이솔레이트를 들 수 있어요. Dart 2.15 이전에는 그 결과를 깊은 복사(deep-copy)해야 했는데, 복사가 프레임 예산보다 오래 걸리면 그 자체로 UI 지연(jank)을 일으킬 수 있었죠.

2.15에서는 워커 아이솔레이트가 Isolate.exit()를 호출하면서 결과를 인자로 넘길 수 있어요. 그러면 Dart 런타임이 결과를 담은 메모리를 복사 없이 워커 아이솔레이트에서 메인 아이솔레이트로 전달하고, 메인 아이솔레이트는 상수 시간(constant time)에 결과를 받을 수 있어요. 우리는 Flutter 2.8의 compute() 유틸리티 함수도 Isolate.exit()를 활용하도록 업데이트했어요. 이미 compute()를 쓰고 있다면 Flutter 2.8로 업그레이드한 뒤 자동으로 이런 성능 이점을 얻게 돼요.

마지막으로, 아이솔레이트 메시지 전달 메커니즘을 구현하는 방식을 재작업해서, 작고 중간 크기의 메시지를 전달하는 것이 약 8배 빨라졌어요. 보내는 것은 훨씬 빨라졌고, 받는 것은 거의 항상 상수 시간에 이뤄져요. 또 아이솔레이트가 서로 보낼 수 있는 객체의 종류도 확장해서 함수 타입, 클로저, 스택트레이스 객체 지원을 추가했어요. 자세한 내용은 SendPort.send() API 문서를 참고하세요.

아이솔레이트를 사용하는 방법을 더 배우고 싶다면, 2.15용으로 추가한 새 Concurrency in Dart 문서를 보세요. 살펴볼 수 있는 코드 샘플도 여러 개 준비되어 있어요.

새 언어 기능: 생성자 tear-off

Dart에서는 함수의 이름을 사용해서 다른 객체의 함수를 가리키는 함수 객체를 만들 수 있어요. 아래 예시에서 main() 메서드의 두 번째 줄이 gm.greet로 설정하면서 이 문법을 보여줘요.

class Greeter {
  final String name;
  Greeter(this.name);

  void greet(String who) {
    print('$name says: Hello $who!');
  }
}

void main() {
  final m = Greeter('Michael');
  final g = m.greet; // g holds a function pointer to m.greet.
  g('Leaf'); // Invokes and prints "Michael says: Hello Leaf!"
}

이런 함수 포인터 — 함수 tear-off라고도 불러요 — 는 Dart 핵심 라이브러리를 사용할 때 자주 나타나요. 여기 iterable에서 foreach()를 함수 포인터를 넘겨 호출하는 예시가 있어요.

final m = Greeter('Michael');

['Lasse', 'Bob', 'Erik'].forEach(m.greet);

// Prints "Michael says: Hello Lasse!", "Michael says: Hello Bob!",
// "Michael says: Hello Erik!"

역사적으로 우리는 생성자에서 tear-off를 만드는 것을 지원하지 않았어요(언어 이슈 #216). 많은 경우 — 예를 들어 Flutter UI를 만들 때 — 필요한 것은 생성자 tear-off이기 때문에 이건 좀 성가신 일이었죠. Dart 2.15부터 이 문법이 지원돼요. 아래는 .map()을 호출하면서 Text 생성자에 대한 tear-off를 넘겨, 세 개의 Text 위젯을 담은 Column 위젯을 만드는 예시예요.

class FruitWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
        children: ['Apple', 'Orange'].map(Text.new).toList());
  }
}

Text.newText 클래스의 기본 생성자를 가리켜요. 이름 있는 생성자도 가리킬 수 있어요 — 예를 들어 .map(Text.rich)처럼요.

관련 언어 변경

생성자 tear-off를 구현하면서, 우리는 기존 함수 포인터 지원의 몇 가지 불일치를 고칠 기회를 가졌어요. 이제 제네릭 메서드를 특수화해서 non-generic 메서드를 만들 수 있어요.

T id<T>(T value) => value;
var intId = id<int>; // New in 2.15.
int Function(int) intId = id; // Pre-2.15 workaround.

제네릭 함수 객체를 특수화해서 non-generic 함수 객체를 만들 수도 있어요.

const fo = id; // Tear off `id`, creating a function object.
const c1 = fo<int>; // New in 2.15; error before.

마지막으로, 제네릭이 포함된 타입 리터럴도 정리했어요.

var y = List; // Already supported.
var z = List<int>; // New in 2.15.
var z = typeOf<List<int>>(); // Pre-2.15 workaround.

dart:core 라이브러리의 개선된 enum

우리는 dart:core 라이브러리의 enum API에 여러 편의 추가를 했어요(언어 이슈 #1511). 이제 .name으로 각 enum 값의 String 값을 얻을 수 있어요.

enum MyEnum {
  one, two, three
}

void main() {
  print(MyEnum.one.name);  // Prints "one".
}

이름으로 enum 값을 찾을 수도 있어요.

print(MyEnum.values.byName('two') == MyEnum.two);  // Prints "true".

마지막으로, 모든 이름-값 쌍의 map을 얻을 수 있어요.

final map = MyEnum.values.asNameMap();
print(map['three'] == MyEnum.three);  // Prints "true".

이 새 API들을 사용하는 예시는 이 Flutter PR을 참고하세요.

압축 포인터(Compressed pointers)

Dart 2.15는 압축 포인터를 지원해요. 32비트 주소 공간만 지원하면 되는 경우(최대 4GB 메모리) 64비트 SDK가 더 공간 효율적인 포인터 표현을 사용할 수 있는 기법이에요. 압축 포인터는 상당한 메모리 절감을 가져와요. GPay 앱으로 진행한 우리 내부 테스트에서는 Dart 힙 크기가 약 10% 줄어드는 걸 봤어요.

압축 포인터는 4GB 위의 사용 가능한 RAM은 주소 지정할 수 없다는 뜻이기 때문에, 이 기능은 Dart SDK의 구성 옵션 뒤에 있어서 SDK가 빌드될 때 Dart SDK의 임베더(embedder)만 토글할 수 있어요. Flutter SDK 버전 2.8은 Android 빌드에서 이 구성을 켰고, Flutter 팀은 향후 릴리스에서 iOS 빌드에도 켜는 것을 검토하고 있어요.

Dart SDK에 포함된 Dart DevTools

디버깅·성능 도구 모음인 DevTools는 이전에는 Dart SDK에 들어 있지 않아서 따로 내려받아야 했어요. Dart 2.15부터는 Dart SDK를 내려받으면 DevTools도 함께 받고, 추가 설치 단계가 필요 없어요. Dart 명령줄 앱에서 DevTools를 사용하는 방법에 대한 자세한 내용은 DevTools 문서를 참고하세요.

패키지 게시자를 위한 새 pub 기능

Dart 2.15 SDK는 dart pub 개발자 명령과 pub.dev 패키지 저장소에 두 가지 새 기능이 있어요.

먼저, 패키지 게시자를 위한 새 보안 기능이 있어요. 목표는 게시자가 pub 패키지 안에 실수로 비밀 — 예를 들어 Cloud나 CI 자격 증명 — 을 게시하는 것을 탐지하는 거예요. 우리는 GitHub 저장소에서 매일 수천 개의 비밀이 유출된다는 것을 알게 된 후 이 유출 감지(leak detection)를 추가하게 됐어요.

유출 감지는 dart pub publish 명령의 게시 전 검증 단계의 일부로 실행돼요. 곧 게시될 파일에서 잠재적 비밀을 감지하면 publish 명령은 게시하지 않고 종료하며, 아래 같은 출력을 출력해요.

Publishing my_package 1.0.0 to https://pub.dartlang.org:
Package validation found the following errors:
* line 1, column 1 of lib/key.pem: Potential leak of Private Key detected.
╷
1 │ ┌ - - -BEGIN PRIVATE KEY - - -
2 │ │ H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
3 │ └ - - -END PRIVATE KEY - - -
╵
* line 2, column 23 of lib/my_package.dart: Potential leak of Google OAuth Refresh Token detected.
╷
2 │ final refreshToken = "1//042ys8uoFwZrkCgYIARAAGAQSNwF-L9IrXmFYE-sfKefSpoCnyqEcsHX97Y90KY-p8TPYPPnY2IPgRXdy0QeVw7URuF5u9oUeIF0";

드물게 이 감지가 오탐(false positive)을 내서, 실제로 게시하려는 내용이나 파일에 잠재적 유출로 표시할 수 있어요. 그런 경우에는 파일을 허용 목록(allowlist)에 추가하면 돼요.

둘째, 게시자가 이미 게시된 패키지 버전을 내릴(retract) 수 있게 해주는 또 다른 기능을 추가했어요. 잘못된 패키지 버전이 게시되면, 보통 원치 않는 문제를 고치는 마이너 버전 증가로 새 버전을 게시하길 권장해요. 드문 경우 — 예를 들어 아직 그런 수정이 없을 때, 또는 새 마이너 버전을 게시하려다 실수로 새 메이저 버전을 게시했을 때 — 새 패키지 내림(package retraction) 기능을 최후의 수단으로 사용할 수 있어요. 이 기능은 pub.dev의 Admin UI에서 사용할 수 있어요.

패키지 버전이 내려지면, pub 클라이언트는 pub get이나 pub upgrade에서 더 이상 그 버전으로 해석하지 않아요. 만약 어떤 개발자가 이미 내려진 버전으로 해석했다면(그래서 그 버전이 pubspec.lock 파일에 있다면), 다음에 pub을 실행할 때 경고를 보게 돼요.

$ dart pub get
Resolving dependencies…
mypkg 0.0.181-buggy (retracted, 0.0.182-fixed available)
Got dependencies!

양방향 유니코드 문자를 감지하는 보안 분석 (CVE-2021–22567)

최근 양방향 유니코드 문자와 관련된 일반적인 프로그래밍 언어 취약점이 발견됐어요(CVE-2021–42574). 이 취약점은 Unicode를 지원하는 대부분의 현대 프로그래밍 언어에 영향을 줘요. 다음 Dart 소스 코드가 그 문제를 보여줘요.

main() {
  final accessLevel = 'user';

  if (accessLevel == 'user . // Check if admin ') {
    print('You are a regular user.');
  } else {
    print('You are an admin.');
  }
}

아마 이 프로그램이 You are a regular user.를 출력할 거라고 생각하겠지만, 실제로는 You are an admin!을 출력할 수 있어요. 이 익스플로잇은 양방향 유니코드 문자를 포함한 문자열을 사용해서 가능해요. 이 문자들은 한 줄 안에서 텍스트의 방향을 왼쪽에서 오른쪽으로, 그리고 다시 오른쪽에서 왼쪽으로 바꿔요. 양방향 문자로 텍스트는 화면에서 실제 텍스트 내용과 아주 다르게 렌더링될 수 있어요. 이 예시를 이 GitHub 코드 gist에서 볼 수 있어요.

이 취약점에 대한 완화는 양방향 유니코드 문자를 감지하는 도구(에디터, 코드 리뷰 도구 등)를 사용해서, 개발자가 그 문자를 인지하고 의도적으로 사용을 받아들일 수 있게 하는 것을 포함해요. 위에 연결된 GitHub gist 파일 뷰어는 이런 문자를 드러내는 도구의 한 예시예요.

Dart 2.15는 추가 완화 하나를 소개해요(Dart 보안 권고 CVE-2021–22567). 이제 Dart 분석기가 양방향 유니코드 문자를 스캔해서 그것들의 사용에 플래그를 표시해요.

$ dart analyze
Analyzing cvetest...                   2.6s

info • bin/cvetest.dart:4:27 • The Unicode code point 'U+202E'
       changes the appearance of text from how it's interpreted
       by the compiler. Try removing the code point or using the
       Unicode escape sequence '\u202E'. •
       text_direction_code_point_in_literal

이 문자들을 Unicode 이스케이프 시퀀스로 교체해서 어떤 텍스트 에디터나 뷰어에서도 보이게 만드는 걸 권장해요. 또는 이 문자들을 정당하게 사용할 일이 있다면, 사용하는 앞의 줄에 오버라이드를 추가해서 경고를 끌 수 있어요.

// ignore: text_direction_code_point_in_literal

서드파티 pub 서버 사용 시 pub.dev 자격 증명 취약점 (CVE-2021–22568)

우리는 두 번째 pub.dev 관련 Dart 보안 권고도 게시해요: CVE-2021–22568. 이 권고는 사설 또는 회사 내부 패키지 서버 같은 서드파티 pub 패키지 서버에 패키지를 게시했을 수 있는 패키지 게시자를 대상으로 해요. 공개 pub.dev 저장소(표준 구성)에만 게시하는 개발자는 이 취약점의 영향을 받지 않아요.

서드파티 저장소에 게시했다면, 그 서드파티 저장소에서 인증을 위해 제시하는 OAuth2 임시(1시간) 액세스 토큰이 공개 pub.dev 저장소에 대한 인증에 오용될 수 있다는 것이 취약점이에요. 따라서 악의적인 서드파티 pub 서버가 그 액세스 토큰을 사용해 pub.dev에서 여러분을 사칭하고 패키지를 게시할 수 있어요. 신뢰할 수 없는 서드파티 패키지 저장소에 패키지를 게시했다면, pub.dev 공개 패키지 저장소에서 계정 활동 전체를 감사해 보는 것을 고려하세요. 이 목적에는 pub.dev 활동 로그를 사용할 수 있어요.

마무리

오늘 사용할 수 있는 Dart 2.15의 새 기능을 즐겁게 사용하시길 바라요. 이번 릴리스가 올해 마지막 릴리스인데, 이 기회에 훌륭한 Dart 생태계에 감사를 전하고 싶어요. 훌륭한 피드백과, 우리의 지속적인 성장으로 입증되는 여러분의 끊임없는 지원, 그리고 지난 1년 동안 pub.dev에 게시된 수천 개의 패키지로 생태계를 확장해 주신 것에 감사드려요. 내년에 다시 만날 날이 정말 기대돼요. 2022년을 위해 정말 많은 흥미로운 계획이 준비되어 있어요. 그때까지 즐거운 연휴 보내세요!

더 알아보기