Flutter 초보자의 클린 아키텍처 적용해보기
다음 내용은 클린 아키텍처를 처음 알고 적용해보고 싶은 초보자들을 위해 작성했습니다.
클린 아키텍처를 적용하려면 클린 아키텍처가 무엇인지를 알아야 하겠죠?
모바일 앱을 개발하다보면 가장 높은 추상화 단계로 다음과 같은 과정을 겪게됩니다.
- 서버(또는 로컬 DB)에서 data를 받아오고
- UI에 뿌릴 수 있게 가공하여
- UI가 유저에게 가공된 데이터를 보여주도록 한다.
처음에는 이 과정들이 낯설기 때문에 구현에 집중하지만 이를 반복하다 보면 위와 같은 1 ~ 3의 과정이 반복됨을 알게될 거예요. 이렇게 위 과정들에서 맡아야 할 책임을 명확히 분리시킨 구조를 설계한 것이 클린 아키텍처라고 할 수 있겠습니다.
클린 아키텍처 톺아보기
구조는 크게 UI, Domain, Data Layer로 역할이 나뉘게 되고 먼저 UI Layer는 다음과 같다.
UI Layer
UI Layer는 사용자에게 data를 보여주거나 서버에서 받아온 데이터를 화면에 보일 데이터로 가공하는 역할 두 가지로 나뉜다.
- UI element: 사용자와 상호작용(북마크를 누르면 북마크 버튼의 색깔이 바뀜 등)을 담당하고 서버에서 받은 가공된 data를 보여주는 역할
- ViewModel: 서버에서 넘어온 데이터를 화면에 보일 Data로 가공하고 UI에서 보이는 Data가 변경되면 이를 서버에 보내고 받아서 UI Element에 전달하는 역할. 예를 들어, 사용자가 북마크를 하면 사용자의 북마크 여부에 대한 데이터 변경을 알고 이를 서버에 보내고 북마크가 됐다는 데이터를 서버로 부터 받아 UI Element에게 전달하면 Element가 이를 감지해 북마크 버튼 색깔을 바꿈.
예를 들어, 제가 만드는 앱을 예시로 생각해보겠습니다. 저는 기숙사 식단을 알리는 서비스를 만들고 있는데, 다음 세 가지 과정을 거치게 됩니다.
- 기숙사 홈페이지에서 식단을 가져오고
- 이를 UI에 뿌릴 수 있는 데이터로 가공하여
- UI에 Binding
여기서 UI에 표시되는 식단을 UI State라고 하는데, 즉 화면에 표시해야 하는 데이터를 말합니다. 이는 유저와 상호작용에 따라 데이터가 변경될 수도 있는데 이를 관리하는 역할이 ViewModel이라고 보시면 되겠습니다.
다음 코드를 통해 더 쉽게 이해해보죠. 유저가 거주하는 기숙사를 선택함에 따라 화면에 표시될 기숙사 유형이 달라지게 되는데요.
따라서 책임은 다음과 같이 나눠집니다.
-
UI Element: 1) 유저가 거주하는 기숙사 텍스트 버튼을 누름으로 인해 (예를 들어 세종기숙사에서 행복기숙사로) UI에 표시될 텍스트의 형태가 바뀌게 되는 것 2) 유저가 텍스트 버튼을 누름으로 인해 상태 변경이 필요하다고 ViewModel에게 알리는 것
-
UI State 1) UI Element로부터 유저의 기숙사 유형이 변경되었다는 event를 처리하고 2) 변경된 기숙사 유형을 담은 UIState를 생성해 다시 UI Element에게 전달하는 것
이를 코드로 살펴보면 다음과 같습니다.
class DormitoryViewModel extends ChangeNotifier {
UserRepository _userRepository;
DormitoryViewModel(this._userRepository);
UserUiState _userUiState = const UserUiState();
UserUiState get userUiState => _userUiState;
void changeDormitory(DormitoryType dormitoryType) {
_userRepository.saveDormitory(dormitoryType);
userDormitoryType = _userRepository.readDormitory(dormitoryType);
_userUiState = _userUiState.copyWith(dormitoryType: userDormitoryType);
notifyListeners();
}
}
final dormsProvder =
ChangeNotifierProvider<DormitoryViewModel>((ref) => DormitoryViewModel());
//기숙사 변경 화면
class SelectDormitoryScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedDormitory = ref.watch(dormsProvider);
return Column(
children: [
Text(selectedDormitory.userUiState.dormitoryType == 세종 ? '세종' : '행복'),
TextButton(
title: '기숙사 변경'
onPressed: ( ){
selectedDormitory.changeDormitory('행복');
}
);
]
);
}
}
정리해보자면, Data Layer에서 받은 app data를 ViewModel에서 Ui에 뿌릴 데이터로(UiState) 가공해서 UI elements로 전달하고 app data가 변경될 event가 UI Element에서 발생하면 event를 ViewModel로 전달하고 ViewModel이 Repository에 전달해 app data 변경을 업데이트하는 일을 반복하게 되는 것입니다.
Data Layer
데이터 레이어는 Repository와 Data Source라는 객체로 이루어지고 주로 서버에 저장된 data들을 받아와 전달하거나 UI Layer에서 어떠한 이벤트로 인해 변경된 app data들을 전달 받아 서버에 업데이트하는 역할을 합니다. Repository는 UI Layer와 교신할 수 있는 진입점역할로 API를 호출하거나 DB에 쿼리를 요청합니다. 그리고 Data Source에서 받은 Response를 모델링하여 객체로 만들어 expose하는 역할을 합니다.
DataSource에서는 서버나 로컬로 부터 데이터를 가져오는 역할로 Repository로부터 받은 API호출 등을 실행하기 위해 실제 API 호출 구현체가 있는 Service객체의 메서드를 사용해 호출합니다.
Data Layer에 있는 객체들은 CRUD Call을 수행하는 함수를 노출시키는데 이는 반드시 비동기 함수여야 하고 만약 app data의 변경사항이 있으면 알려줄 수 있도록 하는 callback 함수를 노출 시켜야합니다.
class DormitoryMealRepository {
final HappyDormsMealRemoteDataSource _happyDormsMealRemoteDataSource;
final SejongDormsMealRemoteDataSource _sejongMealRemoteDataSource;
DormitoryMealRepository(
this._happyDormsMealRemoteDataSource, this._sejongMealRemoteDataSource);
Future<List<MealData>> fetchHappyMeals() async {
_happyDormsMealRemoteDataSource.fetchHappyMeals();
};
Future<List<MealData>> sejongHappyMeals() async {
_sejongDormsMealRemoteDataSource.fetchSejongMeals();
};
//Data Source
class HappyDormsMealRemoteDataSource {
final HappyMealService _happyMealService;
HappyDormsMealRemoteDataSource(this._happyMealService);
Future<Document> fetchHappyMeals() async {
return _happyMealService.fetchHappyMeals();
}
}
//Service
class HappyMealService {
// 실제 api 요청 구현체
Future<Meals>fetchHappyMeals() async { }
}
댓글남기기