의존성을 전달하는 방법들

Uno·2022년 11월 30일
0

Tip-Swift

목록 보기
22/26

SwiftUI 나 Flutter 를 사용하다보면, 상태관리에 대한 다양한 방법들을 접하게 됩니다.
SwiftUI 에서 EnvironmentObject 나 Flutter 에서의 Provider.of(context) 와 같은 것들이죠. 이 둘의 공통점은, 어디선가 "등록" 하는 과정이 있고, 그것을 "호출" 하는 과정으로 나눠져 있다는 겁니다.

여기서 적용된 패턴을 "서비스 로케이터" 라고 부릅니다.

서비스 로케이터에 대해서 정리하고 동시에 DI 와 어떤점이 다른지 궁금증이 생겨서 글을 쓰게 되었습니다.

의존성을 전달하는 방법들

특정 객체를 위해 위존성을 전달하는 방법에는 3 가지가 존재합니다.
1. Dependency Injection
2. Service Locator
3. Self access(or creation)

Dependency Injection

A technicque whereby one object supplies the dependencies of another object.
-위키피디아-

  • 의존성 주입이란, 특정 객체에 필요한 값들을 주입해주는 기술을 의미합니다. "필요한 값" 들은 해당 객체가 동작하는데 반드시 있어야 하는 값입니다.
  • 하나의 동사로 표현한다면, "to give" 라고 표현한다고 합니다. (링크)

예시 1

window = Window();
door = Door();

house = House(window, door);
  • 집을 구성할 때, 창문이 있어야하고, 문이 있어야합니다.
  • "House" 라는 객체를 생성하려면, window 와 door 를 전달받아야합니다.

예시 2

struct 회원정보 {...}

class 사용자정보관리객체 {...}

이렇게 두 객체가 있다고 가정합니다. 사용자 관리 객체는 로그인 시, 서버로부터 전달받은 "회원정보" 가 있어야 동작이 가능합니다.

struct 회원정보 {
	var userID: Int
	var userName: String

	init(userID: Int, userName: String) {
		self.userID = userID
		self.userName = userName
	}
}
class 사용자정보관리객체 {
	init(userInfo: 회원정보) {
		self.userInfo = 회원정보
	}

	...

	public func edit(userName: String) throws -> Bool {
		if userName == self.userInfo.userName { throws Error.변경된정보없음 }
		...(유저이름 변경 코드)...
		return true
	}
}
  • 사용자 정보관리객체에서 edit 를 동작시키기 위해서는 "회원정보" 구조체가 반드시 있어야 합니다.
  • 그래서 초기화 메소드 내에서 해당 정보를 입력받고 있습니다.
  • 이렇게 입력받는 방식을 "초기화 메소드를 이용한 DI" 라고 칭합니다.

장점

  • 초기화 메소드만 봐도, 필요한 객체가 무엇인지 한 눈에 파악하기 쉽다. (가독성 향상)
  • 테스트 코드 작성 시, Fake data 혹은 Mocking 하기 쉽다.

딱히 단점은 모르겠습니다.

Service Locator

DI 가 Service Locator 라고 오해하는 경우가 많은데, 둘은 다른 용어로 부르는게 맞습니다.

With service locator the application class asks for it explicitly by a message to the locator. With (dependency) injection there is no explicit request, the serivce appears in the application class
-Martin Fowler-

  • 서비스 로케이터는, 아주 구체적이고 명확한 의존성에 대한 요청받고, 서비스 로케이터를 통해 의존성에 접근하도록 해줍니다.
  • 의존성 주입은, 명시적인 요청은 없습니다. 그냥 요청자(어플리케이션) 에 있습니다.
  • 추가로, 둘의 공통점은 "디커플링" 을 위한 행위라는 점입니다.
house = serviceLocator.get(House);
house = serviceLocator.get(House.self)
  • 서비스 로케이터는 serviceLocator 라는 객체에게 명확히 어떤 값을 달라고 문의합니다.
  • Dart 든 Swift 든 Class Type 을 파라미터로 전달받든, 혹은 다른 식별자를 통해서 전달받든 합니다.
  • 물론 위 과정에서 생략된 부분은 최초에 House 라는 타입을 등록해두어야 합니다.

장점

  • 의존성에 대한 값을 ServiceLocator 에게 모두 위임할 수 있고, 해당 코드가 모두 한 곳에서 관리할 수 있다는 점
  • 전체 어플리케이션에 어떤 의존성이 추가되는지 등록 과정에서 한 번에 볼 수 있다는 점
  • 의존성이 있는 값을 조회할 때, 호출자는 인스턴스 생성 없이 바로 조회가 가능하다는 점

단점

  • ServiceLoactor 라는 SingleTon 이 생기므로, 싱글톤에서의 단점이 유발될 수 있다는 점
  • 컴파일 타임에 잘못된 코드를 못잡을 우려가 있다는 점.
    (컴파일 때는 해당 값을 조회 못하더라도, 에러로 발견되지 않고 런타임 때, 해당 값에 접근하려고 할 때 값이 없어서 Null Exception Error 발생됨)
  • 클래스 의존성이 숨겨진다는 점.
    (위에 있는 단점 + 코드 변경 시, 어떤부분이 호환이 안되는지 바로 알기 어렵다. 코드 전체를 읽어야 한다.)

ServiceLocator 구현코드

Swift

// 인터페이스 1
protocol Authentication {
	var userID: Int { get }
}

// 인터페이스 2
protocol UserInfo {
	var userName: String { get }
}

// 구현 객체 1
class LoginViewModel: Authentication {
	var userID: Int { 1 }
}

// 구현 객체 2
class ProfileViewModel: UserInfo {
	var userName: String { "woosung" }
}

// 서비스 로케이터 인터페이스
protocol ServiceLocating {
	func getService<T>() -> T?
	func addService<T>(_ service: T)
}

// 서비스 로케이터 구현 객체
class Locator: ServiceLocating {
	static let shared = Locator()
	private init() {}
	private var services: [String: Any] = [:]
	private func typeName<T>(_ service: T) -> String {
		return "\(type(of: service))"
	}

	public func getService<T>() -> T? {
		let key = typeName(T.self)
		return services[key] as? T
	}
	
	public func addService<T>(_ service: T) {
		let key = typeName(T.self)
		services[key] = service
	}
}

/* 실행코드 */
let loginViewModel = LoginViewModel()
let profileViewModel = ProfileViewModel()
let locator = Locator.shared

locator.addService(loginViewModel)
locator.addService(profileViewModel)

let loginVM: LoginViewModel = locator.getService()!
let profileVM: ProfileViewModel = locator.getService()!

print(loginVM.userID)
print(profileVM.userName)

정리

  • DI 는 의존성을 주입 + 어떤 값을 정확히 필요로 하는지 호출 시점에 작성하지 않는다.
  • ServiceLocator 는 등록과정이 필요 + 호출과정에서 명확히 어떤 객체가 필요한지 명시한다.
  • 보통 UI 에서 값에 접근할 때는 ServiceLocator, ViewModel 처럼 테스트가 필요할 때는 DI 를 사용한다.

감사합니다.

참고자료

profile
iOS & Flutter

0개의 댓글