iOS Developer Global Summit 22 - 주니어 트랙 (종속성 주입)

rbw·2022년 12월 26일
0

TIL

목록 보기
61/97

참조

https://www.youtube.com/watch?app=desktop&v=M0c6DGNOUYc&t=9s

위 트랙들을 보며 몇개 정리한 글, 자세한 것은 위 링크 참조바ram


DI, 종속성 주입에 관한 트랙 (00:25:00 ~ )

먼저 기본 방식 세 가지를 살펴보겠슴니당

Initializer Based

이는 초기화 구문으로 전달하는 방식, 파라미터로 전달하는 방식과 다르지 않다라고 설명 하심

테스트 하는 경우는 초기화 시에 의존하고 있는 클래스를 파라미터로 넘겨주면 된다.

final class UserServiceTests: XCTestCase {
    func test_getUser_whenDataIsValid_thenItWillReturnSomeUser() throws {
        // Given
        let userMock: User = .init(name: "Random User")
        let userDataMock: Data = try XCTUnwrap(userMock.asData())

        let networkStub: NetworkStub = .init()
        networkStub.getDataResultToBeReturned = .success(userDataMock)

        let sut = UserService(network: networkStub) // 이니셜라이저에 파라미터로 전달
    }
}

Property Based

이는 외부에서 변경이 가능하다, 초기화 구문에 대한 컨트롤이 없는 경우 실행 가능한 옵션이다.

예를 들어, 스토리보드를 사용하는 뷰 컨트롤러가 있는 경우에, 이니셜라이저는 뷰를 구성하기 위해 사용된다. 여기서는 사용하기가 어렵다고 설명하신듯 ?

테스트 하는 경우

final class ViewControllerTestes: XCTestCase {
    func test_networkIsCalled() {
        // Given
        let sut: ViewController = .init()
    
        let netwrokSpy: NetworkSpy = .init()
        sut.network = networkSpy // 프로퍼티에 종속성을 전달한다

        // When 
        sut.loadData() 

        // Then
        XCTAssertTrue(networkSpy.getDataCalled)
    }
}

Parameter Based

이는 함수의 파라미터에 전달하는 방식입니다.

기존 메소드에 테스트가 필요한 경우 변경할 필요없이 가능한 옵션입니다.

예시 코드는 다음과 같습니다.

extension UIImageView {
    func setImageFromURL(
        _ url: URL,
        network: NetworkProtocol = Network.shared,
        mainQueue: DispatchQueue = .main
    ) {
        network.getData(from: url) { result in
            guard
                let data = try? resutl.get(),
                let remoteImage = UIImage(data: data)
            else { return }
            mainQueue.async { self.image = remoteImage }
        }
    }
}

테스트 하는 경우는 다음과 같슴니다.

final class UIImageViewTests: XCTestCase {
    func test_WhenDataIsValied_thenItShouldReturnTheExpectedImage() throws {
        // Given
        let imageMock: UIImage = .add
        let imageDataMock = try XCTUnwrap(imageMock.pngData())

        let network ...

        let sut: UIImageView = .init()
        
        let dummyURL: URL = try XCTUnwrap(.init(string: "www.something.com/image.png"))

        // When
        // 메소드의 파라미터로 전달을 하고 있다. 
        // 모의 테스트 경우에 맞게 networkStub을 전달하고 있는 모숩~
        sut.setImageFromURL(dummyURL, network: networkStub, mainQueue: .global())

        // Then
        XCTAssertNotNil(sut.image)
    }
}

Advanced DI

Singletons

발표하신 분은 싱글톤에 대해 간단히 설명하셨고, 이것을 어떻게 사용할지, 왜 내가 필요한지 이런 부분들을 생각해서 간단하게 사용하라고 하심. 또 강조하신 점은, 하나의 클래스엔 최대한 하나의 책임을 가지게 설계하라는 부분도 강조 하셨스. 무작정 사용 하는것은 좋지가 않다 ~

싱글톤을 태수투 하눈 예제 코드

protocol UserSessionProtocl {
    var currentUser: LoggedUser? { get }
    var isValid: Bool { get }
    func login(
        username: String,
        password: String,
        then completion: @escaping (Result<Void, Error>) -> Void
    )
}

