Flutter 소셜 로그인 & 프로필 로직, 클린 아키텍처 + 함수형 프로그래밍으로 리팩토링하기
들어가며
현재 앱개발 사이드 프로젝트를 진행하고 있는데, 기능을 추가하면서 비즈니스 로직이 점점 복잡해져서 리팩토링을 진행하고 있다. 오늘은 Flutter에서 소셜 로그인 후 유저 프로필 완성도를 체크하는 로직을 클린 아키텍처와 함수형 프로그래밍 패러다임으로 리팩토링한 과정을 정리했다. 소셜 로그인(인증)과 프로필 완성 로직을 깔끔하게 분리하고, 이 플로우를 관리할 별도의 유즈케이스를 만들었으며, 함수의 인자로 함수를 넘겨 특정 유즈케이스의 재사용성을 높였다.
원래 리팩토링이라고 하면 기존의 코드를 같이 적어서 어떻게 변화했는지 보여줘야하지만.. 사실상 코드를 갈아엎은 수준이라 전/후를 1:1로 비교하기 어려워서 후 코드만 적도록 하겠다 😂
왜 클린 아키텍처를 도입했나
관심사의 분리 (Separation of Concerns)
- Auth(인증)와 User(사용자 프로필) 로직이 뒤섞여 있으면 유지/보수가 어렵다.
- 인증 이후 프로필 검증 과정을 분리해, 필요 시 재사용하기 쉽게 만들고자 했다.
4계층 구조
아키텍처를 크게 Domain, Application, Infrastructure, Presentation으로 나눴다.
- Domain
- 핵심 비즈니스 로직, 엔티티(User, UserProfile 등), 인터페이스(Repository)
- 예) UserRepository, AuthRepository, UserProfile 등
- Application
- UseCase(애플리케이션 로직) 담당
- 예) CheckUserUseCase, SignInFlowUseCase 등
- Infrastructure
- 실제 데이터 접근, 네트워크/DB 연동 담당 (DataSource, Repository 구현체)
- Presentation
- ViewModel, UI
각 계층 간 의존성 방향이 안쪽에서 바깥쪽으로 흐르도록 했다.
Domain ← Application ← Infrastructure ← Presentation
이렇게 하면 안쪽(Domain) 로직을 고칠 때 UI가 영향을 크게 받지 않고, 테스트 또한 용이해진다. 클린 아키텍처에 대해서는 추후에 자세히 다루도록 하겠다 !
리팩토링 과정
로그인 플로우를 관리하는 별도의 Usecase 생성
서비스 이용까지 1. 소셜 로그인 2. 프로필 완성이라는 두 개의 과정이 필요하다. 이 복잡한 과정을 관리해줄 유즈케이스를 만들었다. (기존에는 관리하는 로직이 따로 존재하지 않아, presentation 레이어에 있는 페이지 파일에 로직이 작성되어있었다.)
- 프로필 완성 전에는 인증이 선행되어야하므로, 소셜 로그인 진행
- 소셜로그인 진행 후, user 엔티티의 내부 필드들을 확인해서 프로필을 작성해야하는지 검증
class SignInFlowUseCase
implements AsyncUseCase<SignInFlowResult, SignInFlowParams> {
final AuthRepository _authRepository;
final CheckUserProfileUseCase _checkUserProfileUseCase;
SignInFlowUseCase(this._authRepository, this._checkUserProfileUseCase);
@override
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라는 유즈케이스를 함수형으로 작성해서 조건을 함수형태로 받아서 다양한 조건을 이 유즈케이스 하나로 검사할 수 있도록하였다.
usecase
class CheckUserProfileUseCase
implements AsyncUseCase<bool, bool Function(User)> {
final UserRepository userRepository;
CheckUserProfileUseCase(this.userRepository);
@override
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),
);
}
}
usecase 구현체
...
@override
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에서 사용
위 작업을 통해서 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;
},
);
}
...
}
UI에서의 처리
- needsProfile 값에 따라 프로필 작성 화면을 보여줄지, 메인 화면으로 보낼지 분기했다.
- UI는 로직을 최소화하고, 네비게이션 등 화면 전환만 담당하도록 하였다.
onPressed: () async {
try {
final flowResult = await authViewModel.signInWithGoogle();
if (flowResult.needsProfile) {
context.go('/profile/edit');
} else {
context.go('/home');
}
} catch (e) {
// 에러 UI 표시
}
},
**결론적으로, 클린 아키텍처와 함수형 프로그래밍 스타일을 접목해 소셜 로그인 → 프로필 완성 흐름을 리팩토링함으로써, 복잡도를 낮추고 확장성을 높일 수 있었다. **
클린 아키텍처는 현재 사이드프로젝트에 적용하면서 계속 공부 중인데, 처음엔 적용이 어렵지만 서비스가 복잡해질 수록 빛을 발휘하는 것 같다. 앞으로도 비즈니스 로직이 복잡해질 때마다, “관심사의 분리”와 “함수형 에러 처리”를 적극 활용해 유지보수성을 높여봐야겠다.