[Flutter] 병렬 처리와 스레드 이해 (Thread, Isolate, 레이스 컨디션)

2025. 5. 28. 09:00Dart & Flutter

Flutter와 Dart는 싱글 스레드 기반의 구조를 가지고 있지만, 복잡한 연산이나 UI 렌더링을 방해하지 않기 위해 병렬 처리가 필요할 때가 많아요. 이번 글에서는 병렬 처리의 핵심 개념인 스레드(Thread), 아이솔레이트(Isolate), 그리고 레이스 컨디션(Race Condition)에 대해 정리해볼게요.


📌 스레드(Thread)란?

앱을 개발하다 보면 종종 “스레드(Thread)”라는 단어를 접하게 됩니다. 특히 Flutter에서는 UI 프레임 드랍이나 앱 멈춤 현상을 피하기 위해 병렬 처리를 자주 사용하게 되는데요, 이를 이해하려면 우선 스레드가 무엇인지 제대로 알아야 해요.

 

먼저 스레드의 기본 개념부터 Flutter에서 어떻게 활용되는지까지 쉽게 설명해드릴게요!


1️⃣ 스레드(Thread)란 무엇인가요?

 

스레드는 “프로그램이 작업을 수행하는 실행 단위”예요.

 

컴퓨터는 여러 작업을 동시에 처리하는 것처럼 보이지만, 사실은 짧은 시간 동안 여러 작업을 번갈아가며 처리하고 있어요. 이때 각각의 실행 흐름을 “스레드”라고 부릅니다.

 

예를 들어, CPU는 하루 종일 여러 가지 일을 처리해야 하는 바쁜 사람이라고 생각해봐요.

  • 아침엔 이메일 확인
  • 점심엔 보고서 작성
  • 저녁엔 친구와 대화

이 모든 걸 한 명이 빠르게 왔다 갔다 하며 처리한다면 — 이게 바로 스레드를 번갈아 실행하는 거예요!


2️⃣ 프로세스 vs 스레드

구분  프로세스(Process)  스레드(Thread)
정의  실행 중인 프로그램 자체  프로세스 내에서 실행되는 작업 흐름
메모리  독립적  같은 프로세스 내에서 메모리 공유
생성 비용  높음  낮음
예시  Flutter 앱 하나  UI 처리, 백그라운드 연산 등
  • 하나의 프로세스는 여러 스레드를 가질 수 있어요.
  • 여러 스레드는 같은 메모리를 공유하기 때문에 빠른 속도로 통신할 수 있지만, 그만큼 충돌 위험도 있어요.

3️⃣ Dart와 Flutter는 왜 일반 스레드를 사용하지 않나요?

 

Dart는 스레드 대신 “Isolate”라는 구조를 사용해요.

 

이유는?

  • 일반적인 스레드는 메모리를 공유합니다 → 충돌(Race Condition), 동기화 문제 발생
  • Flutter는 UI 성능이 매우 중요해요 → 안정적이고 예측 가능한 구조가 필요해요
  • 그래서 Dart는 아예 “스레드를 분리된 메모리 공간”으로 대체한 Isolate 방식을 채택했어요!

✅ 정리하면:

Dart는 멀티스레드를 직접 다루지 않고, Isolate를 통해 멀티스레딩과 유사한 병렬 처리를 구현합니다.


4️⃣ Flutter 앱에서 실제 스레드는 어떻게 동작하나요?

 

Flutter 앱을 실행하면 최소한 다음 두 개의 스레드가 생성됩니다:

  • UI Thread (Main Isolate): 위젯 빌드, 화면 렌더링, 사용자 입력 처리 등
  • Platform Thread: 네이티브 코드와의 통신, 이미지 디코딩, I/O 작업 등

필요에 따라 다음과 같은 스레드도 생깁니다:

  • I/O Thread: 파일 읽기, HTTP 요청 등
  • Raster Thread: GPU와 연동되어 UI를 실제로 그림

Flutter에서는 이들 스레드를 자동으로 적절히 사용하지만, 복잡하거나 오래 걸리는 연산은 개발자가 Isolate, compute() 등을 통해 분리해줘야 해요!


5️⃣ 그럼 레이스 컨디션(Race Condition)은 왜 생기나요?

 

여러 스레드가 “동시에 같은 메모리”에 접근할 때, 실행 순서에 따라 결과가 달라지는 문제를 레이스 컨디션이라고 해요.

 

예시:

int counter = 0;