extension UserSession: UserSessionProtocol {}

final class SomeViewModelThatNeedsUserSession {
    let userSession: UserSessionProtocol
    init(userSession: UserSessionProtocol) {
        self.userSession = userSession
    }
}

아래 코드와 같이 앱의 환경에 사용하면 꽤 이해가 쉽고, 커플링을 줄이는 간단한 해결책이 될 수 있다.

protocol HasURLSession {
    var urlSession: URLSession { get }
}
protocol HasUserDefaults {
    var userDefaults: UserDefaults { get }
}
protocol HasUserSession {
    var userSession: UserSessionProtocol { get }
}

protocol AppDependenciesContainer: HasURLSession, HasUserDefaults, HasUserSession {}

final class AppDependenciesEnvironment: AppDependenciesContainer {
    static let shared = AppDependenciesEnvironment()

    private(set) var urlSession: URLSession
    private(set) var userDefaults: UserDefaults
    private(set) var userSession: UserSessionProtocol 

    private init() {
        self.urlSession = .shared
        self.userDefaults = .standard
        self.userSEssion = UserSession.shared
    }
}

final class SomeViewModel {
    typealias Dependencies = HasUserSession & HasUserDefaults
    private let dependencies: Dependencies

    init(dependencies: Dependencies = AppDependenciesEnvironment.shared) {
        self.dependencies = dependencies
    }
}

하지만 모듈화가 있는 상황에서 이러한 방식을 사용하면 원치 않는 경우에 커플링이 생길 수 있다고 설명하심 대형 프로젝트에 모듈화가 이루어져있는 경우 사용을 비추하시는듯 ?

애플에서 사용하는 시스템 클래스에서의 싱글톤은 싱글톤이랑 extension을 사용해서 만든다고 하심 예를들어, 유저디폴트, URLSession, 매니저 등

여기서는 private initializer를 사용하지 않고, 좀 더 공유 상태의 컨트롤을 추가하는 형식임. 그래도 여전히 공유 인스턴스를 옵션중 하나로 유지하고 있다.

open 접근자를 주어 다른 클래스에서 상속이 가능하고 그래서 우리는 함수와 프로퍼티에 조금의 제어를 얻을 수 있다.

open class PersistencyManger {
    static let shared = PersistencyMager(userDefaults: .standard)

    // Dependencies
    private let userDefaults: UserDefaults

    // Public Properties 
    private(set) var values: [String ] = []

    // Initializaition
    init(userDefault: UserDefaults) {
        self.userDefaults = userDefaults
    }

    // Public Functions
    func save(_ value: String) -> Bool {
        ...
        userDefaults.set(...)
        let sincronizationSucceeded = userDefaults.synchronize()
        ...
    }
}

이제 진짜 테스트 하는 코드를 보겠습니다.

final class DefaultsMangerTest: XCTestCase {
    func test_whenAddIsCalled_userDefaultsShouldReceiveValue_andSyncronize() {
        // Given
        // 모의 유저디폴트를 만들어두고, 인스턴스를 생성해줌 
        let userDefaultsSpy = UserDefaultsSpy()
        let sut = persistencyManager(userDefaults: userDefaultsSpy)
        let valueToAdd = "some Value"
        // When
        let addSucceeded = sut.save(valueToAdd)
        // Then
        XCTAssertTrue(addSucceeded)
        XCTAssertTrue(userDefaultsSpy.setValueCalled)
        XCTAssertEqual(1, sut.values.count)
        XCTAssertTrue(userDefaultsSpy.synchronizeCalled)
    }
}

Factories

얘도 싱글톤과 마찬가지로 생성에 관련된 디자인 패턴임. 구체적인 타입을 명시하지 않는 오브젝트들을 만드는것에 초점을 둔다.

목적 : hide/encapsulate 구체적인 세부 사항은 숨기고 캡슐화 한다는 뜻.

장점 : 사용자는 생성과 관련된 내부 논리에 대해 알 필요가 없다. 또 많은 종속성에 의존할 필요 없이 이 친구만 의존해도 되는 부분 ?

