Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
excluded: # 린트 과정에서 무시할 파일 경로. `included`보다 우선순위 높음
- Projects/App/Sources/AppDelegate.swift
- Projects/App/Sources/AppDelegate+Register.swift
- Projects/App/Sources/SceneDelegate.swift
- Projects/App/Tests/**
- Projects/Core/Sources/Extension/String+.swift
Expand Down Expand Up @@ -29,7 +30,7 @@ file_length:
warning: 1000
error: 2000
line_length:
warning: 80
warning: 90
error: 400
disabled_rules: # 제외하고 싶은 룰
- trailing_whitespace
Expand Down

This file was deleted.

111 changes: 24 additions & 87 deletions Projects/App/Sources/AppDelegate+Register.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,92 +17,29 @@ import FirebaseModule

extension AppDelegate {
func registerDependencies() {
let coreDataService: CoreDataService = DefaultCoreDataService()
let networkService: NetworkService = DefaultNetworkService()
let locationService: LocationService = DefaultLocationService()

let favoritesRepository: FavoritesRepository
= DefaultFavoritesRepository(
coreDataService: coreDataService,
networkService: networkService
)
let busStopArrivalInfoRepository: BusStopArrivalInfoRepository
= DefaultBusStopArrivalInfoRepository(networkService: networkService)
let stationListRepository: StationListRepository
= DefaultStationListRepository()
let regularAlarmRepository: RegularAlarmRepository
= DefaultRegularAlarmRepository(
coreDataService: coreDataService,
networkService: networkService
)
let localNotificationService: LocalNotificationService
= DefaultLocalNotificationService()
let regularAlarmEditingService: RegularAlarmEditingService
= DefaultRegularAlarmEditingService()
// TODO: 추후 의존 주입 형태 변경
// let versionCheckRepository: VersionCheckRepository
// = DefaultVersionCheckRepository(networkService: networkService)

DIContainer.register(
type: FavoritesUseCase.self,
DefaultFavoritesUseCase(
busStopArrivalInfoRepository: busStopArrivalInfoRepository,
favoritesRepository: favoritesRepository
)
)

DIContainer.register(
type: RegularAlarmUseCase.self,
DefaultRegularAlarmUseCase(
localNotificationService: localNotificationService,
regularAlarmRepository: regularAlarmRepository
)
)

DIContainer.register(
type: AddRegularAlarmUseCase.self,
DefaultAddRegularAlarmUseCase(
localNotificationService: localNotificationService,
regularAlarmRepository: regularAlarmRepository
)
)

DIContainer.register(
type: SearchUseCase.self,
DefaultSearchUseCase(
stationListRepository: stationListRepository,
locationService: locationService
)
)

DIContainer.register(
type: BusStopUseCase.self,
DefaultBusStopUseCase(
busStopArrivalInfoRepository: busStopArrivalInfoRepository,
favoritesRepository: favoritesRepository,
regularAlarmEditingService: regularAlarmEditingService
)
)

DIContainer.register(
type: NearMapUseCase.self,
DefaultNearMapUseCase(
stationListRepository: stationListRepository,
locationService: locationService
)
)

DIContainer.register(
type: RegularAlarmEditingService.self,
regularAlarmEditingService
)

// DIContainer.register(
// type: VersionCheckUseCase.self,
// DefaultVersionCheckUseCase(
// versionCheckRepository: versionCheckRepository
// )
// )
DIContainer.register(type: FirebaseLogger.self, FirebaseLoggerImpl())
let firebaseLogger = FirebaseLoggerImpl()
DIContainer.setLogger(firebaseLogger)

DIContainer.register(type: ForceUpdateService.self, DefaultForceUpdateService())
DIContainer.register(type: CoreDataService.self, DefaultCoreDataService())
DIContainer.register(type: NetworkService.self, DefaultNetworkService())
DIContainer.register(type: LocationService.self, DefaultLocationService())

DIContainer.register(type: FavoritesRepository.self, DefaultFavoritesRepository())
DIContainer.register(type: BusStopArrivalInfoRepository.self, DefaultBusStopArrivalInfoRepository())
DIContainer.register(type: StationListRepository.self, DefaultStationListRepository())
DIContainer.register(type: RegularAlarmRepository.self, DefaultRegularAlarmRepository())
DIContainer.register(type: LocalNotificationService.self, DefaultLocalNotificationService())
DIContainer.register(type: RegularAlarmEditingService.self, DefaultRegularAlarmEditingService())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복 등록으로 이전 인스턴스가 은폐됩니다.

RegularAlarmEditingService가 두 번 등록되고 있습니다. 두 번째 호출이 첫 번째 인스턴스를 덮어쓰므로, 불필요한 객체 생성 + 디버깅 난이도 상승 위험이 있습니다. 실제로 서로 다른 구현체를 주입하려는 의도가 아니라면 한 번만 등록하도록 수정하세요.

-        DIContainer.register(type: RegularAlarmEditingService.self, DefaultRegularAlarmEditingService())
...
-        DIContainer.register(type: RegularAlarmEditingService.self, DefaultRegularAlarmEditingService())

Also applies to: 38-38

🤖 Prompt for AI Agents
In Projects/App/Sources/AppDelegate+Register.swift at lines 28 and 38, the
RegularAlarmEditingService is registered twice, causing the second registration
to overwrite the first and potentially creating unnecessary instances. To fix
this, remove one of the duplicate registrations so that
RegularAlarmEditingService is registered only once, ensuring no redundant object
creation and clearer dependency injection.

DIContainer.register(type: VersionCheckRepository.self, DefaultVersionCheckRepository())

DIContainer.register(type: FavoritesUseCase.self, DefaultFavoritesUseCase())
DIContainer.register(type: RegularAlarmUseCase.self, DefaultRegularAlarmUseCase())
DIContainer.register(type: AddRegularAlarmUseCase.self, DefaultAddRegularAlarmUseCase())
DIContainer.register(type: SearchUseCase.self, DefaultSearchUseCase())
DIContainer.register(type: BusStopUseCase.self, DefaultBusStopUseCase())
DIContainer.register(type: NearMapUseCase.self, DefaultNearMapUseCase())
DIContainer.register(type: FirebaseLogger.self, firebaseLogger)
DIContainer.register(type: VersionCheckUseCase.self, DefaultVersionCheckUseCase())
}
}
10 changes: 2 additions & 8 deletions Projects/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,14 @@ import Data
import RxSwift

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@Injected private var useCase: VersionCheckUseCase

var window: UIWindow?
var appCoordinator: AppCoordinator?
var deeplinkHandler: DeeplinkHandler?

let disposeBag = DisposeBag()

// MARK: 추후 구체타입이 아닌 형태로 변경
private var useCase: VersionCheckUseCase
= DefaultVersionCheckUseCase(
versionCheckRepository: DefaultVersionCheckRepository(
networkService: DefaultNetworkService()
),
forceUpdateService: DefaultForceUpdateService()
)

func scene(
_ scene: UIScene,
Expand Down
32 changes: 23 additions & 9 deletions Projects/Core/Sources/DIContainer/DIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@

import Foundation

public final class DIContainer {
static var storage: [String: Any] = [:]

private init() { }
import FirebaseInterface

public enum DIContainer {
private static var storage: [String: Any] = [:]
private static var lock: NSLock = .init()
private static var firebaseLogger: FirebaseLogger?
public static func setLogger(_ logger: FirebaseLogger) {
firebaseLogger = logger
}

public static func register<T>(type: T.Type, _ object: T) {
storage["\(type)"] = object
public static func register<Dependency>(
type: Dependency.Type,
_ instance: Dependency
) {
lock.lock()
defer { lock.unlock() }
storage[String(describing: Dependency.self)] = instance
Comment on lines +25 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

중복 등록 감지 로직이 없습니다.

동일한 type이 이미 존재할 때 무조건 덮어쓰고 있는데, 이는 실수로 잘못된 인스턴스를 주입해도 즉시 알 수 없습니다. 기존 값이 존재하면 assertionFailure나 로그 경고를 남겨 개발 단계에서 문제를 조기에 발견하도록 하는 방안을 고려해 주세요.

-        storage[String(describing: Dependency.self)] = instance
+        let key = String(describing: Dependency.self)
+        #if DEBUG
+        if storage[key] != nil {
+            assertionFailure("DIContainer: '\(key)' 가 이미 등록되어 있습니다. 중복 등록을 확인하세요.")
+        }
+        #endif
+        storage[key] = instance
🤖 Prompt for AI Agents
In Projects/Core/Sources/DIContainer/DIContainer.swift around lines 21 to 23,
the code overwrites existing instances in storage without checking for
duplicates. Add a check before assigning to storage to see if an instance for
the given type already exists. If it does, trigger an assertionFailure or log a
warning to alert developers during development that a duplicate registration is
occurring, preventing silent overwrites.

}

static func resolve<T>(type: T.Type) -> T {
guard let object = storage["\(type)"] as? T else {
static func resolve<Dependency>(type: Dependency.Type) -> Dependency {
let typeName = String(describing: Dependency.self)
lock.lock()
defer { lock.unlock() }
guard let savedInstance = storage[typeName] as? Dependency else {
firebaseLogger?.log(name: "DependencyCrash", parameter: ["type": typeName])
fatalError("register 되지 않은 객체 호출: \(type)")
}
return object
return savedInstance
}
}
12 changes: 4 additions & 8 deletions Projects/Core/Sources/DIContainer/Injected.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@
import Foundation

@propertyWrapper
public struct Injected<T> {
private var type: T.Type

public var wrappedValue: T {
DIContainer.resolve(type: type)
public struct Injected<Dependency> {
public var wrappedValue: Dependency {
DIContainer.resolve(type: Dependency.self)
}

public init(_ type: T.Type) {
self.type = type
}
public init() { }
}
Comment on lines +12 to 18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Injected 래퍼가 매 접근마다 resolve 호출 → 캐싱으로 비용·락 횟수 최소화 제안

현재 구현은 프로퍼티 접근마다 DIContainer.resolve 를 재호출합니다.
락 횟수 및 딕셔너리 조회 오버헤드는 미미할 수 있으나, 다수의 Rx 체인에서 자주 접근될 경우 의미 있는 비용이 됩니다. 한 번만 해석-캐싱하도록 아래와 같이 개선을 제안합니다.

 @propertyWrapper
 public struct Injected<Dependency> {
-    public var wrappedValue: Dependency {
-        DIContainer.resolve(type: Dependency.self)
-    }
+    // 최초 1회만 DIContainer 에서 해석
+    private lazy var cached: Dependency = {
+        DIContainer.resolve(type: Dependency.self)
+    }()
+
+    public var wrappedValue: Dependency { cached }
 
     public init() { }
 }

장점
• 불필요한 락 경합 감소
• 의존성 인스턴스가 단일화되어 테스트 시 예측 가능성 ↑
• 기존 사용처 변경 없음

단점
• 의도적으로 매번 새로운 인스턴스를 받아야 하는 특수 케이스가 있으면 별도 래퍼가 필요

검토 부탁드립니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public struct Injected<Dependency> {
public var wrappedValue: Dependency {
DIContainer.resolve(type: Dependency.self)
}
public init(_ type: T.Type) {
self.type = type
}
public init() { }
}
@propertyWrapper
public struct Injected<Dependency> {
// 최초 1회만 DIContainer 에서 해석
private lazy var cached: Dependency = {
DIContainer.resolve(type: Dependency.self)
}()
public var wrappedValue: Dependency { cached }
public init() { }
}
🤖 Prompt for AI Agents
In Projects/Core/Sources/DIContainer/Injected.swift around lines 12 to 18, the
Injected property wrapper calls DIContainer.resolve on every property access,
causing unnecessary overhead. Modify the struct to cache the resolved dependency
instance in a private variable upon first access and return the cached instance
on subsequent accesses. This change reduces lock contention and dictionary
lookups while preserving the existing API and behavior for typical use cases.

5 changes: 1 addition & 4 deletions Projects/CoreDataService/Sources/CoreDataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import RxSwift
public protocol CoreDataService {
var storeStatus: BehaviorSubject<StoreStatus> { get }

func fetch<T: CoreDataStorable>(
type: T.Type
) -> Observable<[T]>
// func fetch<T: CoreDataStorable>(type: T.Type) throws -> [T]
func fetch<T: CoreDataStorable>(type: T.Type) -> Observable<[T]>

func save(data: some CoreDataStorable) throws

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,11 @@ import NetworkService
import RxSwift
import FirebaseInterface

public final class DefaultBusStopArrivalInfoRepository:
NSObject, BusStopArrivalInfoRepository {
private let networkService: NetworkService
@Injected(FirebaseLogger.self) private var logger: FirebaseLogger
private let disposeBag = DisposeBag()
public final class DefaultBusStopArrivalInfoRepository: NSObject, BusStopArrivalInfoRepository {
@Injected private var networkService: NetworkService
@Injected private var logger: FirebaseLogger

public init(networkService: NetworkService) {
self.networkService = networkService
}

public func fetchArrivalList(busStopId: String) ->
Observable<BusStopArrivalInfoResponse> {
public func fetchArrivalList(busStopId: String) -> Observable<BusStopArrivalInfoResponse> {
logger.log(name: "fetchArrivalEvent")
return networkService.request(
endPoint: BusStopArrivalInfoEndPoint(arsId: busStopId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,19 @@ import Foundation
import CoreDataService
import Domain
import NetworkService
import Core

import RxSwift

public final class DefaultFavoritesRepository: FavoritesRepository {
private let coreDataService: CoreDataService
private let networkService: NetworkService
@Injected private var coreDataService: CoreDataService
@Injected private var networkService: NetworkService

public var favorites = BehaviorSubject<[FavoritesBusResponse]>(value: [])

private let disposeBag = DisposeBag()

public init(
coreDataService: CoreDataService,
networkService: NetworkService
) {
self.coreDataService = coreDataService
self.networkService = networkService
public init() {
bindStoreStatus()
}
Comment on lines +26 to 28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

초기화 시점에 bindStoreStatus() 호출 → 순환 의존·시점 오류 가능성

bindStoreStatus() 내부에서 즉시 coreDataService를 사용합니다.
DIContainer 가 아직 준비되지 않았거나, 동일한 CoreDataService 내부에서 다시 이 Repository 를 생성하는 경로가 있으면 순환 의존으로 이어질 수 있으니 한 번 더 살펴보세요.

🤖 Prompt for AI Agents
In Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift at lines 26
to 28, calling bindStoreStatus() directly in the initializer may cause circular
dependency or timing issues because bindStoreStatus() uses coreDataService
immediately. To fix this, remove the bindStoreStatus() call from the init method
and instead call it after the DIContainer and coreDataService are fully
initialized, ensuring no circular creation paths exist.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,20 @@ import Foundation
import CoreDataService
import Domain
import NetworkService
import Core

import RxSwift

public final class DefaultRegularAlarmRepository: RegularAlarmRepository {
private let coreDataService: CoreDataService
private let networkService: NetworkService
@Injected private var coreDataService: CoreDataService
@Injected private var networkService: NetworkService

public let currentRegularAlarm = BehaviorSubject<[RegularAlarmResponse]>(
value: []
)
private let disposeBag = DisposeBag()

public init(
coreDataService: CoreDataService,
networkService: NetworkService
) {
self.coreDataService = coreDataService
self.networkService = networkService
public init() {
bindStoreStatus()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import NetworkService
import RxSwift

public final class DefaultVersionCheckRepository: VersionCheckRepository {
private let networkService: NetworkService
@Injected private var networkService: NetworkService

@UserDefaultsWrapper(
key: "ForceUpdate",
Expand All @@ -26,9 +26,7 @@ public final class DefaultVersionCheckRepository: VersionCheckRepository {
)
private var forceUpdateInfo: ForceUpdate

public init(networkService: NetworkService) {
self.networkService = networkService
}
public init() { }

/// 서버로 부터 받은 App의 최소 지원 버전
public func fetchRequiredVersion()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Foundation
import Domain

public final class DefaultForceUpdateService: ForceUpdateService {

public init() { }

public func compareVersion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,15 @@

import Foundation

import Core

import RxSwift

public final class DefaultAddRegularAlarmUseCase: AddRegularAlarmUseCase {
private let localNotificationService: LocalNotificationService
private let regularAlarmRepository: RegularAlarmRepository
@Injected private var localNotificationService: LocalNotificationService
@Injected private var regularAlarmRepository: RegularAlarmRepository

public init(
localNotificationService: LocalNotificationService,
regularAlarmRepository: RegularAlarmRepository
) {
self.localNotificationService = localNotificationService
self.regularAlarmRepository = regularAlarmRepository
}
public init() { }

public func checkNotificationAuth() {
localNotificationService.authorize()
Expand Down
Loading
Loading