① 배운 것
도메인 엔티티의 필드명이 변경되면서 머지 충돌 발생했습니다:
gold)으로 뷰 개발goldAmount)으로 엔티티 변경 및 뷰 개발// 개발자 A의 코드
class CurrencyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final currency = ref.watch(currencyProvider);
return Text('${currency.gold}'); // gold 필드 직접 참조
}
}
// 개발자 B의 코드 (엔티티 변경 후)
class Currency {
final int goldAmount; // gold -> goldAmount로 변경
}
// => 머지 충돌 발생!
// BAD: 도메인 엔티티를 뷰에서 직접 사용
class CurrencyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final currency = ref.watch(currencyProvider);
return Text('${currency.gold}'); // 도메인 레이어에 직접 의존
}
}
// GOOD: UIState를 통한 데이터 표시
class CurrencyUIState {
final String formattedGold;
CurrencyUIState({required this.formattedGold});
factory CurrencyUIState.fromEntity(Currency currency) {
return CurrencyUIState(
formattedGold: '${currency.gold} Gold',
);
}
}
// Data Layer
class CurrencyDTO {
final int amount;
}
// Domain Layer
class Currency {
final int gold;
}
abstract class CurrencyRepository {
Future<Currency> getCurrency();
}
// Use Case
class GetCurrencyUseCase {
final CurrencyRepository repository;
Future<Currency> execute() => repository.getCurrency();
}
// Presentation Layer
class CurrencyViewModel extends StateNotifier<CurrencyUIState> {
final GetCurrencyUseCase useCase;
}
// 기존 Provider에 UseCase 추가
class CurrencyProvider extends StateNotifier<Currency> {
// UseCase 스타일 메서드
Future<Currency> getCurrency() async {
// 비즈니스 로직
}
}
// Provider에 접근 메서드 추가
class CurrencyProvider extends StateNotifier<Currency> {
int getGoldAmount() => state.gold; // 엔티티 직접 노출 대신 메서드 제공
}
// View
class CurrencyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(currencyProvider.notifier);
return Text('${provider.getGoldAmount()}'); // 메서드를 통한 안전한 접근
}
}
// Optional: UseCase Interface
abstract class GetCurrencyUseCase {
Future<Currency> execute();
}
// Implementation
class GetCurrencyUseCaseImpl implements GetCurrencyUseCase {
final CurrencyRepository repository;
Future<Currency> execute() => repository.getCurrency();
}
// Future 반환 예시
Future<Currency> getCurrency() async {
final dto = await dataSource.getCurrency(); // Future<DTO>
return Currency(gold: dto.amount); // 자동으로 Future<Currency>로 래핑
}
// StateNotifier
class CurrencyNotifier extends StateNotifier<CurrencyUIState> {
Future<void> loadCurrency() async {
state = const CurrencyUIState.loading();
try {
final currency = await useCase.execute();
state = CurrencyUIState.data(currency);
} catch (e) {
state = CurrencyUIState.error(e.toString());
}
}
}
// AsyncNotifier
class CurrencyViewModel extends _$CurrencyViewModel {
Future<CurrencyUIState> build() async {
final currency = await ref.read(getCurrencyUseCaseProvider).execute();
return CurrencyUIState.fromEntity(currency);
}
}
이렇게 코드 예시와 상세한 설명을 함께 포함하여 작성하면, 나중에 다시 볼 때도 전체적인 맥락과 구체적인 구현 방법을 쉽게 이해할 수 있을 것 같습니다.
② 회고 (restropective)
아직 갈 길이 멀다고 느끼지만 클린 아키텍처에 대해 진지하게 공부한 경험이 도움이 된 것 같다. 단순히 개념만 이해하는 것을 넘어 실제로 내 프로젝트에 적용할 방법을 고민하고 실천했다는 점이 뿌듯하다.
처음 이 문제를 고민하게 된 계기는 다른 작업자와 머지할 때 코드 충돌이 너무 자주 발생했기 때문이다. 왜 충돌이 발생하는지, 어떻게 하면 이를 줄일 수 있을지 고민하던 중 구조적인 문제가 있다고 판단했고 더 깊이 들여다보게 되었다. 충돌이 발생했을 때 단순히 “음 충돌이 났네, 대충 해결하고 넘어가자”라고 생각하는 대신, 이를 개선할 방법을 찾아본다는 점에서 스스로 성장했다고 느낀다. 대충 해결하고 넘어갈 수도 있었지만 협업 효율을 높이기 위해 고민했다는 점이 특히 의미 있다고 생각한다.
③ 개선을 위한 방법
이미 많이 진행된 회사 프로젝트에 완전한 클린 아키텍처를 적용하는 건 현실적으로 어려워서 특정 부분에만 적용해봤다. 클린 아키텍처는 이론적으로는 매력적이지만, 현업에서 이를 완벽히 구현하려고 하면 보일러플레이트 코드가 많아진다는 부정적인 의견도 존재한다.
실제로 한 부분에만 완전한 클린 아키텍처를 적용했을 때, 데이터 레이어의 DTO, 도메인 레이어의 Entity, 프레젠테이션 레이어의 UiState 등 하나의 로직에만 세 가지 모델 클래스가 필요했다. 그리고 이들을 매핑하는 과정도 번거롭게 느껴졌다. 지금까지의 경험으로는 DTO를 거의 수정하지 않고 바로 뷰로 전달해도 큰 문제가 없었기 때문에 더 그렇게 느껴졌던 것 같다.
클린 아키텍처는 개발자들 사이에서도 의견이 분분한 주제라 앞으로 더 다양한 사례와 의견을 들어볼 필요가 있을 것 같다.