팡세영
Log sey
팡세영
전체 방문자
오늘
어제
  • 분류 전체보기 (82)
    • PS (45)
      • programmers (13)
      • 백준 (29)
    • Android (16)
    • Daily (0)
    • Kotlin (6)
    • Design Pattern (3)
    • Java (1)
    • Flutter (3)
    • 책 리뷰 (1)
      • 클린 아키텍처 (1)
    • 공식문서 읽기 운동 (4)
      • Flutter (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • Android
  • 안드로이드
  • 데이터계층
  • BFS
  • LEVEL2
  • ArcitecturePattern
  • 공식문서
  • 단일책임원칙
  • 프로그래머스
  • Kotlin
  • 완전탐색
  • flutter
  • 클린아키텍처
  • mvvm
  • DFS
  • programmers #프로그래머스
  • TestCode
  • 골드
  • vercel
  • 문자열
  • java
  • 하단네비게이션바
  • 정렬
  • 플러터
  • 백준
  • 실버
  • CustomView
  • 구현
  • 해쉬맵
  • 자바

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
팡세영

Log sey

Flutter - DataLayer
공식문서 읽기 운동/Flutter

Flutter - DataLayer

2025. 7. 31. 04:50

해당 글은 플러터 공식 문서의 내용을 해석한 글입니다. 

https://docs.flutter.dev/app-architecture/case-study/data-layer

 

Data layer

A walk-through of the data layer of an app that implements MVVM architecture.

docs.flutter.dev


Flutter MVVM 아키텍처 가이드: Compass 앱 사례 중심

데이터 계층은 MVVM 용어로는 모델(Model)이라 불리며,

애플리케이션 내 모든 데이터의 단일 진실 공급원(Source of Truth) 역할을 합니다.

즉, 데이터는 오직 이곳에서만 업데이트되어야 합니다.

 

데이터 계층은

  • 외부 API로부터 데이터를 가져오고
  • 해당 데이터를 UI에 제공하며
  • UI로부터 발생하는 데이터 변경 이벤트를 처리하고
  • 필요한 경우 외부 API에 업데이트 요청을 보내는 역할을 담당합니다.

이 가이드에서 설명하는 데이터 계층은 레포지토리(Repository)와 서비스(Service)라는 두 가지 주요 구성 요소로 이루어져 있습니다.

 

 

레포지토리는 애플리케이션 데이터의 단일 진실 공급원(Source of Truth) 역할을 하며 해당 데이터와 관련된 로직을 포함합니다.

예를 들어 사용자 이벤트에 따라 데이터를 업데이트하거나 서비스로부터 주기적으로 데이터를 받아오는 작업 등이 이에 해당합니다.

또한 오프라인 기능을 지원하는 경우 데이터를 동기화하고 재시도 로직을 관리하며 데이터를 캐싱하는 역할도 담당합니다.

 

서비스는 상태를 가지지 않는(Stateless) Dart 클래스이며 HTTP 서버나 플랫폼 플러그인 같은 외부 API와 상호작용합니다.

애플리케이션 코드 내에서 생성되지 않는 외부 데이터는 모두 서비스 클래스에서 가져와야 합니다.


 

서비스

서비스 클래스는 아키텍처 구성 요소 중 가장 명확한 역할을 갖는 클래스입니다.

상태를 가지지 않으며, 그 안의 함수들은 부작용(side effect) 없이 동작합니다.

서비스 클래스의 유일한 역할은 외부 API를 감싸는 것입니다.

일반적으로 하나의 데이터 소스(예: HTTP 서버 클라이언트, 플랫폼 플러그인)마다 하나의 서비스 클래스가 존재합니다.

 

예를 들어 Compass 앱에서는 APIClient라는 서비스 클래스가 있으며 이 클래스는 클라이언트용 서버와의 CRUD 요청을 처리하는 역할을 합니다.

 

class ApiClient {
  // Some code omitted for demo purposes.

  Future<Result<List<ContinentApiModel>>> getContinents() async { /* ... */ }

  Future<Result<List<DestinationApiModel>>> getDestinations() async { /* ... */ }

  Future<Result<List<ActivityApiModel>>> getActivityByDestination(String ref) async { /* ... */ }

  Future<Result<List<BookingApiModel>>> getBookings() async { /* ... */ }

  Future<Result<BookingApiModel>> getBooking(int id) async { /* ... */ }

  Future<Result<BookingApiModel>> postBooking(BookingApiModel booking) async { /* ... */ }

  Future<Result<void>> deleteBooking(int id) async { /* ... */ }

  Future<Result<UserApiModel>> getUser() async { /* ... */ }
}

 

서비스는 클래스 형태로 정의되며 각 메서드는 서로 다른 API 엔드포인트를 감싸고 비동기 응답 객체를 반환합니다.

앞서 예로 들었던 예약 삭제의 경우 deleteBooking 메서드는 Future<Result<void>>를 반환합니다.

 

일부 메서드는 API에서 받아온 원시 데이터를 표현하는 전용 데이터 클래스를 반환하기도 합니다.

예를 들어 BookingApiModel 클래스가 이에 해당합니다.

 

곧 살펴보겠지만, 레포지토리는 이러한 원시 데이터를 가공하여 다른 형식으로 변환해 외부에 제공합니다.


레포지토리

레포지토리의 유일한 책임은 애플리케이션 데이터를 관리하는 것입니다.

레포지토리는 특정 종류의 데이터에 대한 단일 진실 공급원(Source of Truth) 역할을 하며 해당 데이터는 오직 이곳에서만 변경 되어야 합니다.

 

애플리케이션에서 데이터 유형마다 별도의 레포지토리를 두는 것이 좋습니다.

예를 들어, Compass 앱에는 다음과 같은 레포지토리들이 존재합니다

  • UserRepository
  • BookingRepository
  • AuthRepository
  • DestinationRepository

아래는 Compass 앱의 BookingRepository 예시로, 레포지토리의 기본 구조를 보여줍니다

class BookingRepositoryRemote implements BookingRepository {
  BookingRepositoryRemote({
    required ApiClient apiClient,
  }) : _apiClient = apiClient;

  final ApiClient _apiClient;
  List<Destination>? _cachedDestinations;

  Future<Result<void>> createBooking(Booking booking) async {...}
  Future<Result<Booking>> getBooking(int id) async {...}
  Future<Result<List<BookingSummary>>> getBookingsList() async {...}
  Future<Result<void>> delete(int id) async {...}
}

 

앞선 예시에서 사용된 클래스는 BookingRepositoryRemote로, 이는 추상 클래스인 BookingRepository를 상속합니다.

이 기반 클래스(BookingRepository)는 환경에 따라 서로 다른 레포지토리를 생성할 수 있도록 설계되어 있습니다.

 

예를 들어 Compass 앱에는 다음과 같은 구현체들이 존재합니다:

  • BookingRepositoryRemote: 실제 서버와 통신하는 원격 환경용 레포지토리
  • BookingRepositoryLocal: 로컬 개발을 위한 개발용 레포지토리 (예: 임시 데이터, Mock 데이터 사용)

이처럼 추상화된 레포지토리 구조를 사용하면

개발, 테스트, 프로덕션 환경에 따라 유연하게 구현체를 교체할 수 있어 관리와 테스트가 쉬워집니다.

GitHub에서는 이러한 레포지토리 클래스들의 차이점을 직접 확인할 수 있습니다.

 

BookingRepository는 서버로부터 원시 데이터를 가져오고 업데이트하기 위해 ApiClient 서비스를 주입받아 사용합니다.

이때 중요한 점은, ApiClient는 private 멤버로 선언되어야 하며, UI 레이어에서 직접 접근할 수 없어야 합니다.

이는 UI가 레포지토리를 우회하여 서비스에 직접 요청을 보내지 못하게 하기 위함입니다.

 

ApiClient를 통해 레포지토리는

  • 서버에서 발생한 예약 내역 변경을 주기적으로 확인(polling) 하고
  • 저장된 예약을 삭제하는 POST 요청 등을 수행할 수 있습니다.

또한 레포지토리가 애플리케이션 모델로 변환하는 원시 데이터는 하나 이상의 서비스 또는 다양한 데이터 소스에서 가져올 수 있습니다.

이로 인해 레포지토리와 서비스 간에는 다대다(Many-to-Many) 관계가 형성됩니다:

  • 하나의 서비스는 여러 레포지토리에서 사용될 수 있고,
  • 하나의 레포지토리는 여러 서비스에 의존할 수 있습니다.

이러한 구조는 각 책임을 분리하면서도 유연하고 확장 가능한 아키텍처를 만드는 데 기여합니다.

 


도메인 모델

BookingRepository는 Booking과 BookingSummary라는 도메인 모델(Domain Model) 을 반환합니다.

모든 레포지토리는 이처럼 도메인 모델을 외부로 노출하는 역할을 합니다.

 

이 도메인 모델은 API 모델과는 구별되며 앱 전체에서 실제로 필요한 데이터만을 포함하고 있습니다.

반면 API 모델은 서버로부터 받은 원시 데이터로 앱에서 사용하기 위해선

필터링, 병합, 또는 불필요한 데이터 제거 등의 가공이 필요합니다.

 

레포지토리는 이러한 과정을 담당하며 원시 데이터를 정제하여 도메인 모델로 변환해 내보냅니다.

예제 앱에서 도메인 모델은 BookingRepository.getBooking() 같은 메서드의 반환값을 통해 노출됩니다.

이 getBooking 메서드는

  1. ApiClient 서비스로부터 원시 데이터를 가져오고,
  2. 여러 서비스 엔드포인트로부터 받은 데이터를 조합한 뒤,
  3. 이를 Booking 도메인 모델로 변환합니다.

 

즉, 레포지토리는 단순한 중계자가 아니라 복수의 데이터 소스를 조합하고 정제하는 로직의 중심입니다.

// This method was edited for brevity.
Future<Result<Booking>> getBooking(int id) async {
  try {
    // Get the booking by ID from server.
    final resultBooking = await _apiClient.getBooking(id);
    if (resultBooking is Error<BookingApiModel>) {
      return Result.error(resultBooking.error);
    }
    final booking = resultBooking.asOk.value;

    final destination = _apiClient.getDestination(booking.destinationRef);
    final activities = _apiClient.getActivitiesForBooking(
            booking.activitiesRef);

    return Result.ok(
      Booking(
        startDate: booking.startDate,
        endDate: booking.endDate,
        destination: destination,
        activity: activities,
      ),
    );
  } on Exception catch (e) {
    return Result.error(e);
  }
}

 

Compass 앱에서는 서비스 클래스들이 Result 객체를 반환합니다.

Result는 비동기 호출을 감싸는 유틸리티 클래스로,

에러 처리와 비동기 상태 기반의 UI 관리를 더 쉽게 만들어줍니다.

 

이 패턴은 권장 사항일 뿐 필수는 아닙니다.

이 가이드에서 제안하는 아키텍처는 Result 클래스를 사용하지 않고도 충분히 구현할 수 있습니다.

 

Result 클래스에 대한 자세한 내용은

Result Cookbook Recipe에서 확인할 수 있습니다.

 


이벤트 사이클 완료

이 페이지 전체에서 사용자가 저장된 예약을 삭제하는 흐름을 예시로 살펴보았습니다.

이 흐름은 사용자가 Dismissible 위젯에서 스와이프하는 이벤트로 시작됩니다.

  1. 이벤트 발생: 사용자가 Dismissible 위젯을 스와이프해 예약을 삭제
  2. ViewModel 처리: HomeViewModel이 이 이벤트를 감지하고 실제 데이터 삭제 처리를 BookingRepository에 위임
  3. 레포지토리 처리는 아래와 같이 deleteBooking 메서드를 호출
  4. 서비스 호출: 레포지토리는 _apiClient.deleteBooking(id)를 호출해 서버에 POST 요청을 보내 예약 삭제
  5. 결과 반환: 처리 결과를 Result로 감싸 반환
  6. UI 반영: HomeViewModel이 이 Result를 받아 내부 상태를 업데이트한 후
  7. notifyListeners()를 호출하여 UI에 변경 사항 반영
// booking_repository_remote.dart
Future<Result<void>> delete(int id) async {
  try {
    return _apiClient.deleteBooking(id);
  } on Exception catch (e) {
    return Result.error(e);
  }
}

 

이로써 사용자 이벤트 → 서버 처리 → 상태 변경 → UI 반영이라는

이벤트 사이클이 완성됩니다

 

플러터 공식 문서에서 설명하는 Service는 정통 CleanArchitecture에서 말하는 DataSource를 뜻하는것 같다.

'공식문서 읽기 운동 > Flutter' 카테고리의 다른 글

Flutter - UI Layer  (1) 2025.07.31
Flutter - 앱 아키텍처 가이드  (2) 2025.07.30
Flutter - 앱 아키텍처 핵심개념  (3) 2025.07.30
    '공식문서 읽기 운동/Flutter' 카테고리의 다른 글
    • Flutter - UI Layer
    • Flutter - 앱 아키텍처 가이드
    • Flutter - 앱 아키텍처 핵심개념
    팡세영
    팡세영
    Android, CS, PS

    티스토리툴바