여기서는 최대한 간단히 설명했으므로, 자세한 내용은 팩토리 메서드와 추상화 팩토리?(Abstract Factory)를 체크해보라고 하심

예제 코드

여기서 셀을 누르면 실행되는 코드를 보면 TradingManager()가 존재함. 이 친구는 이전 뷰컨에는 없는 의존성이다. 이는 문제가 발생할 수 있는 좋지 않은 코드임. 여기에 대한 통제권(control)도 없고 무엇이 되는지도 모르기 때문임.

이런 경우를 방지하기 위해 의존성 컨테이너와 뷰 컨을 어케 만드는지 알고 있는 팩토리를 만들 수 있다.

위 팩토리 코드를 이용해서 이전 코드를 리팩토링 해보면 다음과 같다

Service Locator

이는 서비스들과 의존성 인스턴스들을 유지하고 접근을 제공하는 패턴이다.

기본적으로 인스턴스의 큰 싱글톤이다. 다른 인스턴스를 보관하는 장소로, 이들을 만드는 방법을 알려주는 것임.

구현하려면 기본적으로 Resolver가 필요합니다.

리졸버는 어떤 이름이나 유형 또는 어떤 것에 대해 해결하는 방법을 알려준다고 함. 근데 이 개념이 정확히 맞는지는 잘 모르겠슴ㄷㅏ..

필요한 프로토콜을 채택하는 코드를 따로 두는 모숩

이 방식으로 우리는 의존성을 관리하기 위한 매니저를 만들었습니다. 이제 이것을 사용하는 코드로는 다음과 같습니다.

가장 간단한 장소로는 AppDelegate에서 사용을 하면 됩니다. 위 코드는 로그인 서비스에 관한 의존성을 등록하는 모습을 보여주고 있습니다.

서비스 로케이터를 노출하고 싶지 않다면 다음과 같이 작성하면 가능하다.

final class LoginViewModel {
    private let loginService: LoginServiceProtocol
    private let userSession: UserSessionProtocol
    
    init(
        loginService: LoginServiceProtocol? = nil,
        userSession: UserSessionProtocol? = nil
    ) {
        self.loginService = loginService ?? ServiceLocator.shared.autoResolve()
        self.userSession = UserSession ?? ServiceLocator.shared.resolve(UserSessionProtocol.self)
    }
}

// 상관없다면 아래와 같이 작성
// 위 코드는 파라미터에서 노출을 아예 안 시키는 반면, 아래는 기본값으로 주고 있다.
init(
    loginService: LoginServiceProtocol = ServiceLocator.shared.autoResolve(),
    userSession: UserSessionProtocol = ServiceLocator.shared.resolve(UserSessionProtocol.self)
) {
    self.loginService = loginService
    self.userSession = UserSession 
}

프로퍼티 래퍼를 사용해 리팩토링

먼저 프로퍼티 래퍼란 구조체로서, 프로퍼티의 읽기/쓰기의 접근을 캡슐화를 할 수 있습니다.

@propertyWrapper
struct Capitalized {
    var wrappedValue: String {
        didSet { wrappedValue = wrappedValue.capitalized }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.capitalized
    }
}

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

// 사용예시
var user = User(firstName: "eduardo", lastName: "bocato")
print(user.firstName, user.lastName) // "Eduarado Bocato"

위 처럼 만들면 아래와 같이 코드 수정이 가능합니다.

이의 장점으로는 명시적으로 resolve를 부르지않고 간단히 서비스에 접근이 가능하고, 코드가 분명해지고 줄어듭니다. 좀 더 스위프티 하다네요 ~

하지만 이제 테스트를 어떻게 할지 생각해봐야합니다

위와 같이 코드를 작성해줍니다. if DEBUG로 명시하여, 이 코드가 프로덕션 버전에는 포함이 되지 않도록 합니다. 또 이니셜라이저가 private 이므로 같은 파일에서 이를 행해야 합니다.

아래는 테스트코드의 예시입니다.


상당히 길었네여,, 코드가 너무 많긴 합니다만 오히려 이런 코드들이 많아서 좀 더 생각하는 기회가 되었지 않나 개인적으론 생각합니다.

profile
hi there 👋

0개의 댓글