멀티 모듈을 활용한
플러터 클린아키텍처
임태규
• 정보관리 기술사
• 11년차 모바일 엔지니어
(안드로이드, 플러터)
• 전) 삼성전자
• 전) 쿠팡
• 현) Presto Labs
• https://github.com/dualcoder-pe
• https://www.linkedin.com/in/taekyu-lim-b3b629187
Presto Labs
• 싱가폴 기반의 시스템 트레이딩 회사
• https://aqx.com
• Android Engineer 채용 (링크)
목차
클린 아키텍처
플러그인 아키텍처
경계선 긋기
1
2
3
클린 아키텍처
Part 1
클린 아키텍처?
• 깔끔한 아키텍처?
• 유일하게 훌륭한 아키텍처?
클린 아키텍처?
• Robert C Martin이 제안한 아키텍처
• 50년 넘게 개발해보니, 이렇게 하면 좋더라.
è Best Practice
클린 아키텍처!
• 50년 전의 프로그래머를 오늘날 PC에 앉혀둔다면, 24시간 이내에
코드를 작성할 수 있다.
• 프로그램의 구성요소를 정렬하고 조립하는 방법에 대한 규칙은
보편적이며 시간이 흐름에 따라 변하지 않았다.
è보편적 규칙이 존재함
è클린 아키텍처를 통해 이 규칙을 학습
왜 클린 아키텍처인가?
• 제대로 된 소프트웨어를 만들면, 소수의 인력으로 프로그램을
지속시킬 수 있다.
è나는 천국에 가 보았다.
왜 클린 아키텍처인가?
• 소프트웨어 구성요소의 관계를 표현하는 구조
소프트웨어 아키텍처란?
• 필요한 시스템을 만들고 유지 보수하는데 투입되는 인력을
최소화 하는 아키텍처
좋은 아키텍처는?
• 유스케이스 (ex. 상품 관리, 장바구니)
• 운영 (ex. 초당 100,000의 고객 처리)
• 개발 (ex. 팀별 독립적으로 개발 가능한 컴포넌트)
• 배포 (ex. 빌드 후 즉시 배포)
좋은 아키텍처가 지원해야 하는 것
• 인력 최소화? 개발 효율화!
è 개발 비용을 높이는 요소를 제거
좋은 아키텍처를 만들기 위해서는?
• 지속적인 변경 요청
• 소프트웨어 구조의 복잡도는 시간이 지날수록 증가
• 복잡도가 증가할 수록 변경 비용은 증가
개발 비용을 높이는 요소
• 변경에 잘 대응하기
좋은 아키텍처를 위한 목표
• 시간 관점 대응
• 늦게 결정해도 되는 요소를 뒤로 미룸
• 범위 관점 대응
• 변경 시 영향범위 최소화
변경에 잘 대응하기
• 시간 관점 대응
• 늦게 결정해도 되는 요소를 뒤로 미룸
à 플러그인 아키텍처
• 범위 관점 대응
• 변경의 영향범위 최소화
à 경계선 긋기
변경에 잘 대응하기
플러그인 아키텍처
Part 2
• 추상적인 것 (일찍 결정해야 함)
• 비즈니스 규칙 (what)
• ex. 저장 à 주문 내역 저장
• ex. 입출력 à 주문 양을 입력
• 구체적인 것 (늦게 결정해도 됨)
• 도구 (how)
• ex. 데이터베이스 à mysql
• ex. 화면 à TextField를 활용해 주문양을 입력
è 플러그인 아키텍처를 통해 늦게 결정해도 되는 요소를 미룸
일찍 결정해야 하는 것과 아닌 것
클린 아키텍처
Data
Domain
UI
클린 아키텍처에 대한 잘못된 해석
레이어 아키텍처
클린 아키텍처의 레이어 분리
(구체적)
(추상적)
도메인
인프라
Presentation
Data
Device
인프라
클린 아키텍처의 모듈 배치
도메인
플러그인 아키텍처
Data
UI Device
도메인
Dart (Flutter 의존성 X)
도메인 영역 의존성
Data
UI Device
도메인
Flutter
Mobile
Remote
Android
Web
Local
iOS
인프라 영역 의존성
Data
UI Device
도메인
인프라
Mobile
Remote
Android
Web
Local
iOS
테스트 커버리지
Data
UI Device
도메인
인프라
Test Coverage 100%
경계선 긋기
Part 3
Mobile
Remote
Android
Web
Local
iOS
아키텍처 경계
Data
UI Device
경계
• 경계는 소프트웨어 요소를 서로 분리
• 경계 반대편의 요소를 알 수 없음
è한 쪽에서 일어나는 변경이 경계 밖으로 영향을 주지 않음
경계의 의미
• 소스 수준 분리: 모든 컴포넌트가 동일한 주소 공간에서 실행.
함수 호출로 통신. (모놀리틱)
• 바이너리 수준 분리: jar, dll, shared library. 동일한 주소 공간에서
실행 + 함수 호출로 통신. 다른 프로세스에서 실행 +
IPC(Socket/Shared Memory). 독립적으로 배포 가능
• 실행 수준 분리: 데이터 구조에만 의존. 네트워크로 통신.
(마이크로 서비스)
경계선 긋기
• 소스 수준 분리: 모든 컴포넌트가 동일한 주소 공간에서 실행.
함수 호출로 통신. (모놀리틱)
• 바이너리 수준 분리: jar, dll, shared library. 동일한 주소 공간에서
실행 + 함수 호출로 통신. 다른 프로세스에서 실행 +
IPC(Socket/Shared Memory). 독립적으로 배포 가능
• 실행 수준 분리: 데이터 구조에만 의존. 네트워크로 통신.
(마이크로 서비스)
경계선 긋기
클린 아키텍처 선 긋기
import '../data/model/order.dart';
import '../data/model/order_result.dart';
import '../data/repository/order_repository.dart';
class SendOrderUsecase {
final OrderRepository _orderRepository;
SendOrderUsecase(this._orderRepository);
Future<OrderResult> sendOrder(Order order) =>
_orderRepository.sendOrder(order);
}
import '../../data/model/order.dart';
import '../../data/model/order_result.dart';
import '../../data/model/product.dart';
import '../../usecase/send_order_usecase.dart';
class OrderBloc {
final SendOrderUsecase _sendOrderUsecase;
OrderBloc(this._sendOrderUsecase);
Future<OrderResult> send(
String orderId, String productId, String name, String
price) {
final product = Product(productId, name, price);
final order = Order(orderId, product);
return _sendOrderUsecase.sendOrder(order);
}
}
import '../model/order.dart';
import '../model/order_result.dart';
abstract class OrderRepository {
Future<OrderResult> sendOrder(Order order);
}
import 'package:flutter/material.dart';
import '../../../domain/presentation/bloc/order_bloc.dart';
class OrderView extends StatefulWidget {
final OrderBloc orderBloc;
const OrderView({Key? key, required this.orderBloc}) :
super(key: key);
@override
State<StatefulWidget> createState() => OrderViewState();
}
import '../../../domain/data/model/order.dart';
import '../../../domain/data/model/order_result.dart';
import '../../../domain/data/repository/order_repository.dart';
import '../datasource/local_datasource.dart';
import '../datasource/remote_datasource.dart';
class OrderRepositoryImpl implements OrderRepository {
final LocalDatasource _localDatasource;
final RemoteDatesource _remoteDatesource;
OrderRepositoryImpl(this._localDatasource,
this._remoteDatesource);
@override
Future<OrderResult> sendOrder(Order order) async {
final localRes = await _localDatasource.sendOrder(order);
final remoteRes = await _remoteDatesource.sendOrder(order);
return OrderResult((localRes && remoteRes) ? "SUCCESS" : "Fail");
}
}
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import '../../../domain/data/model/order.dart';
class LocalDatasource {
final dbName = "";
Future<bool> sendOrder(Order order) async {
try {
var db = await openDatabase(dbName);
} on Exception {
if (kDebugMode) {
print("Error");
}
}
return true;
}
}
import 'package:dio/dio.dart';
import '../../../domain/data/model/order.dart';
class RemoteDatesource {
final url = "";
Future<bool> sendOrder(Order order) async {
var response = await Dio().get(url);
return true;
}
}
import '../../../app/data/datasource/remote_datasource.dart';
class OrderBloc {
final SendOrderUsecase _sendOrderUsecase;
OrderBloc(this._sendOrderUsecase);
void ping() {
RemoteDatesource remoteDatesource = RemoteDatesource();
remoteDatesource.ping();
}
}
아키텍처 오염
• 강력한 정책
• 코드 리뷰
• 정적 검사
à human error 가능, 사후적 대처
어떻게 막을 수 있을까?
경계를 더 확실하게
나눈다면?
아키텍처
강건성(Robustness) 향상!
모듈을 활용한 클린 아키텍처 선 긋기
의존성 추가
아키텍처 오염 방지
모듈은 어떻게 추가할까?
• Application: 일반적인 앱 개발
• Plugin: Native 기능 활용
• Package: 재사용 강화를 위한 서브 컴포넌트 개발
• Module: 플러터 모듈을 네이티브에 포함시키기 위한 모듈 개발
• Skeleton: 기본 기능 포함된 샘플
프로젝트 유형
• Application: 일반적인 앱 개발
• Plugin: Native 기능 활용
• Package: 재사용 강화를 위한 서브 컴포넌트 개발
• Module: 플러터 모듈을 네이티브에 포함시키기 위한 모듈 개발
• Skeleton: 기본 기능 포함된 샘플
프로젝트 유형
• Application: 일반적인 앱 개발
• Plugin: Native 기능 활용
• Package: 재사용 강화를 위한 서브 컴포넌트 개발 à 플러터 패키지
• Module: 플러터 모듈을 네이티브에 포함시키기 위한 모듈 개발
• Skeleton: 기본 기능 포함된 샘플
프로젝트 유형
Mobile
Remote
Android
Web
Remote
iOS
Data
UI Device
도메인
인프라
Dart (Flutter 의존성 X)
순수 dart package는
IDE에서 미지원
직접 추가하자!
Command에서 직접 생성
전체 패키지 구조
• lib
• src ß 외부 프로젝트에서 접근 불가 (private)
• data
• presentation
• usecase
• domain.dart ß 외부 프로젝트에서 접근 가능
è 공개할 파일들을 domain.dart에 모두export 해야 함
è 캡슐화는 잘 되어있으나, 멀티모듈로 사용할 때는 불편함
Domain 패키지 구조
• lib
• data ß 외부 프로젝트에서 접근 가능
• presentation ß 외부 프로젝트에서 접근 가능
• usecase ß 외부 프로젝트에서 접근 가능
è 캡슐화는 완화되지만, 멀티모듈로 사용할 때 편리함
캡슐화를 완화한 Domain 패키지 구조
• 플러그인 아키텍처 à 중요한 결정을 뒤로 미룸
• 유스케이스 지원을 위한 내부 도메인과, 상세 구현을 위한 외부 인프라를
분리
• 내부 도메인을 먼저 개발하고, 테스트할 수 있음
• 외부 인프라는 나중으로 미루거나, 쉽게 변경이 가능함
• 경계선 긋기 à 변경의 영향 범위 최소화
• 소스코드 수준에서 경계선 긋기
• 플러터 패키지를 활용하면 침범하기 어려운 경계선을 그을 수 있음
• 내부 도메인은 순수 dart 패키지로, 독립성을 지킬 수 있음
정리
Q & A

[2022]Flutter_IO_Extended_Korea_멀티모듈을활용한플러터클린아키텍처_임태규.pdf