[Flutter] Use-Case

Raon·2025년 6월 2일

Flutter

목록 보기
26/29

필요한 기능을 구현할거면 Use-Case로 구현해서 가져다가 사용하세요.

내가 개발을 배우면서 항상 들어왔던 말이다. 어리석게도, 1~3년차 시절 나는 유즈케이스의 중요성에 대해 전혀 알지 못했다.

그래서일까? 최근 들어 유즈케이스를 만들어두지 않았던 나의 과오로 인해 현재 개발 중인 서비스에서 무수히 많은 문제점들이 나타나고 있다.

그래서 오늘은, 나와 같은 과오를 겪지 않도록 하기 위해, 유즈케이스가 왜 필요하고, 사용했을 때와 사용하지 않았을 때, 어떤 점이 다른지 알아보도록 하겠다.

유즈케이스가 뭐에요.

혹시라도, 유즈케이스가 무엇인지 모르는 사람들을 위해, 유즈케이스에 대해 간단하게 설명하고 넘어가고자 한다.

Use Case, 말 그대로 "사용 사례"이다. 우리가 개발을 할 때, 무수히 많은 요구사항들이 있을 텐데, 그 요구사항들 중 일부는 기술적으로 꽤나 유사하거나, 아니면 완전 동일한 경우일 수 있다. 예를 들어보자.

우리는 도서관 관리 프로그램을 만들어 고객에게 전달하고자 한다. 고객은 우리에게, 다음과 같은 요구사항을 말했다.

"우리 도서관에서는 A화면과 B화면에서 인기도서목록을 보여주고 싶어요"

우리는 이를 위해 A화면과 B화면에서 "인기도서목록"을 조회해야한다. 이 경우, 단순히 개발을 흐름대로 만들게 된다면 A화면과 B화면 모두에 "인기도서목록" 조회 함수를 만들게 될 것이다. 뭔가 이상하지 않은가? 두 화면 모두 동일한 함수를 가지게 될 확률이 매우 높다.

유즈케이스는 이러한 경우 유용하게 사용될 수 있다. 우리는 유즈케이스 클래스를 만들고 이 클래스 내에 비즈니스 로직을 구현하는 것을 통해 A화면과 B화면에 구현되어야할 비즈니스 로직을 공통화할 수 있다.

또한 유즈케이스를 작성할 때는 아래의 두 가지를 유념해야한다.

  • 오직 한가지 기능만을 수행해야한다.
  • 서로 다른 유즈케이스끼리 의존할 수 없다.

여기서 "한가지 기능"이라고 하는 것은 개발자의 입장에서 생각하는 "한가지 기능 수행"을 의미한다.
예를 들어, 카카오톡을 예로 들어보면, 사용자가 "채팅 메시지 전송" 버튼을 눌렀을 때 일어나는 일련의 과정을 수행하기 위해서는 다음과 같은 과정이 필요할 것이다.
1. 사용자가 타이핑한 텍스트를 채팅 메시지 데이터로 가공한다.
2. 가공된 데이터를 로컬DB에 저장하고, 동시에 서버로 메시지를 보낸다.
3. 실시간 통신(MQTT 또는 Socket)을 통해 메시지를 수신받는다.
4. 메시지 수신이 성공하면 서버에 저장된 메시지를 보여준다.
5. 메시지 발송이 실패할 경우(비행기모드, 인터넷 연결 끊김 등), 재전송/삭제 버튼이 보이도록 한다.
(위의 과정은 추측이므로, 실제 카카오톡의 메시지 전송 과정과는 차이가 있을 수 있다.)

이 경우, 사람마다 다르겠지만, 나라면 최소 5개의 유즈케이스를 만들 것 이다.

  • 메시지 데이터 가공 UseCase
  • 임시 메시지 저장 UseCase
  • 메시지 발송 UseCase
  • 메시지 수신 성공 UseCase
  • 메시지 수신 실패 UseCase

이렇듯, 한가지 기능은 개발자의 관점에서 생각하는 "단 하나의 동작"을 의미한다.

유즈케이스를 사용하지 않은 경우

그렇다면, 유즈케이스에 대해 알아보았으니, 실제로 사용하지 않은 경우와 사용한 경우를 앞서 소개된 요구사항을 바탕으로 구현해보자. 예제 구현은 상태관리 라이브러리를 사용하지 않고, 가장 순수한 형태의 플러터 코드로 구현했다.

우선, 예제 구현을 위해 필요한 BookModel을 정의해보자.

final class BookModel {
  final String bookName;
  final String authorName;

  const BookModel({
    required this.bookName,
    required this.authorName,
  });
}

/// 예제에서 사용될 더미 데이터 리스트
List<BookModel> dummies = List.generate(30, (idx) {
  return BookModel(
    bookName: "Test Book Title ${idx+1}", 
    authorName: "test Author name ${idx+1}",
    );
},);

예제에는 필요 이상의 데이터를 넣게 되면 헷갈릴 수 있으니, 아주 기초적인 형태의 모델을 정의해주었다.
그리고 이제 뷰모델을 정의해주자.

final class AViewModel extends ChangeNotifier {
  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  AViewModel() {
    _getRatingBooks();
  }

  void _getRatingBooks() async {
    await Future.delayed(const Duration(milliseconds: 500));
    _ratingBooks = dummies;
    notifyListeners();
  }
}

/* ........... */

final class BViewModel extends ChangeNotifier {
  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  BViewModel() {
    _getRatingBooks();
  }

