참조
https://www.youtube.com/watch?app=desktop&v=M0c6DGNOUYc&t=9s
위 트랙들을 보며 몇개 정리한 글, 자세한 것은 위 링크 참조바ram
먼저 기본 방식 세 가지를 살펴보겠슴니당
이는 초기화 구문으로 전달하는 방식, 파라미터로 전달하는 방식과 다르지 않다라고 설명 하심
테스트 하는 경우는 초기화 시에 의존하고 있는 클래스를 파라미터로 넘겨주면 된다.
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) // 이니셜라이저에 파라미터로 전달
}
}
이는 외부에서 변경이 가능하다, 초기화 구문에 대한 컨트롤이 없는 경우 실행 가능한 옵션이다.
예를 들어, 스토리보드를 사용하는 뷰 컨트롤러가 있는 경우에, 이니셜라이저는 뷰를 구성하기 위해 사용된다. 여기서는 사용하기가 어렵다고 설명하신듯 ?
테스트 하는 경우
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)
}
}
이는 함수의 파라미터에 전달하는 방식입니다.
기존 메소드에 테스트가 필요한 경우 변경할 필요없이 가능한 옵션입니다.
예시 코드는 다음과 같습니다.
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)
}
}
발표하신 분은 싱글톤에 대해 간단히 설명하셨고, 이것을 어떻게 사용할지, 왜 내가 필요한지 이런 부분들을 생각해서 간단하게 사용하라고 하심. 또 강조하신 점은, 하나의 클래스엔 최대한 하나의 책임을 가지게 설계하라는 부분도 강조 하셨스. 무작정 사용 하는것은 좋지가 않다 ~
싱글톤을 태수투 하눈 예제 코드
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)
}
}
얘도 싱글톤과 마찬가지로 생성에 관련된 디자인 패턴임. 구체적인 타입을 명시하지 않는 오브젝트들을 만드는것에 초점을 둔다.
목적 : hide/encapsulate
구체적인 세부 사항은 숨기고 캡슐화 한다는 뜻.
장점 : 사용자는 생성과 관련된 내부 논리에 대해 알 필요가 없다. 또 많은 종속성에 의존할 필요 없이 이 친구만 의존해도 되는 부분 ?
여기서는 최대한 간단히 설명했으므로, 자세한 내용은 팩토리 메서드와 추상화 팩토리?(Abstract Factory)를 체크해보라고 하심
예제 코드
여기서 셀을 누르면 실행되는 코드를 보면 TradingManager()
가 존재함. 이 친구는 이전 뷰컨에는 없는 의존성이다. 이는 문제가 발생할 수 있는 좋지 않은 코드임. 여기에 대한 통제권(control)도 없고 무엇이 되는지도 모르기 때문임.
이런 경우를 방지하기 위해 의존성 컨테이너와 뷰 컨을 어케 만드는지 알고 있는 팩토리를 만들 수 있다.
위 팩토리 코드를 이용해서 이전 코드를 리팩토링 해보면 다음과 같다
이는 서비스들과 의존성 인스턴스들을 유지하고 접근을 제공하는 패턴이다.
기본적으로 인스턴스의 큰 싱글톤이다. 다른 인스턴스를 보관하는 장소로, 이들을 만드는 방법을 알려주는 것임.
구현하려면 기본적으로 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
이므로 같은 파일에서 이를 행해야 합니다.
아래는 테스트코드의 예시입니다.
상당히 길었네여,, 코드가 너무 많긴 합니다만 오히려 이런 코드들이 많아서 좀 더 생각하는 기회가 되었지 않나 개인적으론 생각합니다.