TCA + Clean Architecture에서 의존성 관리, Needle 도입한 이유

2026. 1. 11. 16:40·SWIFT개발일지

안녕하세요! 오늘은 아주아주아주 오랜만에 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
'SWIFT개발일지' 카테고리의 다른 글
  • ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까?
  • 아 지겹다 복붙! Xcode 커스텀 템플릿 만들기
  • BLE 완전 기초: CoreBluetooth를 이해하기 위한 필수 개념
  • 이미지 URL 저장 시 마주하는 함정 문제들
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (140)
      • SWIFT개발일지 (31)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (44)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (3)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
TCA + Clean Architecture에서 의존성 관리, Needle 도입한 이유
상단으로

티스토리툴바