  void _getRatingBooks() async {
    await Future.delayed(const Duration(milliseconds: 500));
    _ratingBooks = dummies;
    notifyListeners();
  }
}

뷰모델은 AViewModel, BViewModel이라는 이름으로 정의했다.
이 뷰모델들은 각각 _getRatingBooks라는 이름의 함수를 가지고 있으며, 이 함수를 통해 인기 도서 목록 데이터를 가져온다.

자, 이제 갑자기, 우리의 고객이 요구사항 변경을 요청했다.

"생각해보니, 인기도서목록을 5개 까지만 보여주면 될 것 같아요. 수정 해주세요."

이제 우리는 AViewModel과 BViewModel을 모두 수정해야하는 상황에 놓여있게 되었다. 수정을 하게된다면, 아마 아래와 같이 작성하게 될 것이다.

final class AViewModel extends ChangeNotifier {
  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  AViewModel() {
    _getRatingBooks();
  }

  void _getRatingBooks() async {
    await Future.delayed(const Duration(milliseconds: 500));
    final limitedBooks = dummies.take(5);
    _ratingBooks = limitedBooks.toList();
    notifyListeners();
  }
}

/* ........... */

final class BViewModel extends ChangeNotifier {
  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  BViewModel() {
    _getRatingBooks();
  }

  void _getRatingBooks() async {
    await Future.delayed(const Duration(milliseconds: 500));
    final limitedBooks = dummies.take(5);
    _ratingBooks = limitedBooks.toList();
    notifyListeners();
  }
}

이렇게, UseCase를 사용하지 않을 경우 고객의 요구사항 변경에 대응하기 위해서는 여러군데에 구현되어 있는 비즈니스 로직들을 모두 찾아 수정해주어야하는 경우가 빈번하게 발생하게 된다. 지금의 예제에서는 단순한 코드로 보고 있어 수정이 어렵지 않지만, 5만줄, 10만줄이 넘어가는 프로젝트에서 산재되어 있는 동일한 비즈니스 로직을 수정하려면 꽤나 힘든 작업이 될 것이다.

유즈케이스를 사용한 경우

그렇다면, 이제 유즈케이스를 사용한 경우에 대해 예제를 통해 알아보자. 앞서 소개한 유즈케이스의 유의점에 대해 생각하면서 유즈케이스를 만들어보면 아래와 같은 형태로 만들어 볼 수 있다.

final class GetRatingBooksUseCase {
  const GetRatingBooksUseCase();

  Future<List<BookModel>> execute() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return dummies.take(5).toList();
  }
}

유즈케이스는 일반적으로 하나의 클래스로 작성하며, 내부에는 단 하나의 public function을 가진다(이름의 경우 call, invoke, execute 등 개발자마다 다양하게 사용한다).

이제, 이 유즈케이스를 ViewModel에 의존성으로 주입해준다.

final class AViewModel extends ChangeNotifier {
  final GetRatingBooksUseCase _getRatingBooksUseCase;

  AViewModel({required GetRatingBooksUseCase getRatingBooksUseCase})
    : _getRatingBooksUseCase = getRatingBooksUseCase {
    _getRatingBooks();
  }

  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  void _getRatingBooks() async {
    _getRatingBooksUseCase.execute().then((books) {
      _ratingBooks = dummies;
      notifyListeners();
    });
  }
}

/* .................. */

final class BViewModel extends ChangeNotifier {
  final GetRatingBooksUseCase _getRatingBooksUseCase;

  BViewModel({required GetRatingBooksUseCase getRatingBooksUseCase})
    : _getRatingBooksUseCase = getRatingBooksUseCase {
    _getRatingBooks();
  }

  List<BookModel> _ratingBooks = [];
  List<BookModel> get ratingBooks => _ratingBooks;

  void _getRatingBooks() async {
    _getRatingBooksUseCase.execute().then((books) {
      _ratingBooks = dummies;
      notifyListeners();
    });
  }
}

이전 코드와 비교해보면, _getRatingBooks에 비즈니스 로직이 보이지 않고, ViewModel에서는 단순히 상태만 갱신한다는 것을 알 수 있다. 이런 코드라면, 다음과 같은 고객의 요구사항이 추가적으로 발생해도 손쉽게 대처할 수 있다.

"5개는 너무 적은 것 같아요. 10개까지 보이도록 수정해주세요."
"C화면에서도 인기 도서 목록을 보여주고 싶어요."

수정은 어렵지 않다. ViewModel의 수정 없이, GetRatingBooksUseCase만 수정하면 이를 의존성으로 주입받는 모든 ViewModel에서는 변경사항이 반영될 것이다.

마무리

이렇게 유즈케이스에 대해 알아보았다. 사실, 나는 몇달 전까지만 하더라도 유즈케이스를 작성하는 것이 너무 과하다는 생각을 가지고 있었다. 뷰모델/리포지토리를 통해 비즈니스 로직에 대해 충분히 대처할 수 있다고 굳게 믿었기 때문이다.

하지만, 시간이 지나면지날수록, 점점 구조가 망가져가는 내 프로젝트를 보면서, 유즈케이스의 필요성을 실감하게 되었고, 현재는 되도록이면 유즈케이스로 만들어 두는 것이 프로젝트의 구조적인 안정성과 더불어, 가독성을 높이는 방법이라고 생각하고 있다.

다음 글에서는 인터페이스에 대해 다뤄보도록 하겠다.

profile
Flutter 개발자

0개의 댓글