From e051bd451a6755293a89670227ffe811b282866f Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 30 Mar 2026 19:41:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(arch):=20GetIt=20DI=20+=20IIAPService/IAdS?= =?UTF-8?q?ervice=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/di/: service_locator, IIAPService, IAdService 생성 - IAPService/AdService: implements 인터페이스 + GetIt 위임 - main.dart: setupServiceLocator() 호출 - 기존 .instance getter 호환성 100% 유지 - test/helpers/test_setup.dart: 테스트용 서비스 로케이터 초기화 --- lib/main.dart | 3 ++ lib/src/core/di/i_ad_service.dart | 31 ++++++++++++++++++ lib/src/core/di/i_iap_service.dart | 34 ++++++++++++++++++++ lib/src/core/di/service_locator.dart | 21 ++++++++++++ lib/src/core/engine/ad_service.dart | 21 ++++++++---- lib/src/core/engine/iap_service.dart | 24 ++++++++++---- pubspec.lock | 8 +++++ pubspec.yaml | 2 ++ test/features/new_character_screen_test.dart | 6 ++++ test/helpers/test_setup.dart | 12 +++++++ test/widget_test.dart | 3 +- 11 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 lib/src/core/di/i_ad_service.dart create mode 100644 lib/src/core/di/i_iap_service.dart create mode 100644 lib/src/core/di/service_locator.dart diff --git a/lib/main.dart b/lib/main.dart index cdf87cd..365489a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,9 @@ import 'package:asciineverdie/src/app.dart'; +import 'package:asciineverdie/src/core/di/service_locator.dart'; import 'package:flutter/material.dart'; void main() { + // 서비스 로케이터(service locator) 초기화 + setupServiceLocator(); runApp(const AskiiNeverDieApp()); } diff --git a/lib/src/core/di/i_ad_service.dart b/lib/src/core/di/i_ad_service.dart new file mode 100644 index 0000000..fdc3fcb --- /dev/null +++ b/lib/src/core/di/i_ad_service.dart @@ -0,0 +1,31 @@ +import 'package:asciineverdie/src/core/engine/ad_service.dart'; + +/// 광고 서비스 인터페이스 (interface) +/// +/// 광고 표시 기능의 추상화 계층. +/// 테스트 시 목(mock) 구현체로 교체 가능. +abstract class IAdService { + /// AdMob SDK 초기화 + Future initialize(); + + /// 리워드 광고(rewarded ad) 준비 여부 + bool get isRewardedAdReady; + + /// 인터스티셜 광고(interstitial ad) 준비 여부 + bool get isInterstitialAdReady; + + /// 리워드 광고 표시 + Future showRewardedAd({ + required AdType adType, + required void Function() onRewarded, + }); + + /// 인터스티셜 광고 표시 + Future showInterstitialAd({ + required AdType adType, + required void Function() onComplete, + }); + + /// 리소스 해제 + void dispose(); +} diff --git a/lib/src/core/di/i_iap_service.dart b/lib/src/core/di/i_iap_service.dart new file mode 100644 index 0000000..8fe6114 --- /dev/null +++ b/lib/src/core/di/i_iap_service.dart @@ -0,0 +1,34 @@ +import 'package:asciineverdie/src/core/engine/iap_service.dart'; + +/// IAP 서비스 인터페이스 (interface) +/// +/// 인앱 구매 기능의 추상화 계층. +/// 테스트 시 목(mock) 구현체로 교체 가능. +abstract class IIAPService { + /// IAP 서비스 초기화 + Future initialize(); + + /// 광고 제거 구매 여부 + bool get isAdRemovalPurchased; + + /// 스토어 가용성(store availability) + bool get isStoreAvailable; + + /// 광고 제거 상품 가격 문자열 + String get removeAdsPrice; + + /// 광고 제거 구매 + Future purchaseRemoveAds(); + + /// 구매 복원(restore purchases) + Future restorePurchases(); + + /// 디버그 모드 IAP 시뮬레이션(debug simulation) 활성화 여부 + bool get debugIAPSimulated; + + /// 디버그 모드 IAP 시뮬레이션 토글 + set debugIAPSimulated(bool value); + + /// 리소스 해제 + void dispose(); +} diff --git a/lib/src/core/di/service_locator.dart b/lib/src/core/di/service_locator.dart new file mode 100644 index 0000000..14e6441 --- /dev/null +++ b/lib/src/core/di/service_locator.dart @@ -0,0 +1,21 @@ +import 'package:get_it/get_it.dart'; + +import 'package:asciineverdie/src/core/di/i_ad_service.dart'; +import 'package:asciineverdie/src/core/di/i_iap_service.dart'; +import 'package:asciineverdie/src/core/engine/ad_service.dart'; +import 'package:asciineverdie/src/core/engine/iap_service.dart'; + +/// 전역 서비스 로케이터(service locator) 인스턴스 +final GetIt sl = GetIt.instance; + +/// 서비스 로케이터 초기화 +/// +/// 앱 시작 시 한 번 호출하여 모든 서비스를 등록합니다. +/// 점진적 도입: IAPService, AdService만 먼저 등록. +void setupServiceLocator() { + // IAP 서비스 (싱글톤 등록) + sl.registerLazySingleton(() => IAPService.createInstance()); + + // 광고 서비스 (싱글톤 등록) + sl.registerLazySingleton(() => AdService.createInstance()); +} diff --git a/lib/src/core/engine/ad_service.dart b/lib/src/core/engine/ad_service.dart index e2a3f19..6aced96 100644 --- a/lib/src/core/engine/ad_service.dart +++ b/lib/src/core/engine/ad_service.dart @@ -5,6 +5,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:asciineverdie/src/core/di/i_ad_service.dart'; import 'package:asciineverdie/src/core/engine/iap_service.dart'; /// 광고 타입 @@ -44,16 +47,14 @@ enum AdResult { /// /// AdMob 리워드/인터스티셜 광고 로드 및 표시를 관리합니다. /// 디버그 모드에서는 광고 ON/OFF 토글이 가능합니다. -class AdService { +class AdService implements IAdService { AdService._(); - static AdService? _instance; + /// GetIt 등록용 팩토리 메서드 + factory AdService.createInstance() => AdService._(); - /// 싱글톤 인스턴스 - static AdService get instance { - _instance ??= AdService._(); - return _instance!; - } + /// 싱글톤 인스턴스 (GetIt 서비스 로케이터에 위임) + static IAdService get instance => GetIt.instance(); // =========================================================================== // 광고 단위 ID @@ -126,6 +127,7 @@ class AdService { // =========================================================================== /// AdMob SDK 초기화 + @override Future initialize() async { if (_isInitialized) return; @@ -197,6 +199,7 @@ class AdService { } /// 리워드 광고 준비 여부 + @override bool get isRewardedAdReady => _rewardedAd != null || _shouldSkipAd; /// 리워드 광고 표시 @@ -204,6 +207,7 @@ class AdService { /// [adType] 광고 타입 (로깅용) /// [onRewarded] 보상 지급 콜백 /// Returns: 광고 결과 + @override Future showRewardedAd({ required AdType adType, required void Function() onRewarded, @@ -308,6 +312,7 @@ class AdService { } /// 인터스티셜 광고 준비 여부 + @override bool get isInterstitialAdReady => _interstitialAd != null || _shouldSkipAd; /// 인터스티셜 광고 표시 @@ -315,6 +320,7 @@ class AdService { /// [adType] 광고 타입 (로깅용) /// [onComplete] 광고 완료 콜백 (보상 지급) /// Returns: 광고 결과 + @override Future showInterstitialAd({ required AdType adType, required void Function() onComplete, @@ -386,6 +392,7 @@ class AdService { // =========================================================================== /// 리소스 해제 + @override void dispose() { _rewardedAd?.dispose(); _rewardedAd = null; diff --git a/lib/src/core/engine/iap_service.dart b/lib/src/core/engine/iap_service.dart index a51b66b..80f94aa 100644 --- a/lib/src/core/engine/iap_service.dart +++ b/lib/src/core/engine/iap_service.dart @@ -12,7 +12,10 @@ import 'package:pointycastle/asn1/primitives/asn1_bit_string.dart'; import 'package:pointycastle/asn1/primitives/asn1_integer.dart'; import 'package:pointycastle/asn1/primitives/asn1_sequence.dart'; +import 'package:get_it/get_it.dart'; + import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; +import 'package:asciineverdie/src/core/di/i_iap_service.dart'; /// IAP 상품 ID class IAPProductIds { @@ -62,16 +65,14 @@ String get _googlePlayPublicKey => _gpKeyPart1 + _gpKeyPart2; /// /// 인앱 구매 (광고 제거) 처리를 담당합니다. /// flutter_secure_storage를 사용하여 구매 상태를 보안 저장합니다. -class IAPService { +class IAPService implements IIAPService { IAPService._(); - static IAPService? _instance; + /// GetIt 등록용 팩토리 메서드 + factory IAPService.createInstance() => IAPService._(); - /// 싱글톤 인스턴스 - static IAPService get instance { - _instance ??= IAPService._(); - return _instance!; - } + /// 싱글톤 인스턴스 (GetIt 서비스 로케이터에 위임) + static IIAPService get instance => GetIt.instance(); // =========================================================================== // 상수 @@ -109,6 +110,7 @@ class IAPService { // =========================================================================== /// IAP 서비스 초기화 + @override Future initialize() async { if (_isInitialized) return; @@ -201,9 +203,11 @@ class IAPService { // =========================================================================== /// 디버그 모드 IAP 시뮬레이션 활성화 여부 + @override bool get debugIAPSimulated => _debugIAPSimulated; /// 디버그 모드 IAP 시뮬레이션 토글 + @override set debugIAPSimulated(bool value) { _debugIAPSimulated = value; if (kDebugMode) { @@ -217,18 +221,21 @@ class IAPService { // =========================================================================== /// 광고 제거 구매 여부 + @override bool get isAdRemovalPurchased { if (kDebugMode && _debugIAPSimulated) return true; return _adRemovalPurchased; } /// 스토어 가용성 + @override bool get isStoreAvailable => _isAvailable; /// 광고 제거 상품 정보 ProductDetails? get removeAdsProduct => _removeAdsProduct; /// 광고 제거 상품 가격 문자열 + @override String get removeAdsPrice { if (_removeAdsProduct != null) { return _removeAdsProduct!.price; @@ -246,6 +253,7 @@ class IAPService { // =========================================================================== /// 광고 제거 구매 + @override Future purchaseRemoveAds() async { // 디버그 모드 시뮬레이션 if (kDebugMode && _debugIAPSimulated) { @@ -290,6 +298,7 @@ class IAPService { // =========================================================================== /// 구매 복원 + @override Future restorePurchases() async { // 디버그 모드 시뮬레이션 if (kDebugMode && _debugIAPSimulated) { @@ -489,6 +498,7 @@ class IAPService { // =========================================================================== /// 리소스 해제 + @override void dispose() { _subscription?.cancel(); _subscription = null; diff --git a/pubspec.lock b/pubspec.lock index 5870d2c..54d16e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -333,6 +333,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 + url: "https://pub.dev" + source: hosted + version: "8.3.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f6a264f..177c48e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: pointycastle: ^3.9.1 # 앱 버전 정보 package_info_plus: ^8.3.0 + # 의존성 주입(Dependency Injection) 서비스 로케이터 + get_it: ^8.0.3 dev_dependencies: flutter_test: diff --git a/test/features/new_character_screen_test.dart b/test/features/new_character_screen_test.dart index dc8249b..c6bf9bd 100644 --- a/test/features/new_character_screen_test.dart +++ b/test/features/new_character_screen_test.dart @@ -4,6 +4,8 @@ import 'package:asciineverdie/src/features/new_character/new_character_screen.da import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../helpers/test_setup.dart'; + /// 테스트용 MaterialApp 래퍼 (localization 포함) /// locale을 영어로 고정하여 테스트 텍스트와 일치시킴 Widget _buildTestApp(Widget child) { @@ -16,6 +18,10 @@ Widget _buildTestApp(Widget child) { } void main() { + // 서비스 로케이터(service locator) 초기화 + setUpAll(() { + TestSetup.ensureServiceLocator(); + }); testWidgets('NewCharacterScreen renders main sections', (tester) async { await tester.pumpWidget( _buildTestApp( diff --git a/test/helpers/test_setup.dart b/test/helpers/test_setup.dart index 306675b..02dbe30 100644 --- a/test/helpers/test_setup.dart +++ b/test/helpers/test_setup.dart @@ -1,6 +1,9 @@ import 'package:asciineverdie/src/core/audio/audio_service.dart'; +import 'package:asciineverdie/src/core/di/i_iap_service.dart'; +import 'package:asciineverdie/src/core/di/service_locator.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; /// 위젯 테스트에서 사용하는 공통 셋업/정리 유틸리티 /// @@ -22,6 +25,15 @@ class TestSetup { ); } + /// 서비스 로케이터(service locator) 초기화 + /// + /// 테스트에서 GetIt 의존성이 필요한 경우 setUpAll에서 호출 + static void ensureServiceLocator() { + if (!GetIt.instance.isRegistered()) { + setupServiceLocator(); + } + } + /// 모든 싱글톤 서비스 정리 /// /// tearDown에서 호출하여 타이머 및 리소스 정리 diff --git a/test/widget_test.dart b/test/widget_test.dart index 22ed035..8b2867e 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -4,9 +4,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'helpers/test_setup.dart'; void main() { - // SharedPreferences 모킹 + // SharedPreferences 모킹 및 서비스 로케이터 초기화 setUpAll(() { TestSetup.mockSharedPreferences(); + TestSetup.ensureServiceLocator(); }); // 각 테스트 후 싱글톤 서비스 정리 (타이머 누수 방지)