의존성 주입을 하면 잃게 되는 것

(부제: 객체는 잃지만 개발자가 얻는 것)

의존성 주입을 하면 프로그램에 꼭 필요한 중요 역할이 여러 클래스로 분산됩니다. 의존성을 생성하는 그 막강한 권한이 소비자 객체에서 다른 곳으로 옮겨갑니다. 처음에는 객체가 자신의 의존성을 스스로 생성하고 관리할 권한을 잃는다는게 부정적으로 느껴질 수 있지만 이렇게 함으로써 개발자가 그 권한을 얻게 됩니다.

의존성을 생성하는 작업을 객체 합성(object composition)이라고 부릅니다. 의존성 주입을 하면 객체가 잃게 되는 대표적인 권한입니다. 하지만 이게 전부가 아닙니다. 의존성을 생성하지 못하니 그것의 생애(object lifetime)도 관리하지 못합니다. 개발자가 객체을 합성하고 수명을 통일해서 관리할 수 있는 권한을 가지게 되면, 의존성이 소비자 객체에 주입되기 전에 가로채서(intercept) 무언가를 할 수 있게 됩니다. 다시 말해 객체가 이런것들을 잃는다는 것은 반대로 개발자가 그걸 얻게 된다는 말압니다. 이 3가지에 대해 간략히 살펴보겠습니다.

객체 합성 Object Composition

앱을 개발한다는 건 뜯어보면 여러 기능, 모듈, 객체들을 우리가 원하는 방식대로 조합하는 작업입니다. 레고 조각들을 모아서 원하는 모양을 만드는 것처럼요. 앱 개발자는 이 과정에서 유연성과 확장성을 원합니다. 기존의 코드는 최대한 수정하지 않고 이리저리 조합하여 새로운 요구사항에 대응할 수 있는게 제일 좋습니다. 의존성 주입을 하면 앱의 최상단에서 객체들을 원하는대로 합성할 수 있습니다. 의존성 주입을 하는 가장 큰 이유이기도 합니다.

객체 생애 관리 Object Lifetime Management

더이상 의존성 생성을 직접 하지 않게 된 객체는 그 의존성의 생애를 관리할 수도 없습니다. 의존성을 주입받은 객체는 그 의존성이 언제 어떻게 생성됐는지, 언제 해제되는지 알 방법이 없습니다. 하지만 모르는게 오히려 더 좋습니다. 신경쓸게 적어지니 훨씬 간단해집니다. 반대로 개발자가 이를 관리하게 됩니다. 앱의 객체들이 몇 개나, 얼마나 오래 살아있을지, 또 누구에게 넘겨줄지 결정하고 관리해줄 수 있는 권한이 생깁니다.

스위프트에서는 ARC가 객체의 수명을 관리해줍니다. 소비자 객체가 의존성을 강하게 참조하고 있기만 하면 자신이 살아있는 동안에는 마음껏 사용할 수 있습니다. 아래 코드와 같이 개발자가 레포지토리 인스턴스를 하나만 만들어서 여러 서비스 객체에 주입해 줄 수 있습니다.


let authRepository: AuthenticationRepository = AuthenticationRepositoryImp()

// 하나의 의존성 객체를 여러 소비자에게 주입
let profileService: ProfileService = ProfileServiceImp(auth: authRepository)
let feedService: FeedService = FeedServiceImp(auth: authRepository)

가로채기 Interception

의존성에 대한 권한을 개발자가 쥐게 되면 소비자 객채에 의존성을 넘겨주기 전에 조작을 가할 수 있습니다. 이런 권한을 실제 코드로 구현한 것이 데코레이터 패턴입니다. 데코레이터 패턴을 사용하면 가로채기를 활용해 단일 책임 원칙을 지킬 수 있습니다. 특히 횡단 관심사(cross cutting concern)를 처리할 때 매우 유용합니다. 예를 들어 앱의 주요 API를 모니터링하고 싶습니다. 성공/실패 여부와 네트워킹 소요시간을 측정해서 서비스 로그로 보내려고 합니다. 이와 같은 성능 측정 코드를 API 서비스 객체에 바로 구현하기 보다는 모니터링을 담당하는 데코레이터를 만들고, 이 데코레이터 객체를 주입해줄 수 있습니다. 이렇게 하면 서비스의 고유 로직과 기타 로직을 분리할 수 있습니다.

