## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
300 lines
7.3 KiB
Markdown
300 lines
7.3 KiB
Markdown
# UseCase 패턴 가이드
|
|
|
|
## 📌 개요
|
|
|
|
UseCase 패턴은 Clean Architecture의 핵심 개념으로, 비즈니스 로직을 캡슐화하여 재사용성과 테스트 용이성을 높입니다.
|
|
|
|
## 🏗️ 구조
|
|
|
|
```
|
|
lib/
|
|
├── domain/
|
|
│ └── usecases/
|
|
│ ├── base_usecase.dart # 기본 UseCase 인터페이스
|
|
│ ├── auth/ # 인증 관련 UseCase
|
|
│ │ ├── login_usecase.dart
|
|
│ │ ├── logout_usecase.dart
|
|
│ │ └── ...
|
|
│ ├── company/ # 회사 관리 UseCase
|
|
│ │ ├── get_companies_usecase.dart
|
|
│ │ ├── create_company_usecase.dart
|
|
│ │ └── ...
|
|
│ └── ...
|
|
```
|
|
|
|
## 🔑 핵심 개념
|
|
|
|
### 1. UseCase 추상 클래스
|
|
|
|
```dart
|
|
abstract class UseCase<Type, Params> {
|
|
Future<Either<Failure, Type>> call(Params params);
|
|
}
|
|
```
|
|
|
|
- **Type**: 성공 시 반환할 데이터 타입
|
|
- **Params**: UseCase 실행에 필요한 파라미터
|
|
- **Either**: 실패(Left) 또는 성공(Right) 결과를 담는 컨테이너
|
|
|
|
### 2. Failure 클래스
|
|
|
|
```dart
|
|
abstract class Failure {
|
|
final String message;
|
|
final String? code;
|
|
final dynamic originalError;
|
|
}
|
|
```
|
|
|
|
다양한 실패 타입:
|
|
- **ServerFailure**: 서버 에러
|
|
- **NetworkFailure**: 네트워크 에러
|
|
- **AuthFailure**: 인증 에러
|
|
- **ValidationFailure**: 유효성 검증 에러
|
|
- **PermissionFailure**: 권한 에러
|
|
|
|
## 📝 UseCase 구현 예시
|
|
|
|
### 1. 로그인 UseCase
|
|
|
|
```dart
|
|
class LoginUseCase extends UseCase<LoginResponse, LoginParams> {
|
|
final AuthService _authService;
|
|
|
|
LoginUseCase(this._authService);
|
|
|
|
@override
|
|
Future<Either<Failure, LoginResponse>> call(LoginParams params) async {
|
|
try {
|
|
// 1. 유효성 검증
|
|
if (!_isValidEmail(params.email)) {
|
|
return Left(ValidationFailure(
|
|
message: '올바른 이메일 형식이 아닙니다.',
|
|
));
|
|
}
|
|
|
|
// 2. 비즈니스 로직 실행
|
|
final response = await _authService.login(
|
|
LoginRequest(
|
|
email: params.email,
|
|
password: params.password,
|
|
),
|
|
);
|
|
|
|
// 3. 성공 결과 반환
|
|
return Right(response);
|
|
} on DioException catch (e) {
|
|
// 4. 에러 처리
|
|
return Left(_handleDioError(e));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. 파라미터가 없는 UseCase
|
|
|
|
```dart
|
|
class LogoutUseCase extends UseCase<void, NoParams> {
|
|
final AuthService _authService;
|
|
|
|
LogoutUseCase(this._authService);
|
|
|
|
@override
|
|
Future<Either<Failure, void>> call(NoParams params) async {
|
|
try {
|
|
await _authService.logout();
|
|
return const Right(null);
|
|
} catch (e) {
|
|
return Left(UnknownFailure(
|
|
message: '로그아웃 중 오류가 발생했습니다.',
|
|
));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎯 Controller에서 UseCase 사용
|
|
|
|
### 1. UseCase 초기화
|
|
|
|
```dart
|
|
class LoginControllerWithUseCase extends ChangeNotifier {
|
|
late final LoginUseCase _loginUseCase;
|
|
|
|
LoginControllerWithUseCase() {
|
|
final authService = inject<AuthService>();
|
|
_loginUseCase = LoginUseCase(authService);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. UseCase 실행
|
|
|
|
```dart
|
|
Future<bool> login() async {
|
|
final params = LoginParams(
|
|
email: emailController.text,
|
|
password: passwordController.text,
|
|
);
|
|
|
|
final result = await _loginUseCase(params);
|
|
|
|
return result.fold(
|
|
(failure) {
|
|
// 실패 처리
|
|
_errorMessage = failure.message;
|
|
notifyListeners();
|
|
return false;
|
|
},
|
|
(loginResponse) {
|
|
// 성공 처리
|
|
return true;
|
|
},
|
|
);
|
|
}
|
|
```
|
|
|
|
## 🧪 테스트 작성
|
|
|
|
### 1. UseCase 단위 테스트
|
|
|
|
```dart
|
|
void main() {
|
|
late LoginUseCase loginUseCase;
|
|
late MockAuthService mockAuthService;
|
|
|
|
setUp(() {
|
|
mockAuthService = MockAuthService();
|
|
loginUseCase = LoginUseCase(mockAuthService);
|
|
});
|
|
|
|
test('로그인 성공 테스트', () async {
|
|
// Given
|
|
const params = LoginParams(
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
);
|
|
final expectedResponse = LoginResponse(...);
|
|
|
|
when(mockAuthService.login(any))
|
|
.thenAnswer((_) async => expectedResponse);
|
|
|
|
// When
|
|
final result = await loginUseCase(params);
|
|
|
|
// Then
|
|
expect(result.isRight(), true);
|
|
result.fold(
|
|
(failure) => fail('Should not fail'),
|
|
(response) => expect(response, expectedResponse),
|
|
);
|
|
});
|
|
|
|
test('잘못된 이메일 형식 테스트', () async {
|
|
// Given
|
|
const params = LoginParams(
|
|
email: 'invalid-email',
|
|
password: 'password123',
|
|
);
|
|
|
|
// When
|
|
final result = await loginUseCase(params);
|
|
|
|
// Then
|
|
expect(result.isLeft(), true);
|
|
result.fold(
|
|
(failure) => expect(failure, isA<ValidationFailure>()),
|
|
(response) => fail('Should not succeed'),
|
|
);
|
|
});
|
|
}
|
|
```
|
|
|
|
### 2. Controller 테스트
|
|
|
|
```dart
|
|
void main() {
|
|
late LoginControllerWithUseCase controller;
|
|
late MockLoginUseCase mockLoginUseCase;
|
|
|
|
setUp(() {
|
|
mockLoginUseCase = MockLoginUseCase();
|
|
controller = LoginControllerWithUseCase();
|
|
controller._loginUseCase = mockLoginUseCase;
|
|
});
|
|
|
|
test('로그인 버튼 클릭 시 UseCase 호출', () async {
|
|
// Given
|
|
controller.emailController.text = 'test@example.com';
|
|
controller.passwordController.text = 'password123';
|
|
|
|
when(mockLoginUseCase(any))
|
|
.thenAnswer((_) async => Right(LoginResponse()));
|
|
|
|
// When
|
|
final result = await controller.login();
|
|
|
|
// Then
|
|
expect(result, true);
|
|
verify(mockLoginUseCase(any)).called(1);
|
|
});
|
|
}
|
|
```
|
|
|
|
## 💡 장점
|
|
|
|
1. **단일 책임 원칙**: 각 UseCase는 하나의 비즈니스 로직만 담당
|
|
2. **테스트 용이성**: 비즈니스 로직을 독립적으로 테스트 가능
|
|
3. **재사용성**: 여러 Controller에서 동일한 UseCase 재사용
|
|
4. **의존성 역전**: Controller가 구체적인 Service가 아닌 UseCase에 의존
|
|
5. **에러 처리 표준화**: Either 패턴으로 일관된 에러 처리
|
|
|
|
## 📋 구현 체크리스트
|
|
|
|
### UseCase 생성 시
|
|
- [ ] UseCase 클래스 생성 (base_usecase 상속)
|
|
- [ ] 파라미터 클래스 정의 (필요한 경우)
|
|
- [ ] 유효성 검증 로직 구현
|
|
- [ ] 에러 처리 구현
|
|
- [ ] 성공/실패 케이스 모두 처리
|
|
|
|
### Controller 리팩토링 시
|
|
- [ ] UseCase 의존성 주입
|
|
- [ ] 비즈니스 로직을 UseCase 호출로 대체
|
|
- [ ] Either 패턴으로 결과 처리
|
|
- [ ] 에러 메시지 사용자 친화적으로 변환
|
|
|
|
### 테스트 작성 시
|
|
- [ ] UseCase 단위 테스트
|
|
- [ ] 성공 케이스 테스트
|
|
- [ ] 실패 케이스 테스트
|
|
- [ ] 경계값 테스트
|
|
- [ ] Controller 통합 테스트
|
|
|
|
## 🔄 마이그레이션 전략
|
|
|
|
### Phase 1: 핵심 기능부터 시작
|
|
1. 인증 관련 기능 (로그인, 로그아웃)
|
|
2. CRUD 기본 기능
|
|
3. 복잡한 비즈니스 로직
|
|
|
|
### Phase 2: 점진적 확산
|
|
1. 새로운 기능은 UseCase 패턴으로 구현
|
|
2. 기존 코드는 리팩토링 시 UseCase 적용
|
|
3. 테스트 커버리지 확보
|
|
|
|
### Phase 3: 완전 마이그레이션
|
|
1. 모든 비즈니스 로직 UseCase화
|
|
2. Service 레이어는 데이터 액세스만 담당
|
|
3. Controller는 UI 로직만 담당
|
|
|
|
## 📚 참고 자료
|
|
|
|
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
|
- [Either Pattern in Dart](https://pub.dev/packages/dartz)
|
|
- [Flutter Clean Architecture](https://resocoder.com/flutter-clean-architecture-tdd/)
|
|
|
|
---
|
|
|
|
**작성일**: 2025-01-09
|
|
**버전**: 1.0 |