안녕하세요! 오늘은 아주아주아주 오랜만에 TCA 관련 글을 써보려해요.
어느새 입사 2달차가 되었습니다 🥳

현재 프로젝트에서 TCA + Clean Architecture를 기반으로 기능별 모듈화를 목표하고 있어요
그런데 의존성 관리를 Needle로 하고 있더라고요.
처음엔 솔직히 의문이 들었어요.
TCA에 @Dependency 있잖아. 근데 왜 굳이 Needle을 또 써??
제 윗 선배님들이 왜 Needle을 쓰는지, 기존 TCA의 한계는 뭔지 공부할 수 있는 힌트를 주셨어요 :)
그걸 바탕으로 제가 직접 파헤쳐보고 내린 결론을 정리해보려 합니다.
비슷한 의문을 가진 분들께 도움이 됐으면 좋겠어요!
Clean Architecture를 적용해서 Layer별 모듈화가 되어있다고 가정을 해볼게요
- App/ 앱 진입점, Feature들
- Domain/ UseCase, Entity, Repository Protocol
- Infra/ Repository 구현체, Network, DB
당연히 의존성 방향은 APP -> Domin <- Core로 아주 전형적인 클린아키텍처로요
TCA에서 의존성 주입은 이렇게 하잖아요.
@Dependency(\.orderUseCase) var orderUseCase
깔끔하죠? 근데 이 orderUseCase의 실제 인스턴스는 어디서 오는 걸까요?
struct OrderUseCaseKey: DependencyKey {
static let liveValue: OrderUseCase = ??? // 여기에 실제 인스턴스
}
extension DependencyValues {
var orderUseCase: OrderUseCase {
get { self[OrderUseCaseKey.self] }
set { self[OrderUseCaseKey.self] = newValue }
}
}
liveValue에 실제 인스턴스를 넣어줘야 하는데... 여기서부터 문제가 시작됩니다.
문제 1: "이 의존성, 어디서 가져오지?"
OrderUseCase를 만들려면 여러 Repository가 필요해요
class OrderUseCase {
init(
orderRepository: OrderRepository,
cartRepository: CartRepository,
userRepository: UserRepository
) { ... }
}
그럼 liveValue에서 이렇게 써야 하는데...
struct OrderUseCaseKey: DependencyKey {
static let liveValue = OrderUseCase(
orderRepository: ???, // 어디서 가져오지?
cartRepository: ???, // 이것도?
userRepository: ??? // 이것도??
)
}
static 프로퍼티 안에서 다른 의존성을 어떻게 가져오죠?
해결책: 다른 Key의 liveValue를 직접 참조
struct OrderUseCaseKey: DependencyKey {
static let liveValue = OrderUseCase(
orderRepository: OrderRepositoryKey.liveValue,
cartRepository: CartRepositoryKey.liveValue,
userRepository: UserRepositoryKey.liveValue
)
}
되긴 해요. 근데 이게 쌓이면요...?
struct AKey: DependencyKey {
static let liveValue = A()
}
struct BKey: DependencyKey {
static let liveValue = B(a: AKey.liveValue)
}
struct CKey: DependencyKey {
static let liveValue = C(a: AKey.liveValue, b: BKey.liveValue)
}
struct DKey: DependencyKey {
static let liveValue = D(
a: AKey.liveValue,
b: BKey.liveValue,
c: CKey.liveValue
)
}
흠..
이제 100개가 된다면 어떻게 순서를 맞춰서 끼울수 있을까요?
의존성이 많아질수록 이 파일은 점점 스파게티 코드가 됩니다.
문제 2: "공유 리소스는 어떻게 전달해?"
프로젝트에서 데이터베이스(SwiftData) 를 쓴다고 해볼게요. ModelContainer는 앱에서 딱 하나만 있어야 해요.
// 앱 시작할 때 한 번만 만들어야 함
let database = try ModelContainer(for: Order.self, Product.self)
이걸 여러 Repository에서 공유해야 하는데...
struct OrderRepositoryKey: DependencyKey {
static let liveValue = OrderRepositoryImpl(
database: ??? // ModelContainer를 어디서 가져오지?
)
}
struct ProductRepositoryKey: DependencyKey {
static let liveValue = ProductRepositoryImpl(
database: ??? // 여기도 같은 ModelContainer 써야 하는데...
)
}
결국 전역 변수를 쓰게 돼요
// 어딘가에 전역으로...
let globalDatabase = try! ModelContainer(for: ...)
struct OrderRepositoryKey: DependencyKey {
static let liveValue = OrderRepositoryImpl(database: globalDatabase)
}
되긴 하는데... 전역 변수잖아요. 😅
테스트할 때 Mock으로 교체하기도 어렵고, 누가 어디서 접근하는지 추적도 안 되고...
비유하자면, 중요한 서류를 금고에 넣는 대신 복도에 그냥 놔두는 거예요. "어차피 우리끼리만 쓰니까~" 하면서요.
문제 3: 실수하면 런타임에 에러
TCA Dependency는 문제가 있어도 런타임에야 알 수 있어요
DependencyKey는 있는데 liveValue 내부 의존성이 잘못되었거나 런타임에서만 필요한 값이라면?
- 빌드: 성공 ✅
- 앱 실행: 정상 ✅
- 결제 버튼 탭: 💥 크래시
문제 4: 스코프 관리가 힘들어요
쇼핑앱에서 이런 상황을 생각해봐요:
- 로그인 전: 게스트 모드, 임시 장바구니
- 로그인 후: 유저 정보, 주문 내역, 찜 목록
- 로그아웃: 유저 관련 데이터 전부 정리해야 함
TCA는 스코프 개념을 프레임워크 차원에서 제공하지 않아요
로그아웃했는데 이전 유저의 Usecase가 메모리에 남아있다면?
다음 유저 로그인 시 이전 장바구니가 섞일수도 있지 않을까요?
물론 직접 초기화 로직을 구현할 수 있지만, 의존성이 많아지면 관리가 너무 복잡해져요.
그래서 Needle은 뭔데?
Needle은 Uber에서 만든 컴파일 타임 DI 프레임워크예요.
핵심 개념은 Component입니다
class RootComponent: BootstrapComponent {
// 앱 전체에서 공유되는 의존성들
}
TCA Dependency가 "재료 알아서 구해와" 라면 Needle은 레시피와 재료를 모두 관리해주는 🧑🍳주방장이에요
Needle이 문제를 어떻게 해결하는지 보여드릴게요
해결 1: 의존성 참조
class RootComponent: BootstrapComponent {
var keychainManager: KeychainManager {
shared { KeychainManager() }
}
var orderRepository: OrderRepository {
shared {
OrderRepositoryImpl(
database: database, // 같은 Component 내에서
keychain: keychainManager // 자유롭게 참조 가능!
)
}
}
var orderUseCase: OrderUseCase {
shared {
OrderUseCase(
orderRepository: orderRepository, // 그냥 쓰면 돼요
cartRepository: cartRepository
)
}
}
}
static let 지옥에서 벗어났어요!
그냥 같은 클래스 안의 프로퍼티처럼 자연스럽게 참조하면 됩니다. 훨씬 읽기 쉽죠?
해결 2: 공유 리소스를 깔끔하게 전달
class RootComponent: BootstrapComponent {
private let database: ModelContainer // 생성자에서 받음
init(database: ModelContainer) {
self.database = database
super.init()
}
var orderRepository: OrderRepository {
shared { OrderRepositoryImpl(database: database) }
}
var productRepository: ProductRepository {
shared { ProductRepositoryImpl(database: database) } // 같은 인스턴스!
}
}
// 앱 시작 시
let database = try ModelContainer(for: ...)
let root = RootComponent(database: database) // 생성자 주입!
전역 변수 없이, 생성자로 주입해요. 테스트할 때 Mock 넣기도 쉽고요!
해결 3: shared {}로 싱글톤 자동 보장
var keychainManager: KeychainManager {
shared { KeychainManager() } // 이 블록은 딱 한 번만 실행됨
}
shared { }로 감싸면:
- 처음 접근할 때 한 번만 생성
- 이후엔 같은 인스턴스 반환
- 초기화 순서? Needle이 알아서 해결
개발자가 순서 신경 쓸 필요가 없어요 🥳
해결 4: 컴파일 타임에 문제 발견
Needle은 코드 생성(Code Generation) 을 사용해요.
빌드할 때 의존성 그래프를 분석해서, 뭔가 빠졌으면 빌드 자체가 실패해요.
그래서 런타임 크래시 대신 빌드 에러로 미리 알 수 있어요
레고 조립 전에 "이 세트엔 부품이 3개 부족해요!" 라고 미리 알려주는 느낌이에요
해결 5: 스코프 관리 ✨✨
// 🌍 앱 전역 스코프 (앱 시작 ~ 종료)
class RootComponent: BootstrapComponent {
var analytics: Analytics { shared { ... } }
var keychainManager: KeychainManager { shared { ... } }
}
// 👤 로그인 세션 스코프 (로그인 ~ 로그아웃)
class LoggedInComponent: Component<RootDependency> {
var userProfile: UserProfile { shared { ... } }
var orderHistory: OrderHistory { shared { ... } }
var userCart: UserCart { shared { ... } }
}
// 💳 결제 플로우 스코프 (결제 시작 ~ 완료)
class CheckoutComponent: Component<LoggedInDependency> {
var paymentProcessor: PaymentProcessor { shared { ... } }
}
로그아웃하면?
→ LoggedInComponent만 해제
→ userProfile, orderHistory, userCart 모두 자동 정리!
→ 다음 유저 로그인 시 깨끗한 상태로 시작
Component 계층 구조로 생명주기가 자동 관리돼요.
Needle의 역할: 의존성 생성, 그래프 관리, 스코프 관리
TCA Dependency: Feature에 의존성 주입
참고 자료
https://github.com/nicholascm/needle
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/dependencymanagement
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
'SWIFT개발일지' 카테고리의 다른 글
| ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까? (1) | 2025.11.29 |
|---|---|
| 아 지겹다 복붙! Xcode 커스텀 템플릿 만들기 (1) | 2025.11.25 |
| BLE 완전 기초: CoreBluetooth를 이해하기 위한 필수 개념 (0) | 2025.11.16 |
| 이미지 URL 저장 시 마주하는 함정 문제들 (0) | 2025.09.11 |
| Metal3편 - 메모리 사용량 급증 버그 수정 (0) | 2025.04.20 |