protocol PurchaseService {
  func purchase(_ productID: String) async throws
}

// 데코레이터 패턴으로 성능/에러 로깅 구현
final class PurchaseServiceMonitoringDecorator: PurchaseService {
  private let decoratee: PurchaseService
  private let logger: AppAnalytics
  private let clock: any Clock
  
  func purchase(_ productID: String) async throws {
    do {
      let duration = try await clock.measure {
        try await self.decoratee.purchase(productID)
      }
      self.logger.log(AppEvent.purchase, [AppEvent.durationKey: duration])
    } catch {
      self.logger.logError(error)
      throw error
    }
  }
}

결론

의존성 주입이라는 개념이 처음 생겨났을 때는 오로지 객체 합성의 문제를 포함하고 있었는데 그 개념은 점점 확대되어 오늘날에는 위 3가지를 모두 다루는 것으로 발전해왔습니다. 비록 객체 합성이 없으면 생애 관리도 필요 없고 가로채기도 할 수 없으므로 제일 중요한 부분이긴 하지만 나머지 둘도 잊으면 안됩니다. 객체 생애 관리에 대해서 염두하고 있어야 예기치 못한 사이드 이펙트를 방지할 수 있고, 특히 가로채기야 말로 개발자가 의존성 주입을 통해 비로소 얻을 수 있는 달달한 열매라는 사실을 기억하시길 바랍니다.

📝 Swift 개발자를 위한 DI 강의 사전 신청

객체 합성, 수명 관리, 가로채기 권한을 가지고 앱 개발자가 누릴 수 있는 강력한 설계 능력을 확인해보고 싶으시다면 현재 준비중인 동영상 강의 사전 신청서를 작성해주세요. 강의가 준비되면 가장 먼저 알려드리고 30% 할인 쿠폰을 드립니다.

📎 할인 쿠폰 신청하기: https://forms.gle/JXwKctVS6XqKoHF76

Tags: dependency injection, DI scope  

의존성 주입에 대한 오해와 진실

의존성 주입에 대한 오해

1. 의존성 주입은 유닛 테스트만을 위한 것이다.

의존성 주입이 유닛 테스트를 하기 위한 핵심 기반이긴 합니다. 하지만 유닛 테스트를 작성하려는 목적이 아니더라도 DI를 적용하면 여러 이점들을 똑같이 누릴수 있습니다. 유닛 테스트는 그 이점들 중 하나일 뿐입니다.

2. 의존성 주입은 추상 팩토리 그 이상 그 이하도 아니다.

앱의 의존성들을 생성해주는 추상 팩토리 같은 객체가 필요하다고 생각하셨다면 바로 지워야 합니다. 오히려 그 반대입니다. 의존성 주입이란 의존성을 ‘어디선가 가져오는’게 아니라, 소비자가 제공하도록 코드의 구조를 잡는 것입니다.

3. 의존성 주입을 하려면 DI Container가 필요하다.

2번 미신과 연관지어서 DI Container를 추상 팩토리로 사용해야한다고 생각하는 사람들도 있습니다만 잘못된 생각입니다. DI Container는 앱을 조합하는 과정을 쉽게 해줄 수 있는 라이브러리지만 필수는 아닙니다. 의존성 주입은 원칙과 패턴의 모음입니다. DI Container는 유용하지만 선택사항인 도구입니다.

의존성 주입의 목적

