현재 앱개발 사이드 프로젝트를 진행하고 있는데, 기능을 추가하면서 비즈니스 로직이 점점 복잡해져서 리팩토링을 진행하고 있다. 오늘은 Flutter에서 소셜 로그인 후 유저 프로필 완성도를 체크하는 로직을 클린 아키텍처와 함수형 프로그래밍 패러다임으로 리팩토링한 과정을 정리했다. 소셜 로그인(인증)과 프로필 완성 로직을 깔끔하게 분리하고, 이 플로우를 관리할 별도의 유즈케이스를 만들었으며, 함수의 인자로 함수를 넘겨 특정 유즈케이스의 재사용성을 높였다.
원래 리팩토링이라고 하면 기존의 코드를 같이 적어서 어떻게 변화했는지 보여줘야하지만.. 사실상 코드를 갈아엎은 수준이라 전/후를 1:1로 비교하기 어려워서 후 코드만 적도록 하겠다 😂
아키텍처를 크게 Domain, Application, Infrastructure, Presentation으로 나눴다.
각 계층 간 의존성 방향이 안쪽에서 바깥쪽으로 흐르도록 했다.
Domain ← Application ← Infrastructure ← Presentation
이렇게 하면 안쪽(Domain) 로직을 고칠 때 UI가 영향을 크게 받지 않고, 테스트 또한 용이해진다.
클린 아키텍처에 대해서는 추후에 자세히 다루도록 하겠다 !
서비스 이용까지 1. 소셜 로그인 2. 프로필 완성이라는 두 개의 과정이 필요하다. 이 복잡한 과정을 관리해줄 유즈케이스를 만들었다. (기존에는 관리하는 로직이 따로 존재하지 않아, presentation 레이어에 있는 페이지 파일에 로직이 작성되어있었다.)
class SignInFlowUseCase
implements AsyncUseCase<SignInFlowResult, SignInFlowParams> {
final AuthRepository _authRepository;
final CheckUserProfileUseCase _checkUserProfileUseCase;
SignInFlowUseCase(this._authRepository, this._checkUserProfileUseCase);
Future<Either<Failure, SignInFlowResult>> call(
SignInFlowParams params,
) async {
final authResult = await (params.provider == SignInProvider.google
? _authRepository.loginWithGoogle()
: _authRepository.loginWithApple());
return authResult.fold(
(failure) => left(failure),
(user) async {
final profileCheck = await _checkUserProfileUseCase(
(User user) => user.hasRequiredFields(),
);
return profileCheck.fold(
(failure) => left(failure),
(isComplete) => right(
SignInFlowResult(
user: user,
needsProfile: !isComplete,
),
),
);
},
);
}
}
인증 정보가 사라진 후 재로그인을 진행할 때 기존에 가입했던 사람은 프로필 완성 단계를 건너 뛰고 서비스를 이용할 수 있는데, 이 로직을 구현하기 위해서는 소셜로그인 후 프로필이 완성되어있는지 검사를 해야할 필요가 있다.
프로필을 검사하고 싶은 조건 마다 유즈케이스를 작성하기 보단, 프로필을 검사하는 checkUser라는 유즈케이스를 함수형으로 작성해서 조건을 함수형태로 받아서 다양한 조건을 이 유즈케이스 하나로 검사할 수 있도록하였다.
class CheckUserProfileUseCase
implements AsyncUseCase<bool, bool Function(User)> {
final UserRepository userRepository;
CheckUserProfileUseCase(this.userRepository);
Future<Either<Failure, bool>> call(
bool Function(User) condition,
) async {
final result = await userRepository.checkUser(condition);
return result.fold(
(failure) => left(failure),
(success) => right(success),
);
}
}
...
Future<Either<Failure, bool>> checkUser(Function(User) condition) async {
try {
// 현재 유저의 정보 가져오기
final userEither = await _authRepository.currentUser();
return userEither.fold(
(failure) => left(failure),
// 검사 후 결과를 boolean으로 반환
(user) => right(condition(user)),
);
} on ServerException catch (e) {
logger.e(e);
return left(Failure(e.message));
}
}
...
위 작업을 통해서 ViewModel은 단순히 UseCase 호출 → UI용 상태 업데이트 → 실패/성공에 대한 처리 만 진행하면 되도록 하였다. 클린 아키텍처 원칙대로 Presentation 레이어에서는 비즈니스 로직이 아니라 “사용 흐름”만 신경 썼다.
class AuthViewModel extends StateNotifier<AsyncValue<AuthState>> {
final SignInFlowUseCase _signInFlowUseCase;
AuthViewModel(this._signInFlowUseCase) : super(const AsyncValue.data(AuthInitial()));
Future<SignInFlowResult> signInWithGoogle() async {
state = const AsyncValue.loading();
// 복잡한 로그인 플로우를 해당 유즈케이스로 한 번에 진행
final result = await _signInFlowUseCase(SignInFlowParams(SignInProvider.google));
return result.fold(
(failure) {
// 실패 처리
state = AsyncValue.error(failure, StackTrace.current);
throw failure;
},
(flowResult) {
// 성공 시 AuthState 업데이트하고, 프로필 완성 여부를 반환
state = AsyncValue.data(AuthAuthenticated(flowResult.user));
return flowResult;
},
);
}
...
}
onPressed: () async {
try {
final flowResult = await authViewModel.signInWithGoogle();
if (flowResult.needsProfile) {
context.go('/profile/edit');
} else {
context.go('/home');
}
} catch (e) {
// 에러 UI 표시
}
},
결론적으로, 클린 아키텍처와 함수형 프로그래밍 스타일을 접목해 소셜 로그인 → 프로필 완성 흐름을 리팩토링함으로써, 복잡도를 낮추고 확장성을 높일 수 있었다.
클린 아키텍처는 현재 사이드프로젝트에 적용하면서 계속 공부 중인데, 처음엔 적용이 어렵지만 서비스가 복잡해질 수록 빛을 발휘하는 것 같다. 앞으로도 비즈니스 로직이 복잡해질 때마다, “관심사의 분리”와 “함수형 에러 처리”를 적극 활용해 유지보수성을 높여봐야겠다.