원문 : https://resocoder.com/2020/03/09/flutter-firebase-ddd-course-1-domain-driven-design-principles/
Firebase? 앱을 빠르게 개발할 수 있습니다. Flutter? 역시나 같습니다. 그러나 인생에서는 언제나 트레이드오프가 있습니다. 열정적으로 개발을 시작하지만, 코드가 복잡해지면 금방 이러한 초기의 흥분은 희미해질 것입니다.
Flutter 앱은 코드를 쉽게 파악할 수 있는, 테스트 및 유지보수가 가능한 구조가 필요합니다. Flutter 앱을 설계하는 방식으로 어려움 없이 새로운 기능을 추가할 수 있다면 문제가 되지 않을 것입니다. 특히 Firebase Firestore와 같은 클라이언트 중심 서비스의 경우 코드를 깨끗하게 유지하는 것이 매우 중요합니다. 이런 설계를 Domain-Driven Design의 원칙에 따라 해보겠습니다.
우리는 다음과 같은 기능을 갖춘 상당히 복잡한 메모 작성 애플리케이션을 구축할 것입니다.
여러분은 Firebase 인증 및 Firestore를 사용하여 Flutter 앱을 보거나 구축한 적이 있을 것이며 Firebase 호출을 위젯 코드에 넣지 않았을 것이라고 확신합니다. 즉, 일종의 아키텍처를 사용했을 것이라고 생각하겠습니다.
간단한 REST API를 사용하여 작업한 Clean Architecture 과정을 보셨을 수도 있습니다. 거기에는 파일 및 폴더 구조와 함께 클래스 종속성 흐름이 깔끔하게 설명되어 있으며 Firebase에서도 사용할 수 있습니다. 그러면 DDD(도메인 기반 디자인)는 어떻게 다릅니까?
간단히 말해서 모든 수준에서 더 좋습니다. 우리는 여전히 아름다운 특성의 탐색 용이성과 테스트 가능성을 제공하는 레이어로 잘 분리되어 있습니다. 따라서, 우리는 여전히 모든 코드를 UI 또는 상태 관리에 넣지 않습니다.
모든 내용은 나중에 더 자세히 다루겠지만 지금은 아래 다이어그램이 DDD Flutter 앱에 있는 주요 아키텍처 계층을 개략적으로 설명한다는 점만 알아두세요. 이번 시리즈에서는 BLoC를 사용할 예정이지만, 늘 그렇듯 BLoC를 맹적이라고 선언한 여러분 모두를 잊지 않았습니다. 앱에서 사용하고 싶지 않다면 ChangeNotifier
, MobX Store
또는 새로운 StateNotifier
등 원하는 뷰 모델을 자유롭게 사용하세요. 이에 대해서는 나중에 자세히 설명하겠습니다.
다이어그램에 표시할 수 없는 몇 가지 사항이 있습니다.
저의 Clean Architecture 강좌에 익숙하신 분이라면 위의 다이어그램이 다소 익숙하게 느껴질 것입니다. 그렇지 않더라도 걱정하지 마세요. 이 튜토리얼 시리즈의 전제 조건은 아닙니다. Firestore
또는 FirebaseAuth
와 같은 클래스는 이미 만들어져있는 데이터 소스이므로 repositories 위쪽의 코드를 작성합니다.
데이터를 보유하고 운반하는 것 외에도
Entity
와 검증된ValueObject
에도 로직이 포함되어 있습니다. 이는 데이터 검증 및 복잡한 계산까지 다양합니다.
또한
Exception
들이 일반적인 데이터 흐름에Failure
로 어떻게 입력되는지 잘 보세요.try
및catch
문을 위한 유일한 장소는Repositories
입니다. 이렇게 하면 예외를 처리하지 않을 수 없게 되는데, 이는 매우 좋은 일입니다.
계속해서 모든 레이어와 해당 클래스의 역할이 무엇인지 자세히 살펴보기 전에 먼저 폴더 구조에 대한 오래된 질문을 다루겠습니다.
저는 Clean Architecture 과정에서 폴더 구조가 다루기 힘들다는 것을 처음으로 인정했습니다. DDD 코스에서는 다른 접근 방식을 취할 것입니다.
Layers will hold features, not the other way around. 이렇게 하면 여전히 코드를 읽을 수 있고, 가장 중요한 것은 더 많은 기능과 하위 기능을 추가하는 것이 정말 행복해질 것이라는 점입니다!
notes feature를 살펴보겠습니다. 기본 notes 폴더는 모든 레이어(application, domain, infrastructure, presentation) 내에 존재하지만 하위 폴더는 다릅니다!
이것은 무엇을 의미 할까요? 음, 우리가 좋은 폴더 구조와 아키텍처 레이어로의 분리를 동시에 가질 수 있는 걸까요?
일부 feature는 모든 레이어에서 반드시 표현될 필요조차 없다는 점도 주목할 가치가 있습니다. presentation 레이어에 splash 폴더가 보이시나요? 고유한 "splash 화면 로직"이 없으므로 이를 다른 레이어에 넣는 것은 의미가 없습니다.
전체적으로 위 다이어그램에 설명된 종속성 흐름을 유지하는 한 feature 간의 종속성을 혼합하고 일치시킬 수 있습니다(domain 레이어는 다른 레이어에 대한 종속성이 0이어야 함).
스파게티 아키텍처와 달리, 도메인 기반 설계 원칙을 따를 때 특정 클래스를 어디에 배치할지 항상 알 수 있습니다. 각 레이어에는 명확한 책임이 있습니다. 위의 폴더 구조 그림에서 볼 수 있듯이 모든 아키텍처 레이어에는 feature가 포함되어 있으며 해당 레이어의 모든 feature에 공통된 클래스(도우미, 추상 클래스 등)를 보유하는 core 폴더도 포함될 수 있습니다.
이 레이어는 모두 Widget인가요? Widget
의 상태도 이 레이어에 있습니다. 나는 이미 이 시리즈에서 BLoC를 사용할 것이라고 언급했습니다. 이 상태 관리 패턴에 익숙하지 않다면 이 튜토리얼을 확인해 보시기 바랍니다. ChangeNotifier
와 주요 차이점은 BLoC가 3가지 핵심 구성 요소로 분리된다는 것입니다.
ChangeNotifier
내부의 메서드와 동일합니다. 이는 BLoC 내부의 로직을 트리거하고 선택적으로 일부 원시 데이터(예: TextField
의 String
)를 BLoC로 전달할 수 있습니다.따라서 BLoC를 사용하지 않는 경우 State
및 Event
클래스를 원하는 단일 View Model 클래스로 바꾸면 됩니다.
도메인 기반 디자인에서는 UI가 앱에서 가장 멍청한 부분입니다. 이는 코드 경계에 있고 Flutter 프레임워크에 전적으로 의존하기 때문입니다. 그 논리는 사용자를 위한 "눈요기"를 만드는 것으로 제한됩니다. 따라서 애니메이션 코드는 이 레이어에 속하지만 양식 유효성 검사와 같은 작업도 프레젠테이션 레이어 내에서 수행되지 않습니다.
경험상, 일부 논리가 나중에 서버로 전송되거나 로컬 데이터베이스에 유지되는 데이터로 작동할 때마다 해당 로직이 presentation 레이어와 아무 관련이 없다는 것입니다.
이 레이어는 앱의 모든 외부 인터페이스와 떨어져 있습니다. 여기서는 UI 코드, 네트워크 코드 또는 데이터베이스 코드를 찾을 수 없습니다. application 레이어에는 다른 모든 레이어를 조정하는 단 하나의 작업만 있습니다. 데이터의 출처(사용자 입력, 실시간 Firestore 스트림, 기기 위치)에 관계없이 첫 번째 목적지는 application 레이어가 됩니다.
(그림의 화살표는 presentation 레이어로부터 전송되는 이벤트입니다.)
application 레이어의 역할은 데이터를 사용하여 "다음에 수행할 작업"을 결정하는 것입니다. 복잡한 비즈니스 로직을 수행하지 않고 대신 주로 사용자 입력이 검증되었는지(domain 레이어에서 호출하여) 확인하거나 infrastructure 데이터 스트림에 대한 구독을 관리합니다(직접적이지는 않지만 의존성 역전을 활용하여). 원칙에 대해서는 나중에 자세히 설명합니다.)
BLoC를 사용하지 않는 경우에는 스스로 선택하고 애플리케이션 로직을 뷰 모델에 넣지 마십시오. 재사용 가능한 일회용
UseCase
클래스를 만드는 것이 좋습니다. 신비한 삼촌으로부터 자세한 내용을 알아보세요.
domain 레이어는 앱의 순수한 중심입니다. 완전히 독립적이며 다른 레이어에 의존하지 않습니다. 도메인은 자신의 일을 잘하는 것 외에는 아무 관심도 없습니다.
이는 Firebase에서 REST API로 전환하거나 마음이 바뀌어 Hive 데이터베이스에서 Moor로 마이그레이션하더라도 상관하지 않는 앱의 일부입니다. domain은 외부에 의존하지 않기 때문에 구현 세부 사항을 변경해도 영향을 받지 않습니다. 반면에 다른 모든 레이어는 domain에 의존합니다.
그렇다면 domain 레이어 내부에서는 정확히 무슨 일이 벌어지고 있을까요? 여기에는 비즈니스 로직이 존재하는 곳입니다. 이 시리즈의 다음 부분에서 모든 것에 대해 자세히 다룰 예정이지만 Flutter/server/device에 종속되지 않는 모든 것은 도메인에 속합니다. 여기에는 다음이 포함됩니다.
ValueObject
를 사용하여 데이터 유효성을 검사하고 유효성을 유지합니다. 예를 들어, Note
본문에 일반 String
을 사용하는 대신 NoteBody
라는 별도의 클래스를 사용하겠습니다. 이는 String
값을 캡슐화하고 길이가 1000자를 넘지 않고 비어 있지 않은지 확인합니다.
이러한 종류의 유효성 검사는 일반적으로 UI용으로 예약되어 있습니다. 그러나 우리는 Dart 타입 시스템을 최대한 활용할 것입니다. 예를 들어
EmailAddress
와Password
는 모두String
을 캡슐화하지만Password
인수를 기대하는 메서드에EmailAddress
를 전달하는 것은 불가능하며 그 반대의 경우도 마찬가지입니다.
데이터 변환(예: 색상을 완전히 불투명하게 만들기)
Entity
클래스(예: User
또는 Note
엔터티)를 통해 함께 속하는 데이터를 그룹화하고 고유하게 식별
복잡한 비즈니스 로직 수행 - 복잡한 로직을 서버에 맡길 수도 있기 때문에 클라이언트 Flutter 앱에서 항상 그런 복잡한 로직을 수행하는 것은 아닙니다. 하지만 진정한 서버리스를 구축하고 있다면? 앱에서는 여기에 해당 로직을 입력합니다.
domain 레이어는 앱의 핵심입니다. 다른 레이어의 변경 사항은 영향을 미치지 않습니다. 그러나 domain에서의 변경은 다른 모든 레이어에 영향을 미칩니다. 이것은 의미가 있습니다. 아마도 매일 비즈니스 로직을 변경하지 않을 것입니다.
이 모든 것 외에도 domain 계층은 Failure
의 본거지이기도 합니다. 예외 처리는 ? 경험입니다. 그들은 여러 코드 계층을 가로질러 좌우로 날아다니고 있습니다. 어떤 메서드가 어떤 예외를 발생시키는지 알려면 문서(심지어 자신의 문서라도)를 백만 번 확인해야 합니다. 그럼에도 불구하고 몇 달 후에 코드로 다시 돌아오면 모든 예외 사례를 처리했는지 다시 확신할 수 없게 될 것입니다.
우리는 유니온 타입을 통해 이러한 고통을 완화하고 싶습니다! "올바른" 데이터에 대해 return
키워드를 사용하고 문제가 발생했을 때 throw
키워드를 사용하는 대신 Failure
유니온을 사용할 것입니다. 이를 통해 문서를 확인하지 않고도 메서드의 실패 가능성을 알 수 있습니다. 다시 한 번, 다음 부분에서 세부 사항을 살펴보겠습니다.
presentation과 마찬가지로 이 레이어도 앱의 경계에 있습니다. 물론 "반대"에 있지만 사용자 입력 및 시각적 출력을 처리하는 대신 API, Firebase 라이브러리, 데이터베이스 및 장치 센서를 처리합니다.
infrastructure 레이어는 로우 레벨 데이터 소스와 하이 레벨 리포지토리라는 두 부분으로 구성됩니다. 또한 이 계층에는 DTO(data transfer objects)가 포함됩니다. 자세히 살펴보겠습니다.
DTO는 domain 레이어의 entities와 value objects간 데이터와 외부 세계의 일반 데이터를 변환하는 것이 유일한 목적인 클래스입니다. 아시다시피String
또는 int
와 같은 멍청한 데이터만 Firestore에 저장할 수 있지만 우리는 앱 전체에서 이런 종류의 검증되지 않은 데이터를 원하지 않습니다. 이것이 바로 우리가 infrastructure 레이어를 제외하고 위에서 설명한 ValueObject
를 모든 곳에서 사용하는 이유입니다. DTO는 직렬화 및 역직렬화될 수도 있습니다.
데이터 소스는 가장 낮은 레벨에서 작동합니다. 원격 데이터 소스는 서버에서 받은 JSON 응답 문자열을 DTO에 맞추고 JSON으로 변환된 DTO를 사용하여 서버 요청을 수행합니다. 마찬가지로 로컬 데이터 소스는 로컬 데이터베이스나 장치 센서에서 데이터를 가져옵니다.
cloud_firestore 및 firebase_auth와 같은 Firebase 클라이언트 라이브러리는 우리를 위해 데이터 소스의 무거운 작업을 수행합니다. 그렇기 때문에 이 과정에서는 데이터 소스를 만들지 않습니다.
리포지토리는 domain 및 application 레이어와 추악한 외부 세계 사이의 경계가 되는 중요한 작업을 수행합니다. 데이터 소스의 DTO 및 무질서한 Exception
를 입력으로 사용하고 멋진 Ether<Failure, Entity>
를 출력으로 반환하는 것이 그들의 임무입니다.
Either
에 대해 처음 듣는 경우 다음 강의에서 이에 대해 자세히 알아볼 수 있습니다. 또 다른 옵션은 참을성이 없다면 이 Kotlin 문서를 읽는 것입니다.
Firebase Firestore를 사용하지 않는 경우 로컬에서 직접 데이터를 캐시해야 할 수도 있습니다. 이 경우 캐싱 논리를 수행하고 원격 데이터 소스의 데이터를 로컬 데이터 소스에 배치하는 것을 조정하는 것이 리포지토리의 임무입니다.