iBetter Books
수정

Ch 04. 성능 프로파일링과 최적화

"최적화는 측정 후에 한다." 프로파일링 없는 최적화는 시간 낭비입니다. 어디가 느린지, 메모리를 얼마나 쓰는지 먼저 측정하고 그 결과를 보고 개선합니다.

Dart는 Dart DevTools라는 공식 프로파일링 도구를 제공합니다. CPU 프로파일러, 메모리 프로파일러, 타임라인 뷰가 포함되어 있습니다.

Dart DevTools 시작

dart pub global activate devtoolsdart devtools

또는 dart run--observe 플래그를 붙이면 Observatory가 실행됩니다.

dart run --observe bin/server.dart

출력된 URL을 브라우저에서 열면 DevTools 대시보드가 표시됩니다.

Observatory listening on http://127.0.0.1:8181/token.../

CPU 프로파일러

CPU 프로파일러는 어떤 함수가 실행 시간을 많이 차지하는지 보여줍니다.

다음 예제 코드로 프로파일링을 실습합니다.

// 새 파일: bin/profile_example.dart
import 'dart:math';

void main() {
  // CPU를 집중적으로 사용하는 코드
  final result = computeExpensive();
  print('결과: $result');
}

double computeExpensive() {
  var sum = 0.0;
  for (var i = 0; i < 10000000; i++) {
    sum += sqrt(i.toDouble());
  }
  return sum;
}
dart run --observe bin/profile_example.dart

DevTools → CPU Profiler → Record → 앱 실행 → Stop 순서로 CPU 사용량을 기록합니다. "Bottom Up" 뷰에서 가장 많은 시간을 차지하는 함수를 확인할 수 있습니다.

메모리 프로파일러

메모리 누수가 의심될 때 메모리 프로파일러를 사용합니다.

// 새 파일: bin/memory_example.dart

class DataCache {
  static final Map<String, List<int>> _cache = {};

  // 메모리 누수 예: 캐시가 무한히 증가
  static void store(String key, List<int> data) {
    _cache[key] = data;
  }

  // 올바른 구현: 크기 제한 추가
  static void storeWithLimit(String key, List<int> data, {int maxSize = 100}) {
    if (_cache.length >= maxSize) {
      _cache.remove(_cache.keys.first);
    }
    _cache[key] = data;
  }
}

void main() async {
  // 메모리 누수 시뮬레이션
  for (var i = 0; i < 1000; i++) {
    DataCache.store('key_$i', List.generate(10000, (j) => j));
    await Future.delayed(Duration.zero);
  }
  print('완료');
}

DevTools → Memory → 힙(Heap) 스냅샷 비교로 어떤 객체가 계속 누적되는지 확인합니다.

Isolate로 CPU 집약적 작업 처리

Dart는 단일 스레드이지만 Isolate로 병렬 처리가 가능합니다. Isolate는 별도 메모리 공간을 가진 독립적인 실행 단위입니다. 메모리를 공유하지 않으므로 경쟁 조건(race condition)이 없습니다.

// 새 파일: bin/isolate_example.dart
import 'dart:isolate';

// Isolate에서 실행할 함수 (최상위 함수 또는 static 메서드여야 함)
Future<int> computeInIsolate(int n) {
  return Isolate.run(() => _heavyComputation(n));
}

int _heavyComputation(int n) {
  var result = 0;
  for (var i = 0; i < n; i++) {
    result += i;
  }
  return result;
}

void main() async {
  print('메인 Isolate에서 시작');

  // Isolate.run으로 간단하게 비동기 실행
  final futures = List.generate(4, (i) => computeInIsolate(10000000 * (i + 1)));
  final results = await Future.wait(futures);

  for (var i = 0; i < results.length; i++) {
    print('작업 $i 결과: ${results[i]}');
  }

  print('완료');
}

Isolate.run()은 Dart 2.19+에서 사용할 수 있는 간편한 API입니다. 함수를 별도 Isolate에서 실행하고 결과를 반환합니다.

todo_server에서 이미지 처리나 복잡한 암호화 연산 같은 무거운 작업이 있다면 Isolate로 분리하면 메인 이벤트 루프가 블로킹되지 않아 다른 요청 처리가 끊기지 않습니다.

서버 성능 측정

서버의 응답 시간과 처리량을 측정합니다.

# Apache Bench (ab) 사용ab -n 1000 -c 50 http://localhost:8080/todos# wrk 사용 (더 정확한 HTTP 벤치마크)wrk -t12 -c400 -d30s http://localhost:8080/todos

결과 예시입니다.

Running 30s test @ http://localhost:8080/todos
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.23ms    1.12ms  45.67ms   89.45%
    Req/Sec     6.42k   512.00    8.12k    71.23%
  2308145 requests in 30.04s, 1.02GB read
Requests/sec:  76,840.20

Requests/sec가 낮거나 Latency가 높다면 병목 지점을 찾아야 합니다.

일반적인 Dart 성능 최적화 패턴

String 연결 최적화

// 느림: 매번 새로운 String 객체 생성
String result = '';
for (var i = 0; i < 10000; i++) {
  result += 'item_$i,';
}

// 빠름: StringBuffer 사용
final buffer = StringBuffer();
for (var i = 0; i < 10000; i++) {
  buffer.write('item_$i,');
}
final result = buffer.toString();

컬렉션 연산 최적화

// 불필요한 중간 List 생성
final result = items
    .toList()           // 불필요
    .where((e) => e > 0)
    .toList()           // 불필요
    .map((e) => e * 2)
    .toList();          // 최종에만 필요

// 더 효율적
final result = items
    .where((e) => e > 0)
    .map((e) => e * 2)
    .toList();          // 최종에만 한 번

const 활용

// 매번 새로운 RegExp 객체 생성
bool isEmail(String s) {
  return RegExp(r'^[\w-\.]+@[\w-]+\.[\w]+$').hasMatch(s);
}

// static final로 한 번만 생성
class Validator {
  static final _emailRegex = RegExp(r'^[\w-\.]+@[\w-]+\.[\w]+$');

  static bool isEmail(String s) => _emailRegex.hasMatch(s);
}

Timeline 뷰로 이벤트 흐름 분석

DevTools → Timeline에서 이벤트의 시간 흐름을 시각적으로 분석합니다. 어떤 Future가 언제 시작하고 끝났는지, 이벤트 루프가 블로킹된 시점이 있는지 확인할 수 있습니다.

Timeline.startSyncTimeline.finishSync로 커스텀 이벤트를 추가할 수도 있습니다.

import 'dart:developer';

void processItems(List items) {
  Timeline.startSync('processItems', arguments: {'count': items.length});
  try {
    // 처리 로직
  } finally {
    Timeline.finishSync();
  }
}

이번 챕터 정리

  • Dart DevTools의 CPU 프로파일러로 느린 함수를 찾았습니다.
  • 메모리 프로파일러로 메모리 누수를 탐지하는 방법을 배웠습니다.
  • Isolate.run()으로 CPU 집약적 작업을 별도 Isolate에서 실행해 메인 이벤트 루프를 보호했습니다.
  • StringBuffer, static final 정규식, 불필요한 toList() 제거 등 실용적인 최적화 패턴을 익혔습니다.

PART 09가 완성되었습니다. 다음이자 마지막 PART에서는 네 개의 프로젝트를 돌아보고 앞으로의 학습 방향을 제시합니다.