[Flutter] Clean Architecture & DI 샘플앱 개발 후기

메모하는 개발자·2022년 10월 2일
5

Flutter메모

목록 보기
4/7

Clean Architecture와 Dependency Injection 개념적인 내용보다는 실제 샘플앱을 어떤식으로 개발했는지와 그 후기입니다.

샘플앱 Git

https://github.com/cw-hanna/flutter_clean_architecture_sample

샘플앱 Diagram

Flow

사용자가 Presentation Layer에 있는 ScreenWidget에 이벤트를 발생시키면 Bloc혹은 Provider에서 Buisiness Rule이 담긴 UseCase를 사용하여 로직을 수행한다.
이때 UseCase는 Repository 인터페이스를 주입받아 Api 개체가 주입된 Repository 구현체를 실행시킨다. (UseCase는 Repository 인터페이스만 호출할뿐 Repository구현체나 Api객체를 알 필요가 없다.)
호출 결과를 Bloc 혹은 Provider에서 notify하면서 ScreenWidget이 재 빌드되게 된다.

- Data layer

DataSource : API호출, DB접근, DTO

- Domain layer

Entities : 순수한 형태의 기본 model클래스, 꼭 model이 아니어도 불변의 룰이나 핵심적인 무언가(Buisiness Object)
UseCase : 사용목적에 맞는 repository 호출로 결과값을 다루는 (Buisiness Rule)

- Presentation layer

BLOC, Provider : 저장된 값에 따라 UI를 업데이트 시키는 BLOC, Provider, Widget

폴더구조

참고

config : 앱의 전반적 구성요소들.

  • theme
  • route

core : 앱에서 전역적으로 사용되는 요소들

  • params : API호출시 필요한 파라미터 모아둔 클래스
  • resources : API호출시 결과를 갖는 wrapper class. 통신 성공/실패 케이스를 쉽게 챙길수있다.
  • utils : 각종 util

data : data layer 관련 요소들

  • datasources : api호출 클래스
  • models : api결과 데이터 관련 클래스(DTO)
  • repositories : api 클래스를 주입받아 사용하는 레포지토리 구현체

domain : domain layer 관련 요소들

  • entities : POJO (data폴더의 model과 달리 데이터 파싱, 변환에 영향을 받지않는 근본적인 데이터 구조를 나타내는 클래스)
  • repositories : api호출 인터페이스.
  • usecases : repository를 주입받아서 하나의 액션 수행하는 클래스

presentation : presentation layer 관련 요소들

  • blocs : BLOC
  • widgets

Dependency Injection

get_it패키지 사용

get_it은 DI가 목적이 아니라 Service Locator패턴을 다트에 제공하는게 원래 목적이다.

Service Locator패턴이란

쉽게 말해서 Locator라는 컨테이너 안에 객체나 서비스들을 몽땅 저장한후, 필요할때마다 Locator에서 꺼내쓰는 패턴이다.

Service Locator패턴으로 어떻게 DI를?

주입될 객체들을 Locator컨테이너에 모두 등록하고 의존성이 필요할때마다 Locator에서 꺼내서 주입시켜주는 방식으로 DI를 할수있다.

get_it - Register Instance

getIt공식문서에 register, registerSingleton, registerLazySingleton, registerFactoryAsync, unregister등 여러 기능을 제공한다.

 //register 종류
 void registerFactory<T>(FactoryFunc<T> func) : call할때마다 new instance / 등록과 동시에 인스턴스 생성
 void registerFactoryParam<T,P1,P2>(FactoryFuncParam<T,P1,P2> factoryfunc, {String instanceName}); : registerFactory와 동일하지만 최대 2개의 파라미터를 던질수있다는 차이점. / 등록과 동시에 인스턴스 생성
 void registerSingleton<T>(T instance) : 싱글톤 객체 등록. call 할때마다 동일한 객체 반환. / 등록과 동시에 인스턴스 생성
 void registerLazySingleton<T>(FactoryFunc<T> func) : registerSingleton과 동일하지만 / 호출됐을때 인스턴스 생성.
 
 //인스턴스 등록시 비동기 동작 호출 하는 경우
 void registerFactoryAsync<T>(FactoryFuncAsync<T> func, {String instanceName});