의존성 주입 자체는 목적이 될 수 없습니다. 의존성 주입은 느슨한 결합을 가능케 해주고, 느슨한 결합은 코드를 유지보수하기 좋게 만들어줍니다. 우리 주변에서 쉽게 찾아볼 수 있는 느슨한 결합의 아주 좋은 예는 전기 콘센트입니다. 우리 집 벽에 튀어나와 있는 전기 콘센트는 그와 연결된 기기들의 플러그(구현체)를 통해 연결됩니다. 그 둘의 모양이 맞고(인터페이스를 구현하면), 전압과 주파수가 호환된다면(인터페이스의 계약을 준수) 무사히 작동하고, 또 우리가 원하는 대로 다양하게 전자 기기들을 조합해서 사용할 수 있습니다.

반면에 여러분의 티비가 벽에 직접 연결되어 있었다고 상상해보세요. 티비가 고장나면 그 티비와 건물 인프라를 둘 다 잘 아는 기술자를 불러야할 뿐더러 수리를 하는 동안 집 전체에 전기를 끊어야할 수도 있습니다. 이게 강하게 결합된 코드의 모습입니다. 다른 한 쪽에 영향을 주지 않으면서 다른 하나를 건드릴 수 없습니다.

의존성 주입은 어떻게 코드의 유지보수성을 좋게 해줄까요? 의존성 주입이 가져다 주는 장점은 예전 글을 통해 확인하시기 바랍니다. 느슨한 결합이 가져다 주는 이점 5개

의존성 주입을 왜 해야 하나?

의존성 주입은 도구나 기술이라기 보다는 코드를 설계하는 방식에 대한 원칙과 패턴의 모음입니다. 코드를 어떻게 구성하는게 좋은지 판단할 수 있는 틀을 제공해줍니다. 그래서 의존성 주입은 코드 전체에 퍼져있어야 합니다. 느슨한 결합으로 얻을 수 있는 열매는 앱이 복잡해질수록, 코드양이 많아질수록 잘 드러납니다.

흔히 말하는 스파게티 코드란 의존성 주입이 적용되지 않은 강하게 결합된 코드를 의미합니다. 의존성 주입은 구현에 의존하지 말고 인터페이스에 의존하라(Program to an interface, not an implementation) 이라고 했던 Gang of Four의 격언을 현실화하는 방법입니다.

더 상세한 내용과 실제 코드와 예제 앱을 통해 확인하고 싶으시다면 아래 사전 신청서를 작성해주세요.

📝 Swift 개발자를 위한 DI 강의 사전 신청

의존성 주입의 원칙과 패턴에 대해서 더 깊게 알고 싶으시다면, 의존성 주입이 앱 개발자에게 가져다 주는 이점을 구체적으로 확인해보고 싶으시다면 현재 준비중인 동영상 강의 사전 신청서를 작성해주세요. 강의가 준비되면 가장 먼저 알려드리고 30% 할인 쿠폰을 드립니다.

📎 할인 쿠폰 신청하기: https://forms.gle/JXwKctVS6XqKoHF76

Tags: dependency injection  

의존성 주입 안티패턴: Ambient Context

Ambient Context 정의와 예시

Ambient Context는 static 접근자를 통해 정적 타입의 의존성을 노출시키는 패턴입니다. 이 패턴은 volatile dependency가 의존성 주입 없이 코드베이스 전역에서 사용되도록 만듭니다. 예시를 하나 보겠습니다.

// 나쁜 코드 예시 ❌
func hoursSince(_ date: Date) -> Double {
  let timeProvider: TimeProvider = DefaultTimeProvider.current
  let now: Date = timeProvider.now
    
  let timeInterval = now.timeIntervalSince(date)
  return timeInterval / 3600.0
}

위 예시에서 TimeProvider라는 프로토콜은 현재 시간을 가져오기 위한 추상화입니다. 일반적으로 앱에서 시간이라는 개념을 통제하고 싶을 때(특히 테스트에서) Date() 또는 Date.now를 직접 호출하지 않기 위해 이와 같은 방법을 씁니다. 이런 추상화를 한 다음, 자칫 DefaultTimeProvider.current 와 같이 기본 구현체(default implementation)를 함께 제공해서 소비자 객체에서 쉽게 사용하고 싶은 유혹이 생깁니다. 싱글턴과 비슷하긴 하지만, 싱글턴은 인스턴스가 절대 바뀌지 않게 하는 반면에 ambient context는 의존성을 교체할 수 있게 해줍니다.