// 두 스레드가 동시에 아래 코드를 실행하면?
counter += 1;

 

→ 어떤 스레드가 먼저 counter 값을 읽느냐에 따라 최종 값이 1 또는 2가 될 수 있어요! 예측이 어렵고, 디버깅도 매우 어려운 문제입니다.

 

⚠️ 이 때문에 Dart는 아예 메모리를 공유하지 않는 Isolate 구조로 설계되어 있어요.


6️⃣ Flutter에서는 어떻게 병렬 처리를 하나요?

 

스레드를 직접 다루지는 않지만, Isolate, compute() 함수를 통해 멀티스레딩처럼 병렬 작업을 처리할 수 있어요.

  • compute() 함수: 간단한 백그라운드 작업 처리에 적합
  • Isolate.spawn(): 직접 Isolate를 생성하고 통신하고 싶을 때 사용
  • Future, async/await: 동기처럼 보이는 비동기 코드 작성 가능

이 부분에 대해 자세히 다룬 글입니다:

2025.05.27 - [Dart & Flutter] - [Flutter] 비동기 프로그래밍 핵심 개념 정리 (Observer, Pub/Sub, Event Loop)

 

[Flutter] 비동기 프로그래밍 핵심 개념 정리 (Observer, Pub/Sub, Event Loop)

Flutter를 포함한 대부분의 프레임워크는 비동기(Asynchronous) 프로그래밍을 기반으로 동작합니다. 특히 사용자 경험이 중요한 UI 앱에서는 ‘비동기 처리’가 필수예요. 오늘은 비동기 프로그래밍

wse46.tistory.com


✅ 마무리 요약

 

스레드(Thread) : 실행 흐름의 최소 단위, 같은 메모리 공유

Dart의 구조 : 스레드 대신 Isolate 사용 (독립된 메모리)

Flutter에서의 활용 : UI Thread, Platform Thread, I/O Thread 등 자동 운영

병렬 처리 방법 : compute(), Isolate, async/await

 

스레드를 이해하면 왜 Flutter에서 compute()Isolate를 쓰는지 명확하게 알 수 있어요. 다음에는 Isolatecompute()를 실제로 사용하는 방법, 그리고 Future와의 차이점도 함께 정리해볼게요!


📌 Isolate란? 

위에서 Flutter는 스레드(Thread) 대신 Isolate를 사용한다고 말씀드렸어요. 하지만 Isolate가 도대체 뭔지, 왜 compute()를 쓰면 빨라지는지 의문이 드셨을 거예요.

 

이번 글에서는 Flutter에서 병렬 처리를 책임지는 Isolate에 대해 하나씩 풀어볼게요. 개념부터 실제 사용 예시까지 쉽게 정리해드릴게요!


1️⃣ Isolate란?

 

Isolate는 Dart에서 사용하는 독립된 실행 단위예요.

 

간단히 말하면:

  • Dart의 ‘스레드(Thread)’ 같은 존재지만,
  • 기존 스레드와 다르게 “메모리를 공유하지 않는” 구조입니다.

“독립된 작은 프로그램”이라고 생각하면 이해가 쉬워요!


2️⃣ 왜 Dart는 Isolate를 사용하는 걸까요?

 

위에서 언급했던 “스레드 간 메모리 공유의 위험성” 기억나시죠?

  • 일반 스레드는 메모리를 공유해 빠르지만, 충돌(Race Condition), 데이터 오염 문제 발생
  • Dart는 안전하고 예측 가능한 구조를 위해 “아예 메모리를 공유하지 않겠다”는 선택을 했어요!

 

🔐 그래서 Isolate는:

  • 각자 독립된 메모리와 실행 환경을 가지고 있어요.
  • 서로 데이터를 주고받을 땐 반드시 “메시지”를 통해 통신해요 (SendPort / ReceivePort 사용)

이로 인해 Flutter는 UI 프레임 드랍 없이 부드러운 앱을 만들 수 있는 거죠!


3️⃣ Isolate의 구조 한눈에 보기

 

Isolate 간에는 공유 메모리가 없기 때문에 다음과 같은 방식으로 통신합니다:

 

Main Isolate (UI 처리)

         |

  SendPort 메시지 전송

       

Sub Isolate (백그라운드 연산)

       

  ReceivePort 메시지 수신

 

  • 하나의 Isolate는 하나의 스레드에서 실행됩니다.
  • 메인 Isolate는 UI를 담당하고, Sub Isolate는 오래 걸리는 연산이나 I/O를 처리할 수 있어요.

