[iOS] ๐Ÿ“ฆ ์˜์กด์„ฑ ๊ด€๋ฆฌ ๊ฒฝํ—˜์ผ์ง€ (w. Tuist, Swinject)

Charlieยท2024๋…„ 3์›” 14์ผ
post-thumbnail

ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ๊ฐ€ ๋น„๋Œ€ํ•ด์งˆ์ˆ˜๋ก ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ์ค‘์š”ํ•ด์ง€๊ณ  ์ด์— ๋”ฐ๋ผ ์˜์กด์„ฑ ๊ด€๋ฆฌ๋ฅผ ์ž˜ ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜์กด์„ฑ์ด ์™ธ๋ถ€์—์„œ ๋‚ด๋ถ€๋กœ ํ–ฅํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฒ ์ €ํžˆ ํ•ด์•ผํ•˜๊ณ , ๋ชจ๋“ˆ ๊ฐ„ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๊ฒฐํ•ฉ์ด ๋˜๋ฏ€๋กœ ํ•„์š” ์‹œ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์„ ๊ฐˆ์•„ ๋ผ์šธ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.



Tuist๋กœ ํƒ€๊ฒŸ ์˜์กด์„ฑ ๊ด€๋ฆฌํ•˜๊ธฐ

Tuist๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ๋ชจ๋“ˆ์˜ ์˜์กด์„ฑ๊ณผ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ธฐ์กด์—๋„ ์—ฌ๋Ÿฌ ์ฐจ๋ก€ ์‚ฌ์šฉ ๊ฒฝํ—˜์ด ์žˆ์—ˆ๊ณ  ์‰ฝ๊ฒŒ ๋ชจ๋“ˆํ™”๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Tuist๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธฐํšŒ์— ๊ธฐ์กด๊ณผ๋Š” ์ƒˆ๋กœ์šด ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•œ ๊ฒƒ์€ ์•„๋ž˜์™€ ๊ฐ™์ด ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ์•ฑ ํƒ€๊ฒŸ๊ณผ ํ…Œ์ŠคํŠธ ํƒ€๊ฒŸ ์˜์กด์„ฑ์„ ๋”ฐ๋กœ ๋ฐ›์•„ ํƒ€๊ฒŸ์— ๋งž๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ Project๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ App Target์˜ ์˜์กด์„ฑ๋“ค๊ณผ Test Target ์˜์กด์„ฑ์„ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ๊ฐ Target์— ๋งž๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

let project = Project.makeModule(name: "Feature",
                                 product: .staticFramework,
                                 appTargetDependencies: [.SPM.RxCocoa,
                                                         .SPM.RxDataSources,
                                                         .SPM.RxGesture,
                                                         .SPM.RxSwift,
                                                         // ...
                                                         .Workspace.DesignSystem,
                                                         // ...
                                                         ],
                                 testTargetDependencies: [.Workspace.TestUtility]
)

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ํ…Œ์ŠคํŠธ์™€ ๊ด€๋ จํ•œ ๊ฐ์ฒด ๋ฐ ํ•จ์ˆ˜๋“ค์„ ๋ณ„๋„์˜ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ•˜์˜€๊ณ , ํ…Œ์ŠคํŠธ ํƒ€๊ฒŸ์—๋งŒ ์ด๋ฅผ ์˜์กดํ•˜๋„๋ก ํ•˜์—ฌ ์ •ํ™•ํ•˜๊ณ  ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.






Swinject๋กœ ๊ฐ์ฒด ๊ด€๋ฆฌํ•˜๊ธฐ

๊ฐ์ฒด๋“ค์˜ ์˜์กด์„ฑ์€ Swinject๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. Swinject์˜ ์˜์กด์„ฑ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ™œ์šฉํ•ด ๋ณด๋‹ค ๊ฐ„ํŽธํ•˜๊ฒŒ register, resolveํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์˜์กด์„ฑ ๋“ฑ๋ก ์‹œ ๊ฐ์ฒด์˜ ์ƒ๋ช… ์ฃผ๊ธฐ์™€ ๊ตฌํ˜„ ํ”„๋กœํ† ์ฝœ ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์–‘ํ•œ ๋ ˆ์ด์–ด๋กœ ๋‚˜๋ˆ„๊ณ  ์•ˆ์ชฝ์„ ๋ฐ”๋ผ๋ณด๋Š” ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์ ํ•ฉํ•  ๊ฒƒ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

