Singleton Design Parttern
🤔 Flutter에서 정말 Thread Safety가 필요할까?
많은 개발자들이 "Flutter는 Single Thread 아닌가?"라고 생각하지만, 실제로는 여러 상황에서 동시성 문제가 발생할 수 있습니다.
📍 동시성 문제가 발생하는 실제 상황들
1. async/await를 사용한 비동기 초기화
가장 흔한 상황입니다. 데이터베이스나 API 클라이언트를 초기화할 때:
dart
class ApiClient {
static ApiClient? _instance;
static Future<ApiClient> getInstance() async {
if (_instance == null) {
// 네트워크 호출이나 파일 I/O 등 시간이 걸리는 작업
await Future.delayed(Duration(seconds: 1));
_instance = ApiClient._internal();
}
return _instance!;
}
}
// 문제 상황: 앱 시작 시 여러 화면에서 동시 호출
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
UserProfile(), // ApiClient.getInstance() 호출
PostList(), // ApiClient.getInstance() 호출 (동시에!)
CommentList(), // ApiClient.getInstance() 호출 (동시에!)
],
),
),
);
}
}
결과: 3개의 서로 다른 ApiClient 인스턴스가 생성될 수 있음!
2. Isolate를 사용하는 경우
dart
// Background에서 데이터 처리
void backgroundTask(String data) {
Database.getInstance(); // Background Isolate에서 호출
}
void main() async {
Database.getInstance(); // Main Isolate에서 호출
compute(backgroundTask, "data"); // Background Isolate 시작
}
3. 앱 라이프사이클 이벤트 처리
dart
class MyApp extends StatefulWidget {
@override
void initState() {
super.initState();
// 여러 라이프사이클 이벤트에서 동시 호출 가능
WidgetsBinding.instance.addPostFrameCallback((_) {
Database.getInstance();
});
AppStateManager.getInstance();
}
}
⚡ 실제 테스트로 문제 확인하기
dart
void demonstrateRaceCondition() async {
print('=== Race Condition 테스트 ===');
// 동시에 10개의 Future 실행
List<Future<UnsafeDatabase>> futures = List.generate(
10,
(index) => UnsafeDatabase.getInstance()
);
List<UnsafeDatabase> instances = await Future.wait(futures);
// 결과 확인
Set<int> hashCodes = instances.map((e) => e.hashCode).toSet();
print('생성된 인스턴스 수: ${hashCodes.length}'); // 1개여야 하지만 여러 개 나올 수 있음
print('해시 코드들: $hashCodes');
}
🎯 상황별 해결책
1. Completer를 사용한 해결 (권장)
dart
class SafeDatabase {
static SafeDatabase? _instance;
static Completer<SafeDatabase>? _completer;
SafeDatabase._internal();
static Future<SafeDatabase> getInstance() async {
// 이미 생성된 경우
if (_instance != null) {
return _instance!;
}
// 이미 생성 중인 경우 - 기다림
if (_completer != null) {
return _completer!.future;
}
// 새로 생성 시작
_completer = Completer<SafeDatabase>();
try {
// 실제 초기화 작업 (DB 연결, API 설정 등)
await _performAsyncInitialization();
_instance = SafeDatabase._internal();
_completer!.complete(_instance!);
} catch (error) {
_completer!.completeError(error);
_completer = null; // 실패 시 재시도 가능하도록
rethrow;
}
return _instance!;
}
static Future<void> _performAsyncInitialization() async {
await Future.delayed(Duration(milliseconds: 500)); // 초기화 시뮬레이션
}
}
2. 동기적 생성 + 비동기 초기화 (심플)
dart
class SimpleDatabase {
static final SimpleDatabase _instance = SimpleDatabase._internal();
bool _isInitialized = false;
SimpleDatabase._internal();
static SimpleDatabase get instance => _instance;
Future<void> initialize() async {
if (_isInitialized) return;
await Future.delayed(Duration(milliseconds: 500));
_isInitialized = true;
}
}
// 사용법
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SimpleDatabase.instance.initialize(); // 앱 시작 시 미리 초기화
runApp(MyApp());
}
3. GetIt 패키지 사용 (최고 권장)
yaml
dependencies:
get_it: ^7.6.4
dart
import 'package:get_it/get_it.dart';
final GetIt locator = GetIt.instance;
void setupServiceLocator() {
// 비동기 싱글톤 등록
locator.registerSingletonAsync<Database>(
() async {
final db = Database._internal();
await db.initialize();
return db;
},
);
// 동기적 싱글톤 등록
locator.registerSingleton<ApiClient>(ApiClient._internal());
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
setupServiceLocator();
// 비동기 서비스들이 준비될 때까지 대기
await locator.allReady();
runApp(MyApp());
}
// 사용법
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final database = locator<Database>();
final apiClient = locator<ApiClient>();
return Container(
// UI 구현
);
}
}
4. Provider와 함께 사용
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final database = await SafeDatabase.getInstance();
runApp(
Provider<SafeDatabase>.value(
value: database,
child: MyApp(),
),
);
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final database = Provider.of<SafeDatabase>(context, listen: false);
return Container(
// database 사용
);
}
}
📊 성능 비교와 권장사항
🎯 결론 및 Best Practice
✅ 권장사항
간단한 싱글톤: 동기적 생성 + 별도 초기화 메서드
복잡한 의존성: GetIt 패키지 사용
앱 시작 시: 모든 싱글톤을 미리 초기화
테스트: 항상 동시성 테스트 코드 작성
❌ 피해야 할 것
async/await 없이 비동기 싱글톤 구현
앱 실행 중 동적 싱글톤 생성
Isolate간 싱글톤 공유 시도
💡 핵심 포인트
Flutter는 Single Thread이지만, async/await와 Future를 사용하면 동시성 문제가 발생할 수 있습니다. 특히 앱 시작 시 여러 위젯에서 동시에 싱글톤을 요청할 때 Race Condition이 발생하기 쉽습니다.
Last updated