안녕하세요, 우기입니다! Flutter 개발 가이드라인 시리즈의 세 번째 글에서는 '낮은 결합도'의 중요성과 이를 통해 유지보수성을 높이는 다양한 방법에 대해 자세히 알아보겠습니다.
복잡한 앱을 개발할수록 코드베이스의 결합도 관리는 장기적인 프로젝트 성공에 결정적인 영향을 미칩니다. 이번 글에서는 Flutter 앱에서 결합도를 효과적으로 관리하는 구체적인 방법과 패턴을 살펴보겠습니다.
이 글을 통해 얻을 수 있는 것:
결합도의 의미와 영향 이해
다양한 결합도 감소 패턴 및 기법
실제 프로젝트에 적용 가능한 구체적인 예시
효과적인 아키텍처 구성 방법
결합도(Coupling)는 코드의 한 부분이 다른 부분에 얼마나 의존하는지를 나타내는 척도입니다. 높은 결합도는 다음과 같은 문제를 일으킵니다:
반면, 낮은 결합도는 다음과 같은 이점을 제공합니다:
모든 결합도가 동일하게 생성되는 것은 아닙니다. 결합도에는 다양한 종류와 수준이 있으며, 이를 이해하면 코드를 더 효과적으로 구성할 수 있습니다.
Flutter 앱을 개발할 때는 가능한 한 데이터 결합도나 스탬프 결합도 수준을 유지하는 것이 좋습니다.
// ❌ 내용 결합도: 다른 클래스의 비공개 멤버에 직접 접근
class UserProfileScreen extends StatelessWidget {
Widget build(BuildContext context) {
final userManager = UserManager();
// userManager의 내부 구현에 직접 접근
return Text('Welcome, ${userManager._currentUser.name}');
}
}
// ❌ 공통 결합도: 전역 상태에 의존
class CartButton extends StatelessWidget {
Widget build(BuildContext context) {
// 전역 변수 사용
return Badge(
count: GlobalState.cartItems.length,
child: IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// 전역 함수 호출
GlobalState.navigateToCart();
},
),
);
}
}
// ❌ 제어 결합도: 한 클래스가 다른 클래스의 흐름 제어
class LoginScreen extends StatelessWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
final authService = AuthService();
if (authService.login()) {
// 다른 클래스의 내부 상태에 기반한 제어 흐름
if (authService.isFirstLogin) {
Navigator.pushNamed(context, '/onboarding');
} else {
Navigator.pushNamed(context, '/home');
}
// 다른 클래스의 내부 상태 직접 변경
authService.isFirstLogin = false;
}
},
child: Text('로그인'),
);
}
}
의존성 주입은 클래스가 자신의 의존성을 직접 생성하지 않고 외부에서 받아오는 패턴입니다.
// ❌ 높은 결합도: 직접 의존성 생성
class UserRepository {
final ApiClient apiClient = ApiClient();
final LocalStorage storage = LocalStorage();
Future<User> getUser(String id) async {
try {
final userData = await apiClient.get('/users/$id');
final user = User.fromJson(userData);
await storage.save('last_viewed_user', user);
return user;
} catch (e) {
throw Exception('사용자 정보를 가져오지 못했습니다');
}
}
}
// ✅ 낮은 결합도: 의존성 주입을 위한 클래스 설계
class UserRepository {
final ApiClient apiClient;
final StorageService storage;
UserRepository({
required this.apiClient,
required this.storage,
});
Future<User> getUser(String id) async {
try {
final userData = await apiClient.get('/users/$id');
final user = User.fromJson(userData);
await storage.save('last_viewed_user', user);
return user;
} catch (e) {
throw Exception('사용자 정보를 가져오지 못했습니다');
}
}
}
// ❌ 불완전한 의존성 주입: 사용 위치에서 의존성을 직접 생성
// 이 방식은 여전히 구체적인 구현체(ApiClient, SharedPrefsStorage)에 의존합니다
final repository = UserRepository(
apiClient: ApiClient(),
storage: SharedPrefsStorage(await SharedPreferences.getInstance()),
);
// ✅ 진정한 의존성 주입: 중앙 집중식 의존성 관리
// DI 컨테이너 설정 (예: 앱 시작 시점)
Future<void> setupDependencies() async {
// 필요한 의존성을 앱 시작 시점에 한 번만 생성
final sharedPrefs = await SharedPreferences.getInstance();
final storageService = SharedPrefsStorage(sharedPrefs);
final apiClient = ApiClient();
// 의존성 주입 컨테이너(GetIt) 등록
getIt.registerSingleton<StorageService>(storageService);
getIt.registerSingleton<ApiClient>(apiClient);
getIt.registerSingleton<UserRepository>(
UserRepository(
apiClient: apiClient,
storage: storageService,
),
);
}
// 사용 시점 (예: 위젯 내부)
Widget build(BuildContext context) {
// 미리 생성된 인스턴스를 가져옴 (구현 세부 사항 모름)
final userRepository = getIt<UserRepository>();
return FutureBuilder<User>(
future: userRepository.getUser('123'),
builder: (context, snapshot) {
// UI 구현...
},
);
}
의존성 주입의 장점:
구체적인 구현보다 인터페이스(추상 클래스)에 의존하는 방식으로 설계합니다.
// 인터페이스 정의
abstract class AuthService {
Future<User?> getCurrentUser();
Future<bool> signIn(String email, String password);
Future<void> signOut();
}
// 구현체 1: Firebase 인증
class FirebaseAuthService implements AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<User?> getCurrentUser() async {
final firebaseUser = _auth.currentUser;
if (firebaseUser == null) return null;
// Firebase 사용자를 앱 User 모델로 변환
return User(
id: firebaseUser.uid,
email: firebaseUser.email ?? '',
name: firebaseUser.displayName ?? '',
);
}
Future<bool> signIn(String email, String password) async {
try {
await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return true;
} catch (e) {
return false;
}
}
Future<void> signOut() => _auth.signOut();
}
// 구현체 2: 자체 백엔드 인증
class ApiAuthService implements AuthService {
final ApiClient _apiClient;
ApiAuthService(this._apiClient);
Future<User?> getCurrentUser() async {
try {
final token = await _getStoredToken();
if (token == null) return null;
final response = await _apiClient.get(
'/user/me',
headers: {'Authorization': 'Bearer $token'},
);
return User.fromJson(response);
} catch (e) {
return null;
}
}
// 나머지 구현...
}
// 사용하는 클래스는 구체적인 구현이 아닌 인터페이스에 의존
class AuthBloc {
final AuthService _authService;
AuthBloc(this._authService);
Future<void> checkAuthStatus() async {
final user = await _authService.getCurrentUser();
// 비즈니스 로직...
}
Future<bool> login(String email, String password) async {
return _authService.signIn(email, password);
}
}
이 방식의 장점:
직접적인 참조 대신 중앙 조정자를 통해 컴포넌트 간 통신을 구현합니다.
// 이벤트 버스 구현
class EventBus {
static final EventBus _instance = EventBus._internal();
factory EventBus() => _instance;
EventBus._internal();
final _controller = StreamController<Event>.broadcast();
Stream<T> on<T extends Event>() {
return _controller.stream.where((event) => event is T).cast<T>();
}
void fire(Event event) {
_controller.add(event);
}
void dispose() {
_controller.close();
}
}
// 이벤트 정의
abstract class Event {}
class UserLoggedInEvent extends Event {
final User user;
UserLoggedInEvent(this.user);
}
class CartUpdatedEvent extends Event {
final List<CartItem> items;
CartUpdatedEvent(this.items);
}
// 컴포넌트 A: 이벤트 발신자
class AuthService {
Future<void> login(String username, String password) async {
// 로그인 로직...
final user = User(id: '123', name: username);
EventBus().fire(UserLoggedInEvent(user));
}
}
// 컴포넌트 B: 이벤트 수신자 (완전히 분리됨)
class UserProfileWidget extends StatefulWidget {
_UserProfileWidgetState createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State<UserProfileWidget> {
User? user;
late StreamSubscription _subscription;
void initState() {
super.initState();
_subscription = EventBus().on<UserLoggedInEvent>().listen((event) {
setState(() {
user = event.user;
});
});
}
void dispose() {
_subscription.cancel();
super.dispose();
}
Widget build(BuildContext context) {
return user == null
? Text('로그인이 필요합니다')
: Text('안녕하세요, ${user!.name}님');
}
}
// 컴포넌트 C: 다른 이벤트 수신자
class CartIndicator extends StatefulWidget {
_CartIndicatorState createState() => _CartIndicatorState();
}
class _CartIndicatorState extends State<CartIndicator> {
int itemCount = 0;
late StreamSubscription _subscription;
void initState() {
super.initState();
_subscription = EventBus().on<CartUpdatedEvent>().listen((event) {
setState(() {
itemCount = event.items.length;
});
});
}
void dispose() {
_subscription.cancel();
super.dispose();
}
Widget build(BuildContext context) {
return Badge(
count: itemCount,
child: Icon(Icons.shopping_cart),
);
}
}
매개체 패턴의 장점:
서비스 로케이터는 필요한 서비스를 중앙에서 관리하고 제공하는 패턴입니다. Flutter에서는 GetIt과 같은 라이브러리를 사용하여 구현할 수 있습니다.
// GetIt 설정
final getIt = GetIt.instance;
void setupDependencies() {
// 싱글톤으로 등록
getIt.registerSingleton<ApiClient>(ApiClient());
getIt.registerSingleton<StorageService>(
SharedPrefsStorage(await SharedPreferences.getInstance()),
);
// 팩토리로 등록 (매번 새 인스턴스)
getIt.registerFactory<AuthService>(
() => FirebaseAuthService(),
);
// 지연 싱글톤 등록 (처음 사용할 때 초기화)
getIt.registerLazySingleton<UserRepository>(
() => UserRepository(
apiClient: getIt<ApiClient>(),
storage: getIt<StorageService>(),
),
);
}
// 사용 방법
class UserProfileScreen extends StatelessWidget {
Widget build(BuildContext context) {
final userRepository = getIt<UserRepository>();
return FutureBuilder<User>(
future: userRepository.getUser('123'),
builder: (context, snapshot) {
// UI 구현...
},
);
}
}
서비스 로케이터의 장점:
그러나 서비스 로케이터는 '서비스를 찾는' 의존성을 만들기 때문에, 순수한 의존성 주입보다 결합도가 약간 높다는 점을 인식해야 합니다.
더 큰 규모의 앱에서는 클린 아키텍처를 적용하여 시스템적으로 결합도를 관리할 수 있습니다. 앞서 첫 번째 글에서 소개한 레이어 분리를 통해 결합도를 체계적으로 관리합니다.
lib/
├── domain/ # 비즈니스 규칙 및 엔티티 (가장 안쪽 레이어)
│ ├── entities/ # 비즈니스 엔티티
│ ├── repositories/ # 저장소 인터페이스
│ └── usecases/ # 비즈니스 유스케이스
│
├── data/ # 데이터 관련 구현 (중간 레이어)
│ ├── datasources/ # 데이터 소스 구현
│ ├── models/ # DTO 모델
│ └── repositories/ # 저장소 구현
│
└── presentation/ # UI 및 상태 관리 (가장 바깥쪽 레이어)
├── pages/ # 화면 위젯
├── widgets/ # 재사용 가능한 위젯
└── blocs/ # 상태 관리
각 레이어는 안쪽 레이어에만 의존해야 하며, 바깥쪽 레이어는 안쪽 레이어로 직접 의존성을 갖지 않아야 합니다. 이는 의존성 규칙을 통해 결합도를 체계적으로 관리합니다.
예를 들어:
domain
레이어는 다른 어떤 레이어에도 의존하지 않습니다.data
레이어는 domain
레이어에만 의존합니다.presentation
레이어는 domain
레이어에만 의존합니다 (직접적으로 data
레이어에 의존하지 않음).이러한 구조는 핵심 비즈니스 로직을 UI나 데이터 소스 변경으로부터 보호합니다.
외부 API나 데이터베이스 모델을 앱 내부 모델로 변환하는 레이어를 만들어 외부 의존성으로부터 앱 로직을 보호합니다.
// 외부 API 응답 모델
class UserApiResponse {
final String id;
final String fullName;
final String emailAddress;
final String avatarUrl;
final Map<String, dynamic> metadata;
UserApiResponse({
required this.id,
required this.fullName,
required this.emailAddress,
required this.avatarUrl,
required this.metadata,
});
factory UserApiResponse.fromJson(Map<String, dynamic> json) {
return UserApiResponse(
id: json['user_id'],
fullName: json['name'],
emailAddress: json['email'],
avatarUrl: json['profile_picture'] ?? '',
metadata: json['additional_data'] ?? {},
);
}
}
// 앱 내부 도메인 모델
class User {
final String id;
final String name;
final String email;
final String? avatar;
User({
required this.id,
required this.name,
required this.email,
this.avatar,
});
}
// 변환 로직
extension UserMapper on UserApiResponse {
User toDomain() {
return User(
id: id,
name: fullName,
email: emailAddress,
avatar: avatarUrl.isNotEmpty ? avatarUrl : null,
);
}
}
// 사용 예시
class UserRepository {
final ApiClient _apiClient;
UserRepository(this._apiClient);
Future<User> getUser(String id) async {
final response = await _apiClient.get('/users/$id');
final apiUser = UserApiResponse.fromJson(response);
return apiUser.toDomain(); // 외부 모델을 내부 모델로 변환
}
}
이 접근 방식의 장점:
자주 변경될 가능성이 높은 코드(예: UI 레이아웃, API 호출, 외부 라이브러리 사용)를 격리하여 변경의 영향 범위를 제한합니다.
// 외부 라이브러리를 위한 어댑터 패턴 사용
abstract class AnalyticsService {
void logEvent(String name, {Map<String, dynamic>? parameters});
void setUserProperty(String name, String value);
void logScreenView(String screenName);
}
// Firebase 분석 구현
class FirebaseAnalyticsService implements AnalyticsService {
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
void logEvent(String name, {Map<String, dynamic>? parameters}) {
_analytics.logEvent(name: name, parameters: parameters);
}
void setUserProperty(String name, String value) {
_analytics.setUserProperty(name: name, value: value);
}
void logScreenView(String screenName) {
_analytics.logScreenView(screenName: screenName);
}
}
// 가짜 분석 구현 (개발/테스트용)
class FakeAnalyticsService implements AnalyticsService {
void logEvent(String name, {Map<String, dynamic>? parameters}) {
debugPrint('📊 Event: $name, Parameters: $parameters');
}
void setUserProperty(String name, String value) {
debugPrint('📊 User Property: $name = $value');
}
void logScreenView(String screenName) {
debugPrint('📊 Screen View: $screenName');
}
}
어댑터 패턴을 사용하면 분석 제공업체를 변경하거나, 다른 분석 옵션을 추가하거나, 테스트 중에 분석을 비활성화하기가 훨씬 쉬워집니다.
Flutter UI 결합도를 낮추기 위해 위젯을 적절히 캡슐화하고 명확한 인터페이스를 정의합니다.
// ❌ 높은 결합도: 너무 많은 책임과 세부 구현 노출
class ProductCard extends StatelessWidget {
final Product product;
final CartBloc cartBloc;
final FavoritesBloc favoritesBloc;
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network(product.imageUrl),
Text(product.name),
Text('\$${product.price}'),
Row(
children: [
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {
// 블록 직접 사용
favoritesBloc.add(ToggleFavoriteEvent(product.id));
},
),
ElevatedButton(
onPressed: () {
// 장바구니 추가 로직 직접 구현
cartBloc.add(AddToCartEvent(product));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} 추가됨')),
);
},
child: Text('장바구니에 추가'),
),
],
),
],
),
);
}
}
// ✅ 낮은 결합도: 콜백을 통한 상호작용, 상세 구현 은닉
class ProductCard extends StatelessWidget {
final String name;
final String imageUrl;
final double price;
final bool isFavorite;
final VoidCallback onFavoriteToggle;
final VoidCallback onAddToCart;
const ProductCard({
Key? key,
required this.name,
required this.imageUrl,
required this.price,
required this.isFavorite,
required this.onFavoriteToggle,
required this.onAddToCart,
}) : super(key: key);
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network(imageUrl),
Text(name),
Text('\$${price.toStringAsFixed(2)}'),
Row(
children: [
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: onFavoriteToggle,
),
ElevatedButton(
onPressed: onAddToCart,
child: Text('장바구니에 추가'),
),
],
),
],
),
);
}
}
// 사용 예시
BlocBuilder<FavoritesBloc, FavoritesState>(
builder: (context, favoritesState) {
return BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, productsState) {
return ListView.builder(
itemCount: productsState.products.length,
itemBuilder: (context, index) {
final product = productsState.products[index];
final isFavorite = favoritesState.favoriteIds.contains(product.id);
return ProductCard(
name: product.name,
imageUrl: product.imageUrl,
price: product.price,
isFavorite: isFavorite,
onFavoriteToggle: () {
context.read<FavoritesBloc>().add(
ToggleFavoriteEvent(product.id),
);
},
onAddToCart: () {
context.read<CartBloc>().add(
AddToCartEvent(product),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} 추가됨')),
);
},
);
},
);
},
);
},
)
이 접근 방식의 장점:
ProductCard
위젯은 특정 상태 관리 솔루션에 의존하지 않습니다상태 관리 라이브러리나 라우팅 시스템에 대한 직접적인 의존성을 추상화 레이어 뒤에 숨깁니다.
// 라우팅 추상화
abstract class AppNavigator {
void navigateToProductDetails(String productId);
void navigateToCart();
void navigateToCheckout();
void pop();
Future<T?> showDialog<T>(Widget dialog);
}
// Navigator 2.0 구현
class FlutterAppNavigator implements AppNavigator {
final GlobalKey<NavigatorState> navigatorKey;
FlutterAppNavigator(this.navigatorKey);
NavigatorState get _navigator => navigatorKey.currentState!;
void navigateToProductDetails(String productId) {
_navigator.pushNamed('/product/$productId');
}
void navigateToCart() {
_navigator.pushNamed('/cart');
}
void navigateToCheckout() {
_navigator.pushNamed('/checkout');
}
void pop() {
_navigator.pop();
}
Future<T?> showDialog<T>(Widget dialog) {
return showDialog<T>(
context: _navigator.context,
builder: (context) => dialog,
);
}
}
// 비즈니스 로직에서 사용
class ProductDetailController {
final AppNavigator navigator;
final CartRepository cartRepository;
ProductDetailController({
required this.navigator,
required this.cartRepository,
});
void addToCartAndNavigate(Product product) async {
await cartRepository.addToCart(product);
navigator.navigateToCart();
}
}
이 접근 방식의 장점:
코드베이스의 결합도를 체계적으로 개선하려면 현재 상태를 측정하고 점진적으로 개선해야 합니다.
의존성 그래프 분석: 주요 클래스와 모듈 간의 의존성 그래프를 그려 "핫스팟"(과도한 의존성이 있는 곳)을 식별합니다.
변경 영향 평가: 특정 클래스나 모듈을 변경할 때 영향을 받는 다른 부분을 추적합니다. 영향이 광범위하다면 결합도가 높다는 신호입니다.
코드 리뷰 체크리스트: 다음과 같은 질문을 통해 결합도 문제를 식별합니다.
대부분의 프로젝트에서는 결합도 문제를 한 번에 모두 해결하기 어렵습니다. 다음과 같은 점진적 접근법을 고려하세요:
새 코드에 대한 높은 기준 적용: 신규 코드는 낮은 결합도 패턴을 철저히 따르도록 합니다.
리팩토링 우선순위 지정: 가장 자주 변경되거나 문제가 많은 영역부터 리팩토링을 시작합니다.
인터페이스 도입: 높은 결합도를 가진 클래스의 인터페이스를 추출하여 점진적으로 의존성을 인터페이스로 전환합니다.
어댑터 패턴 활용: 기존 코드를 바로 변경하기 어렵다면, 어댑터 패턴을 사용하여 새 인터페이스에 맞게 조정합니다.
지속적인 교육: 팀 전체가 낮은 결합도의 중요성을 이해하고 관련 패턴을 적용할 수 있도록 지속적으로 교육합니다.
과도한 추상화와 지나친 단순함 사이의 균형을 찾는 것이 중요합니다. 다음 지침이 도움이 될 수 있습니다:
// API 클라이언트 추상화 예시
abstract class UserApi {
Future<Map<String, dynamic>> getUser(String id);
Future<List<Map<String, dynamic>>> getUserFriends(String id);
}
// 현재 API 버전 구현
class UserApiV1 implements UserApi {
final Dio dio;
UserApiV1(this.dio);
Future<Map<String, dynamic>> getUser(String id) async {
final response = await dio.get('/v1/users/$id');
return response.data;
}
Future<List<Map<String, dynamic>>> getUserFriends(String id) async {
final response = await dio.get('/v1/users/$id/friends');
return List<Map<String, dynamic>>.from(response.data);
}
}
// 테스트 가능한 저장소 추상화
abstract class AuthRepository {
Future<User?> getCurrentUser();
Future<bool> signIn(String email, String password);
}
// 비즈니스 로직 클래스는 구체적인 구현체가 아닌 인터페이스에 의존
class AuthViewModel {
final AuthRepository _repository;
AuthViewModel(this._repository);
Future<bool> login(String email, String password) async {
// 실제 구현이나 모의 객체 모두 사용 가능
return _repository.signIn(email, password);
}
}
// 테스트 코드
test('로그인 성공 시 true를 반환해야 함', () async {
// given
final mockRepository = MockAuthRepository();
when(mockRepository.signIn(any, any))
.thenAnswer((_) async => true);
final viewModel = AuthViewModel(mockRepository);
// when
final result = await viewModel.login('test@example.com', 'password');
// then
expect(result, isTrue);
});
추상화는 코드 복잡성을 증가시키므로 모든 상황에서 필요하지는 않습니다. 다음과 같은 경우에는 추상화를 피하는 것이 좋습니다:
// ❌ 과도한 추상화: 단순 데이터 처리에 불필요한 복잡성 추가
abstract class UserPreferences {
Future<void> saveUsername(String username);
Future<String?> getUsername();
Future<void> clearUsername();
}
class SharedPrefsUserPreferences implements UserPreferences {
final SharedPreferences _prefs;
SharedPrefsUserPreferences(this._prefs);
Future<void> saveUsername(String username) async {
await _prefs.setString('username', username);
}
Future<String?> getUsername() async {
return _prefs.getString('username');
}
Future<void> clearUsername() async {
await _prefs.remove('username');
}
}
// ✅ 단순한 접근: 간단한 작업에 더 적합
class UserPrefs {
final SharedPreferences _prefs;
UserPrefs(this._prefs);
Future<void> saveUsername(String username) async {
await _prefs.setString('username', username);
}
Future<String?> getUsername() {
return Future.value(_prefs.getString('username'));
}
Future<void> clearUsername() async {
await _prefs.remove('username');
}
}
// ❌ 불필요한 추상화: Flutter의 안정적인 API에 대한 과도한 래핑
abstract class DialogService {
Future<bool?> showConfirmationDialog(String title, String message);
}
class FlutterDialogService implements DialogService {
final BuildContext context;
FlutterDialogService(this.context);
Future<bool?> showConfirmationDialog(String title, String message) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('확인'),
),
],
),
);
}
}
// ✅ 더 단순한 접근: 안정적인 Flutter API는 직접 사용
Future<void> confirmAndDeleteAccount(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('계정 삭제'),
content: const Text('정말 계정을 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('삭제'),
),
],
),
);
if (confirmed == true) {
// 계정 삭제 로직...
}
}
// ❌ 소규모 프로젝트에서 과도한 아키텍처
// 복잡한 클린 아키텍처 구현
class GetUserUseCase implements UseCase<User, GetUserParams> {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<Either<Failure, User>> call(GetUserParams params) {
return repository.getUser(params.userId);
}
}
class GetUserParams extends Equatable {
final String userId;
GetUserParams(this.userId);
List<Object> get props => [userId];
}
// 호출 코드
final result = await getUserUseCase(GetUserParams('123'));
result.fold(
(failure) => handleError(failure),
(user) => showUser(user),
);
// ✅ 소규모 프로젝트에 더 적합한 단순한 접근
class UserService {
final http.Client client;
UserService(this.client);
Future<User> getUser(String id) async {
try {
final response = await client.get(Uri.parse('https://api.example.com/users/$id'));
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('사용자를 가져오지 못했습니다');
}
} catch (e) {
rethrow;
}
}
}
// 호출 코드
try {
final user = await userService.getUser('123');
showUser(user);
} catch (e) {
handleError(e);
}
점진적 추상화: 처음부터 모든 것을 추상화하지 말고, 필요에 따라 추상화 레이어를 추가합니다.
YAGNI(You Aren't Gonna Need It) 원칙: 정말 필요할 때까지 추상화를 미룹니다.
코드 중복 허용: 때로는 약간의 코드 중복이 과도한 추상화보다 나을 수 있습니다.
팀 역량 고려: 팀의 기술적 역량과 프로젝트 마감일에 맞는 수준의 추상화를 선택합니다.
낮은 결합도는 장기적으로 유지보수 가능하고 확장 가능한 Flutter 앱의 핵심입니다. 이 글에서 살펴본 다양한 패턴과 기법을 적용하면 다음과 같은 이점을 얻을 수 있습니다:
결합도 관리는 단순한 기술적 선택을 넘어, 장기적으로 개발 생산성과 코드 품질에 영향을 미치는 아키텍처 원칙입니다. 프로젝트의 복잡성과 팀의 역량에 맞게 적절한 수준의 추상화와 결합도 관리 전략을 선택하세요.
다음 글에서는 Flutter 앱의 상태 관리 패턴에 대해 더 자세히 알아보겠습니다.
다음 포스트: Flutter 개발 가이드라인: 효과적인 상태 관리 패턴
Flutter에서 사용할 수 있는 다양한 상태 관리 솔루션의 장단점과 적합한 사용 사례를 자세히 알아봅니다.