Flutter에서 BLoC와 유스케이스 분리: 클린 아키텍처 실전 적용
Flutter의 BLoC 패턴은 종종 거대 객체(god object)로 변질됩니다. 이벤트 핸들러에서 서비스 호출, 검증, 상태 방출이 뒤섞여 유지보수, 테스트, 확장이 악몽이 됩니다. 비즈니스 로직을 유스케이스로 옮기면 클린 아키텍처 원칙에 따른 적절한 계층 분리가 가능합니다: 프레젠테이션(UI와 BLoC), 도메인(유스케이스, 엔티티), 데이터(리포지토리, 서비스).
유스케이스는 특정 시나리오를 구현합니다. 서비스를 조율하고, 오류를 처리하며, 비즈니스 규칙을 적용하고 UI와 무관하게 동작합니다. BLoC는 단방향 데이터 흐름에만 집중—이벤트 처리와 상태 방출입니다.
전통적인 BLoC: 로직과 UI가 얽히는 문제
전형적인 BLoC는 의존성 주입으로 서비스를 직접 호출하며 핸들러에서 부수 효과를 수행합니다. 간단한 ItemsBloc 예시:
class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
final FetchItemsService _service;
ItemsBloc(this._service) : super(Initial()) {
on<FetchItemsEvent>(_onFetchItemsEvent);
}
Future<void> _onFetchItemsEvent(FetchItemsEvent event, Emitter<ItemsState> emit) async {
emit(Loading());
try {
final items = await _service.fetchItems();
emit(Loaded(items: items));
} catch (error) {
emit(Error(error: error));
}
}
}
문제점: 강한 결합, 테스트 어려움, 단일 책임 원칙(SRP) 위반. BLoC가 데이터, 네트워킹, 비즈니스 로직을 너무 많이 알게 됩니다.
리팩토링: 순수 상태 관리자로서의 BLoC
리팩토링 후 BLoC는 이벤트만 상태로 매핑하며 의존성이 없습니다:
class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
ItemsBloc() : super(Initial()) {
on<SetLoading>(_onSetLoading);
on<SetLoaded>(_onSetLoaded);
on<SetError>(_onSetError);
}
Future<void> _onSetLoading(SetLoading event, Emitter<ItemsState> emit) async {
emit(Loading());
}
Future<void> _onSetLoaded(SetLoaded event, Emitter<ItemsState> emit) async {
emit(Loaded(items: event.items));
}
Future<void> _onSetError(SetError event, Emitter<ItemsState> emit) async {
emit(Error(error: event.error));
}
}
유스케이스가 조율을 담당합니다:
class FetchItemsUseCase {
final ItemsBloc bloc;
final FetchItemsService service;
FetchItemsUseCase({required this.bloc, required this.service});
Future<void> call() async {
bloc.add(SetLoading());
try {
final items = await service.fetchItems();
bloc.add(SetLoaded(items: items));
} catch (error) {
bloc.add(SetError(error: error.toString()));
}
}
}
BLoC가 20줄로 줄고 완전 테스트 가능: 모킹 이벤트와 상태 검증.
조율자로서 유스케이스의 이점
유스케이스는 단순 프록시가 아닙니다. 리포지토리 데이터를 결합하고 규칙 적용, 로깅, 동기화 처리합니다. 구체 구현이 아닌 추상화(인터페이스)에 의존합니다.
주요 장점:
- 테스트 용이성: 유스케이스 단위 테스트 시 서비스/리포지토리 모킹.
- 확장성: BLoC 건드리지 않고 새 시나리오 추가(개방/폐쇄 원칙).
- DI 유연성: 유스케이스에 서비스 주입; BLoC 독립 유지.
- 가독성: 비즈니스 시나리오가 전용 클래스에 격리.
실제 프로젝트에서 이 리팩토링으로 개발 시간 30% 단축, 버그 감소, 테스트 간소화.
데이터 흐름과 트레이드오프
UI → 유스케이스 → 서비스/리포지토리 → BLoC 이벤트 → UI 상태. 유스케이스가 BLoC를 알지만(단순화를 위한 클린 아키텍처 타협), UI는 bloc.add 호출에서 자유롭습니다.
BLoC는 단방향 데이터 흐름: 예측 가능하고 테스트 쉬움. 유스케이스는 독립 테스트: 응답 시뮬레이션과 호출 검증.
핵심 요약
- 계층 분리: BLoC는 상태 관리자, 유스케이스는 도메인 로직.
- SRP 실천: 각 클래스 하나의 책임.
- 간단한 테스트: BLoC 순수 함수, 유스케이스 모킹.
- 확장성: Cubit, Riverpod 등 다른 상태 관리자와 호환.
BLoC가 비대해지는 모든 Flutter 프로젝트에 적합. 전후 메트릭 측정: 클래스 크기, 테스트 커버리지, 기능 배포 시간.
— Editorial Team
아직 댓글이 없습니다.