Ambient Context가 테스트를 복잡하게 만드는 이유

Ambient Context를 사용하면 테스트도 작성할 수 있습니다. TimeProvider 기본 구현체를 TimeProviderStub으로 교체하여 테스트가 가능합니다.

// 나쁜 코드 예시 ❌
func testHoursSince() {
  // given
  let currentTime = Date(timeIntervalSince1970: 1647965400)
  DefaultTimeProvider.current = TimeProviderStub(now: currentTime)

  // ⚠️ TimeProvider에 의존하고 있다는 사실이 숨어있음
  let sut = TimeCalculator()

  // when
  // ⚠️ DefaultTimeProvider.current와 hoursSince 사이에 Temporal Coupling이 존재함
  let result = sut.hoursSince(currentTime.addingTimeInterval(-3600))

  // then
  XCTAssertEqual(result, 1, accuracy: 0.001)
}

그러나 이 같은 테스트 코드에는 문제가 있습니다. TimeCalculator 객체의 생성자에는 파라미터가 하나도 없어서 TimeProvider를 필수 의존성으로 가지고 있다는 정보가 숨겨져 있습니다. 또한 DefaultTimeProvider.current로 의존성을 주입해줘야 하는 것과 hoursSince 호출 사이에는 Temporal Coupling 문제가 있습니다.

Ambient Context가 활용된 로깅

Ambient Context가 종종 발견되는 곳은 로깅 기능입니다. 로깅은 코드 전반에 걸쳐 호출되는 경우가 많아 개발자는 로깅에 필요한 의존성을 주입해주는걸 무척 번거롭다고 느낍니다. 가뜩이나 Swift에서는 protocol extension을 통해 추상 인터페이스의 기본 구현을 제공하는 것이 매우 쉽기 때문에 iOS 개발자라면 공통 코드를 재사용하기 위한 방법으로 Ambient Context를 사용하게 됩니다. 이런 구현은 위의 예시 코드보다도 더 간단하기에 유혹에 빠지지가 더 쉽습니다.

// 나쁜 코드 예시 ❌
public protocol Loggable {
  func log(level: LogLevel, _ message: @autoclosure () -> String)
}

public extension Loggable {
  func log(level: LogLevel, _ message: @autoclosure () -> String) {
    //Firebase Analytics 등의 로깅 라이브러리를 호출
    Analytics.logEvent(message(), parameters: ...)
  }
}

Ambient Context의 문제점

  • 의존성이 숨어있다.
  • 테스트가 더 어려워진다.
  • 상황에 따라 구현체를 바꾸는 것이 어렵다.
  • 의존성 생성과 사용 사이에 Temporal Coupling이 생긴다.

의존성을 숨겨 코드 스멜을 약화시킨다.

의존성이 숨겨져 있으면 객체가 얼마나 많은 의존성이 있는지 파악하기가 어렵고, 그렇게 되면 객체가 너무 커지고 있다(SRP 위배)는 코드 스멜이 약해져 리팩토링이 필요하다는 신호를 놓치게 됩니다.

테스트를 어렵게 만든다.

Ambient Context를 사용하면 테스트를 할 때 전역 상태를 건드려야 하기 때문에 다른 테스트에 영향을 줄 수 있습니다. 병렬 테스트 실행을 할 수 없게 되고, 순차 테스트를 하더라도 제대로 setup이나 teardown을 해주지 않으면 문제가 생길 여지가 있어서 테스트 코드의 유지 보수가 어려워집니다.

소비자에 따라 다른 구현체를 제공하기 어렵다.