Feature๋ชจ๋“ˆ์˜ ViewController, ViewModel๋“ฑ์˜ ๊ฐ์ฒด๋“ค๊ณผ
Core, Network๋“ฑ์˜ ๋ชจ๋“ˆ์˜ UseCase, Repository ๊ตฌํ˜„ ๊ฐ์ฒด๋“ฑ์˜ ์˜์กด์„ฑ์„ ๋ชจ๋‘ Swinject๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.



๐Ÿชž ๊ฐ์ฒด Mocking

Assembly๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์ด MockableAssembly๋ฅผ ํ†ตํ•ด ํ•„์š” ์‹œ Mock ๊ฐ์ฒด ์˜์กด์„ฑ๋„ ํ•จ๊ป˜ ๋“ฑ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

import Swinject

public protocol MockableAssembly: Assembly {
    func assembleActual(_ container: Container)
    func assembleMock(_ container: Container)
}

extension MockableAssembly {
    public func assemble(container: Container) {
        self.assembleActual(container)
        self.assembleMock(container)
    }
}
import Swinject

struct MyAssembly: MockableAssembly {
    func assembleActual(_ container: Container) {
    	container.register(MyRepository.self) { _ in return MyRepositoryImpl() }
        container.register(MyUseCase.self) { resolver in
        	let aService = resolver.resolve(AService.self)!
            let bService = resolver.resolve(BService.self)!
            
            return MyUseCaseImpl(aService: aService, bService: bService)
        }
    }
    
    func assembleMock(_ container: Container) {
        container.register(MyRepository.self, name: "Mock") { _ in return MockMyRepositoryImpl() }
        
        container.register(MyUseCase.self, name: "Mock") { resolver in
         	let aService = resolver.resolve(AService.self, name: "Mock)!
            let bService = resolver.resolve(BService.self, name: "Mock)!
            
            return MyUseCaseImpl(aService: aService, bService: bService)
        }
    }
}

๊ทธ๋ฆฌ๊ณ  ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ๋‚˜ ๋นŒ๋“œ ์Šคํ‚ค๋งˆ์—์„œ๋Š” ํŽธ๋ฆฌํ•˜๊ฒŒ Mock ๊ฐ์ฒด๋ฅผ resolveํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

#if DEBUG
	let viewModel = container.resolve(MyViewModel.self, name: "Mock")!
#else
	let viewModel = container.resolve(MyViewModel.self)!
#endif



๐Ÿ™„ MockableAssembly์€ ๋ณ„๋กœ์ธ ๋“ฏ

MockableAssembly๋ฅผ ํ†ตํ•ด ์‹ค์ œ ๊ตฌํ˜„ ๊ฐ์ฒด์™€ Mock ๊ฐ์ฒด๋ฅผ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ๊ตฌํ˜„ ๊ฐ์ฒด์™€ Mock ๊ฐ์ฒด๊ฐ€ ๋™์‹œ์— ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋Š” ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๊ตณ์ด MockableAssembly ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , assemble(container: ) ๋ฉ”์†Œ๋“œ์—์„œ ํ•„์š” ์‹œ ์ง์ ‘ ์Šคํ‚ค๋งˆ ์กฐ๊ฑด ๋ถ„๊ธฐ๋ฅผ ํ†ตํ•ด Mock ๊ฐ์ฒด๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ์ด๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜๋Š” ํŽธ์ด ๋” ๋‚ซ๋‹ค๊ณ  ์ƒ๊ฐ๋ฉ๋‹ˆ๋‹ค.

profile
Hello

0๊ฐœ์˜ ๋Œ“๊ธ€