void registerSingletonAsync<T>(FactoryFuncAsync<T> factoryfunc,
        {String instanceName,
        Iterable<Type> dependsOn,
        bool signalsReady = false});
 
//Unregister   
void unregister<T>({Object instance,String instanceName, void Function(T) disposingFunction}) : 등록된 싱글톤이나 팩토리를 등록해제, disposingFunction은 등록을 리셋하기전에 리소스를 해제할경우 사용할 수 있다.
 
///isRegistererd
bool isRegistered<T>({Object instance, String instanceName}); : 이미 등록되어있는지 체크
     
//Resetting LazySingletons
void resetLazySingleton<T>({Object instance, String instanceName,void Function(T) disposingFunction}) : LazySingleton을 등록해제하는것은 원치않지만 인스턴스를 초기화시켜서 다음 호출때 새로 생성
 
//Resetting GetIt completely
Future<void> reset({bool dispose = true}); : 모든 등록된 타입과 인스턴스를 초기화. 만약에 등록할때 dispose 메서드를 제공했다면 dispose = true 옵션을 주었을때 해당 dispose 메서드를 실행

사용예)

final serviceLocator = GetIt.instance;
void initServiceLocator() {
  //Register Api Instatnce
  serviceLocator.registerLazySingleton<CommitApi>(() => CommitApi());
  serviceLocator.registerLazySingleton<OrgApi>(() => OrgApi());
  serviceLocator.registerLazySingleton<SearchUserApi>(() => SearchUserApi());
...

  //Register RepositoryImpl instance
  serviceLocator.registerLazySingleton<CommitApiRepository>(
      () => CommitApiRepositoryImpl());

  serviceLocator
      .registerLazySingleton<OrgApiRepository>(() => OrgApiRepositoryImpl());

  serviceLocator.registerLazySingleton<SearchUserApiRepository>(
      () => SearchUserApiRepositoryImpl());

  //Register UseCase instance
  
  serviceLocator
      .registerLazySingleton<GetCommitsUseCase>(() => GetCommitsUseCase());
  serviceLocator.registerLazySingleton<GetOrgsUseCase>(() => GetOrgsUseCase());
  serviceLocator
      .registerLazySingleton<SearchUserUseCase>(() => SearchUserUseCase());
}

get_it - Registered Instance LifeCycle?

app start 시점에 lazy를 제외한 모든 인스턴스가 등록(생성)된다.
해당 인스턴스를 더이상 쓰지 않는시점에 수동으로 unregister로 해제필요.

get_it - Inject Instance

ex)

class GetOrgsUseCase {
  OrgApiRepository? repository;

//추후 mockito를 활용한 유닛테스트를 위해 주입할 객체인 repository를 optional parameter로 받게하고
repository가 null일경우 locator에서 객체를 주입받도록 함.
  GetOrgsUseCase({OrgApiRepository? repository})
      : repository = repository ?? serviceLocator<OrgApiRepository>();

  Future<Result<List<Org>>> call() async {
    final result = await repository!.fetch();

    return result.when(success: (orgs) {
      return Result.success(orgs);
    }, error: (message) {
      return Result.error(message);
    });
  }
}


class OrgApiRepositoryImpl implements OrgApiRepository {
  OrgApi? api;
  
  //추후 mockito를 활용한 유닛테스트를 위해 주입할 객체인 api optional parameter로 받게하고
api null일경우 locator에서 객체를 주입받도록 함.
  OrgApiRepositoryImpl({OrgApi? api}) : api = api ?? serviceLocator<OrgApi>();

  @override
  Future<Result<List<Org>>> fetch() async {
    final Result<Iterable> result = await api!.fetch();

    return result.when(
      success: (iterable) {
        return Result.success(iterable.map((e) {
          return OrgModel.fromJson(e);
        }).toList());
      },
      error: (message) {
        return Result.error(message);
      },
    );
  }
}

1개의 댓글

comment-user-thumbnail
2022년 10월 2일

플러터로 클린아키텍쳐 예제는 흔하지 않은데, sample 코드부터 폴더구조까지 설명이 디테일하네요!! 도움이 많이 될것같아요!

답글 달기