가령 앱에서 로그를 보내야하는 곳이 하나가 아니라 종류에 따라 다른 로깅 라이브러리를 써야한다면, 이를 구분하기 위해서 아마도 인터페이스에 파라미터를 추가하는 등, 소비자 입장에서 더 복잡한 인터페이스를 제공할 수 밖에 없습니다.

Temporal Coupling 문제

숨겨져 있는 의존성을 제때 생성해주지 않으면, 기능을 첫 호출하는 시점에서야 잘못됐다는걸 알 수 있습니다. 코드에 문제가 있다면 최대한 빨리 아는 것이 좋습니다.

Ambient Context 대신 의존성 주입 패턴을 사용하자

Ambient Context 대신 생성자 주입과 decorator 패턴을 통해 Ambient Context 안티패턴을 대체할 수 있습니다. 특히 로깅과 같은 cross-cutting concern에 대해서는 의존성 주입 패턴을 활용하는 것이 유지보수 관점에서 훨씬 좋습니다. (더 자세한 내용은 아래 강의를 통해 확인해주세요.)

📝 Swift 개발자를 위한 DI 강의 사전 신청

의존성 주입의 원칙과 패턴에 대해서 더 깊게 알고 싶으시다면, Ambient Context 안티패턴을 리팩토링 하는 구체적인 방법과 코드 예제에 대해서 궁금하시다면 현재 준비중인 동영상 강의 사전 신청서를 작성해주세요. 강의가 준비되면 가장 먼저 알려드리고 30% 할인 쿠폰을 드립니다.

📎 할인 쿠폰 신청하기: https://forms.gle/JXwKctVS6XqKoHF76

Tags: DI, anti-pattern, ambient context  

링크드인과 이력서 관리 팁

링크드인 프로필과 이력서의 가장 큰 차이점은 독자의 시선이 얼마나 머무느냐에 있습니다. 이력서는 검토자가 무수히 많은 이력서 중에 상위 몇 개를 골라내야하는 입장입니다. 그리고 보통은 시간에 쫓깁니다. 그래서 이력서는 몇 초 안에 이목을 끌어야만 하는 문서입니다. 반면에 링크드인은 검토자가 직접 내 프로필을 찾아와서 보는 것이기 때문에 시간이 더 많이 주어집니다.

이런 차이로 인해 분량에 큰 차이를 둡니다. 이력서는 경력이 10년이 넘어가지 않는다면 가급적 1장 내로, 최대 2장 정도로 간결하고 압축적이야 합니다. 반면에 링크드인은 훨씬 자세하게 많이 적어도 괜찮죠. 오히려 검색에 더 잘 노출되기 위해서라도 내 커리어의 중요 키워드는 최대한 많이 적어놔야 합니다.

그래서 저는 링크드인에는 내가 했던 프로젝트, 주요 키워드 등등 빠짐없이 상세하게 빼곡히 적어둡니다. 그리고 나서 이력서를 업데이트할 때는 링크드인에 적어 놓은 수많은 항목 중에 제일 중요한 것만 몇 개 골라서 1장으로 만듭니다. 이렇게 하면 여러 버전의 이력서를 쉽게 만들어낼 수 있습니다. 내가 꼭 가고 싶은 회사에 제출할 이력서를 맞춤식으로 만들때는 채용 공고에 나와있는 키워드와 우대 사항에 해당하는 경험을 꼭 포함시키고 덜 중요한 것들을 빼내는 식으로 조합을 바꿔가면서 만들 수 있습니다.

이력서 업데이트가 어렵고 귀찮게 느껴지셨던 분들이 계시다면, 평소에는 프로젝트가 끝날때마다 링크드인에 상세히 업데이트 해놓고, 나중에 이력서가 필요해졌을 때 링크드인에 적어 놓은 것들을 cherry picking해서 완성해보세요. 이렇게 하면 이력서를 업데이트하는데 걸리는 시간이 확 줄어들고, 회사별 맞춤형 이력서를 쉽게 만들어낼 수 있습니다.

Tags: resume, linkedin