4️⃣ compute() 함수란?

 

Isolate를 쉽게 사용할 수 있도록 Flutter에서 제공하는 함수입니다.

 

특징:

  • 내부적으로 새로운 Isolate를 생성해서 연산을 수행해요.
  • 매우 간단한 API로 백그라운드 연산을 분리할 수 있어요.
  • 단점: 복잡한 통신이나 지속적인 처리에는 부적합

한 번 호출해서 결과를 반환받는 간단한 연산에 사용해요!

 

예시 코드:

import 'package:flutter/foundation.dart';

void main() async {
  final result = await compute(heavyTask, 10000000);
  print('결과: $result');
}

int heavyTask(int count) {
  int sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
}

 

  • 위 코드는 무거운 연산을 별도의 Isolate에서 처리하고, 결과만 메인 Isolate로 가져옵니다.
  • UI는 멈추지 않고 부드럽게 유지돼요!

5️⃣ 직접 Isolate 생성해보기

 

복잡한 통신이나 장기 실행 작업이 필요할 땐 직접 Isolate를 생성해야 해요.

 

기본 예시:

import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();

  // 새로운 Isolate 생성
  await Isolate.spawn(worker, receivePort.sendPort);

  // 결과 수신
  receivePort.listen((message) {
    print('메인에서 받은 메시지: $message');
  });
}

void worker(SendPort sendPort) {
  sendPort.send('백그라운드에서 보낸 인사: 안녕하세요!');
}

 

핵심 개념:

  • ReceivePort: 메인에서 메시지를 받을 통로
  • SendPort: 다른 Isolate에게 메시지를 보낼 통로
  • Isolate.spawn(): 새로운 Isolate 생성

→ 메시지를 통해 통신하는 방식이라 구조가 명확하고 충돌 위험이 없습니다!


6️⃣ Isolate는 언제 쓰면 좋을까?

 

짧은 연산 (ex. 숫자 계산)  compute() 사용

장기 연산 / 지속 통신 필요  Isolate.spawn() 직접 사용

단순한 비동기 작업 (ex. HTTP, DB)  Future + async/await 사용


✅ 정리 요약

 

Isolate: 독립된 실행 단위 (메모리 공유 X)

compute(): 간단한 연산용 Isolate 추상화 함수

직접 생성: SendPort / ReceivePort로 메시지 주고받기

장점: 안정적, UI 멈춤 방지, 레이스 컨디션 없음

단점: 데이터 공유 불가, 구조가 복잡할 수 있음

 

✅ 핵심 내용

  • Isolate는 Flutter 앱을 “끊김 없이 부드럽게” 만드는 데 핵심 역할을 합니다.
  • 복잡한 작업은 꼭 compute()나 Isolate로 분리하세요!

📌 레이스 컨디션(Race Condition)이란?

Flutter에서 스레드 대신 Isolate를 사용하는 이유는 “안전한 병렬 처리” 때문이라고 말씀드렸어요. 그 중심엔 바로 “레이스 컨디션”이라는 문제가 있습니다.

 

이번에는 레이스 컨디션이 무엇인지, 왜 위험한지, 그리고 Flutter에서는 왜 안전한지를 쉽게 설명해보도록 하겠습니다.


1️⃣ 레이스 컨디션(Race Condition)이란?

 

레이스 컨디션이란, 여러 실행 단위(스레드 등)가 “공유 자원”에 동시에 접근하면서, “예상하지 못한 결과”를 초래하는 상황을 말해요.

 

쉽게 말해서 둘 다 동시에 접근해서 값이 꼬여버리는 현상이에요!

 

예시 상황:

  • 두 스레드가 동시에 어떤 변수의 값을 수정하려고 함
  • 실행 순서에 따라 결과가 달라짐
  • 실행할 때마다 값이 바뀌거나, 엉뚱한 값이 저장됨

2️⃣ 실제 예시로 이해하기

 

아래는 Dart에서 가상의 스레드 개념으로 만든 예시예요:

int counter = 0;

void increment() {
  for (int i = 0; i < 100000; i++) {
    counter++; // 여러 스레드가 동시에 이 부분 실행
  }
}

 

이 함수를 동시에 여러 번 실행하면 어떤 결과가 나올까요?

 

기대한 값보다 작거나 이상한 값이 나올 수 있어요!

 

