[Flutter] 함수형 프로그래밍으로 클린 아키텍처 구현하기: dartz, typedef

도톨이·2025년 3월 31일
0

앱 개발-flutter

목록 보기
31/35

앞선 글에서는 Equatable 을 통해 도메인 레이어의 Entity 을 값으로 비교하는 방식을 알아봤다.
이번에는 UseCase와 Repository 추상 클래스를 구현하며 그 안에서 사용되는 dartz, Either, typedef, Failure 을 알아보려고 한다.

Repository 추상 클래스

클린아키텍처에서 도메인 계층에는 Repository 추상 클래스를 사용한다.
보통 도메인 레이어에서는 비즈니스 로직을 정의하는데 이때 외부에서 데이터를 가져오거나 저장하는 작업이 어떻게 이루어지는지 그 세부 구현에 대해서는 몰라도 된다. 따라서 이런 접근을 위해 Repository Pattern 을 사용한다.
이 Repository 패턴은 도메인 레이어에서는 Repository 의 추상 클래스만 정의하고, 구체적 구현을 data layer 에서 구현하는 것이다.

그 예로 AuthenticationRepository 추상 클래스는 다음처럼 작성할 수 있다.

아래의 Repository 는 유저 생성 및 유저 가져오기 작업을 가진다.
여기서 배울 수 있는 점은 리턴 타입으로 쓰인 ResultVoid, ResultFuture<List<User>> 이다. 이 타입은 자체적으로 내장된 것이 아닌 반복되는 타입 정의에 개발자가 별명을 붙여서 사용한 것(typedef)인데 다음 챕터들에서 알아볼 것이다.

abstract class AuthenticationRepository {
  const AuthenticationRepository();

  ResultVoid createUser({
    required String createdAt,
    required String name,
    required String avatar,
  });

  ResultFuture<List<User>> getUsers();
}

dartz 패키지와 Either, typedef 에 대해

dartz 패키지는 Dart 에서 함수형 프로그래밍을 쉽게 구현하도록 하는 패키지이다.
함수형 프로그래밍은 불변성(immutability)순수 함수(pure function) 특징을 가진다.

  • 불변성(immutability): 데이터는 변경되지 않고, 새로운 값을 리턴하는 방식으로 상태를 관리한다.
  • 순수 함수(pure function): 입력이 같으면 항상 같은 출력을 내고, 외부 상태에 의존하지 않는 함수다.

이러한 특징 덕분에 코드의 예측 가능성이 높고, 테스트가 쉬워진다. Either<L, R> 도 이러한 철학을 반영한 타입으로, 기존의 try-catch보다 더 명확하게 에러 처리 흐름을 표현할 수 있다. 이는 실패 시에는 L(Left) 을 성공 시에는 R(Right) 을 리턴한다는 타입이다.

예를 들어 Either<Failure, User> 은 실패하면 Failure 을 성공하면 User 을 담는다는 뜻이다.

dart 에서 typedef 는 길거나 반복되는 타입 정의에 별명을 붙이는 기능인데, 다음처럼 사용할 수 있다.

typedef ResultFuture<T> = Future<Either<Failure, T>>;
typedef ResultVoid = ResultFuture<void>;

위와 같은 typedef 을 통해 기존에는

Future<Either<Failure, List<User>>> 로 썼던 코드를 ResultFuture<List<User>> 로 간결하게 표현할 수 있다.

함수형 에러 처리는 어떻게 이루어지는가?

기존에 내가 했던 에러처리는 다음처럼 진행했었다.

try {
  final user = await repository.getUser();
} catch (e) {
  // 실패 처리
}

이때 함수형 에러 처리를 하면 더 간결하게 표현할 수 있다.
위에서 배웠던 Either 을 이때 사용할 수 있다.
아래 코드를 보면 repository.getUsers()의 타입은 ResultFuture<List<User>>
즉, 타입 정의에 따르면 Future<Either<Failure, List<User>>> 이다. 그러면 result.fold() 을 통해 만약 failure 이면 에러를 출력하고 List 이면 유저 수를 출력하도록 할 수 있다.

final result = await repository.getUsers();

result.fold(
  (failure) => print('에러: ${failure.message}'),
  (users) => print('유저 수: ${users.length}'),
);

Failure 은 뭔가요?

자연스럽게 사용하고 있던 Failure 은 사실 오류 타입을 개발자가 정해둔 것이었다.
core/errors/failure.dart 에 다음처럼 정의를 하였다.
아래와 같은 코드를 통해 다양한 실패를 하나의 타입(Either<Failure, T>)으로 통합해서 처리할 수 있다
예를 들어 서버 에러, 캐시 실패, 인증 실패 등을 각각 ApiFailure, CacheFailure, AuthFailure 등으로 관리하여 에러를 구조화할 수 있다.

abstract class Failure extends Equatable {
  final String message;
  final int statusCode;
}

class ApiFailure extends Failure {
  const ApiFailure({required super.message, required super.statusCode});
}

Usecase 구현하기

클린 아키텍처에서 UseCase(유즈케이스)는 앱의 비즈니스 로직을 캡슐화하는 핵심 요소이다.
즉, "앱에서 일어나는 행동 하나하나(예: 유저 생성하기, 유저 리스트 가져오기)"를 UseCase 단위로 정의한다.

UseCase 를 그냥 CreateUser, GetUsers .. 등으로 바로 작성해도 되지만, 그 전에 UseCase 의 베이직 클래스를 제작하면 더욱 구조화된 설계가 가능하다.

예를 들어, 특정 파라미터를 받아야 동작하는 UseCase 가 있고, 파라미터 없이 동작하는 UseCase 가 있을 수 있다.

특정 파라미터를 받아야 동작하는 유즈케이스의 베이스 클래스는 다음과 같은 추상 클래스로 정의할 수 있다.

abstract class UsecaseWithParams<Type, Params> {
  const UsecaseWithParams();

  ResultFuture<Type> call(Params params);
}

그리고 파라미터 없이 동작하는 유즈케이스를 정의할 때 쓰는 베이스 클래스는 다음과 같은 추상 클래스로 정의할 수 있다.

abstract class UsecaseWithoutParams<Type> {
  const UsecaseWithoutParams();

  ResultFuture<Type> call();
}

이렇게 베이스 유즈케이스를 정의하면 사용할 때 상속해서 사용할 수 있다.

domain/usecases/create_user.dart

class CreateUser extends UsecaseWithParams<void, CreateUserParams> {
 const CreateUser(this._repository);

 final AuthenticationRepository _repository;

 
 ResultVoid call(CreateUserParams params) async =>
     _repository.createUser(
       createdAt: params.createdAt,
       name: params.name,
       avatar: params.avatar,
     );
}

domain/usecases/get_users.dart

class GetUsers extends UsecaseWithoutParams<List<User>> {
  const GetUsers(this._repository);

  final AuthenticationRepository _repository;

  
  ResultFuture<List<User>> call() async => _repository.getUsers();
}
profile
Kotlin, Flutter, AI | Computer Science

2개의 댓글

comment-user-thumbnail
2025년 6월 27일

최근에 Flutter 클린아키텍처 구현에 해당 코드가 있어, 어려움을 느꼈는데 쉽게 설명해주셔서 이해 잘 되었습니다. 감사합니다

1개의 답글