왜냐하면,

  1. 스레드 A가 counter 값을 읽음 (예: 0)
  2. 스레드 B도 동시에 counter 값을 읽음 (0)
  3. 둘 다 1을 더하고 저장 (둘 다 1을 저장해버림)
  4. 실제론 2번 더했지만, 결과는 1

이게 바로 Race Condition 입니다.


3️⃣ 왜 위험한가요?

 

예측 불가: 실행할 때마다 결과가 달라질 수 있어요

디버깅 어려움: 문제 상황을 재현하기 어렵고, 찾기도 힘들어요

데이터 오염: 중요한 정보(로그인 상태, 금액 등)가 잘못 저장될 수 있어요

앱 다운/에러 발생: 동기화가 안 돼 앱이 멈추거나 크래시가 날 수도 있어요


4️⃣ Flutter는 어떻게 해결할까?

 

Dart는 기본적으로 “Isolate” 기반 구조를 사용하므로 공유 메모리를 아예 없앴습니다!

  • Isolate는 메모리를 공유하지 않고, 오직 메시지로만 데이터를 주고받아요
  • 따라서 Race Condition이 발생할 가능성이 매우 낮아요!

compute() 함수나 Isolate.spawn()으로 작업을 분리하면, 각 실행 단위는 완전히 독립적이라 충돌이 없습니다.


5️⃣ 만약 Future/async를 쓴다면 괜찮은가요?

 

Future, async/await는 단일 Isolate 내에서 순차적으로 실행되기 때문에 Race Condition 위험이 적어요.

 

하지만 아래처럼 비동기 작업이 “같은 데이터”를 수정하면 문제가 생길 수 있어요:

int count = 0;

void increase() async {
  final prev = count;
  await Future.delayed(Duration(milliseconds: 1));
  count = prev + 1;
}

 

위 코드를 동시에 여러 번 실행하면:

count 값이 꼬일 수 있어요! await 중간에 다른 작업이 끼어들 수 있기 때문이죠.

 

→ 이런 상황에선 “동기화”가 필요합니다.


6️⃣ 동기화(Synchronization)란?

 

동기화는 여러 실행 단위가 “공유 자원”에 접근할 때 순서를 제어하는 방식입니다.

 

Dart에서는 다음과 같은 방식으로 동기화를 구현할 수 있어요:

 

✅ 1. Mutex (패키지 사용)

final mutex = Mutex();

Future<void> safeIncrement() async {
  await mutex.protect(() async {
    count++;
  });
}

 

✅ 2. Synchronized 패키지

final lock = Lock();

Future<void> safeIncrement() {
  return lock.synchronized(() {
    count++;
  });
}

 

이런 방식으로 “한 번에 하나만 접근 가능하게” 막을 수 있어요!


7️⃣ 정리 요약

 

레이스 컨디션: 여러 실행 단위가 동시에 자원에 접근해 꼬이는 현상

발생 조건: 공유 자원 + 동시 접근 + 순서 제어 실패

Flutter/Dart 대응: 메모리 공유 없는 Isolate 구조로 설계

async/await 주의점: await 사이에 다른 작업 끼어들 수 있음

해결 방법: 동기화 (mutex, lock 등 사용)

 

 

  • Race Condition은 병렬 처리에서 자주 발생하는 치명적인 문제예요.
  • Flutter에서는 Isolate 구조 덕분에 기본적으로 안전하지만,
  • Future나 비동기 코드에서도 데이터를 동시에 다룬다면 항상 조심해야 해요!

📌 정리

개념  설명  Flutter에서의 활용
Thread (스레드)  OS 수준의 실행 단위. 메모리 공유 가능  Dart에서 직접 사용하지 않음 (Flutter는 사용 지양)
Isolate (아이솔레이트)  완전히 분리된 실행 환경. 메모리 공유 없음  compute(), ReceivePort/SendPort로 병렬 처리 구현
Race Condition  동시에 공유 자원에 접근할 때 발생하는 충돌  Isolate는 메모리를 공유하지 않기 때문에 발생하지 않음

 

✅ 마무리

  • Flutter는 UI 성능을 최우선으로 하기에, 무거운 연산이나 I/O 작업은 반드시 분리해서 처리해야 해요.
  • Dart의 Isolate는 안전하고 효율적인 병렬 처리를 위한 핵심 도구입니다.
  • compute()를 사용하면 간편하게 백그라운드 연산을 처리할 수 있어요.
  • 병렬 처리와 Race Condition 개념을 이해하면, 앱이 버벅이지 않고 매끄럽게 작동하는 구조를 만들 수 있어요!