Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Xcode
- 애니메이션
- RxCocoa
- uitableview
- UITextView
- Refactoring
- swift documentation
- 리펙터링
- UICollectionView
- ios
- uiscrollview
- Clean Code
- 리팩토링
- tableView
- 스위프트
- Observable
- Human interface guide
- Protocol
- SWIFT
- HIG
- clean architecture
- combine
- 리펙토링
- map
- swiftUI
- rxswift
- ribs
- collectionview
- 클린 코드
- MVVM
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[Clean Architecture] 5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙 본문
Clean Architecture/Clean Architecture 코드
[Clean Architecture] 5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙
jake-kim 2021. 9. 20. 18:260. 코드로 알아보는 SOLID - 클래스 다이어그램 필수 표현
1. 코드로 알아보는 SOLID - SRP(Single Responsibility Principle) 단일 책임 원칙
2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙
3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙
4. 코드로 알아보는 SOLID - ISP(Interface Segregation Principle) 인터페이스 분리 원칙
5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙
6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환
7. 코드로 알아보는 SOLID - Network, REST (URLSession, URLRequest, URLSessionDataTask)
8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)
DIP
- Dependency Inversion Principle
- 의존성 역전 원칙
- 개념: 핵심 부분을 담당하는 모듈의 제어흐름과 다른 모듈들의 의존 방향이 반대
- 방법: 변동성이 큰 구현체에 의존하지 않고 추상 클래스에만 의존해야 한다는 정의
- interface는 구현체보다 변동성이 낮은점을 이용
Abstract Factory를 통해 구현
- 이상적인 의존관계: 핵심체인 Application의 제어 흐름과 다른 클래스의 의존 방향이 반대
- 좌측: factory를 사용하지 않은 경우
- 우측: factory를 사용한 경우
- 좌측 Factory를 안쓴 경우 코드
// 구현체 service
class ServiceConcrete_NotFactory {
func service() {}
}
// 핵심체인 Application에서 구현체에 의존하는 상태
class Application_NotFactory {
var service: ServiceConcrete_NotFactory?
init() {
service = makeService()
}
/// 구현체에 의존하는 상태
func makeService() -> ServiceConcrete_NotFactory {
return ServiceConcrete_NotFactory()
}
}
- Factory를 사용한 경우
- Fafctory
// Service도 protocol이므로 추상 클래스끼리 의존성
protocol ServiceFactory {
func makeService() -> Service
}
// 핵심체가 아닌 Factory클래스의 구현체에서는 다른 구현체 의존해도 무방
class ServiceConcrete_ApplyFactory: ServiceFactory {
func makeService() -> Service {
return ServiceConcreteImpl()
}
}
- Service
protocol Service {
func service()
}
class ServiceConcreteImpl: Service {
func service() {
}
}
- Application
// 핵심체인 Application에서는 모두 protocol에만 의존
class Application_ApplyFactory {
let serviceFactory: ServiceFactory
var service: Service?
init(serviceFactory: ServiceFactory) {
self.serviceFactory = serviceFactory
service = makeService()
}
func makeService() -> Service {
return serviceFactory.makeService()
}
}
DIP를 이용한 코드의 장점
- 변경되기 쉬운것에 의존하지 않기 때문에, 변경이 잦아도 코드에서 변경해야할 부분이 적은 장점
- testable한 코드 작성에 용이: 핵심체는 protocol에만 의존하기 때문에 protocol의 구현체를 갈아끼워, testable 코드 작성에 용이
DIP를 이용한 testable 코드 작성 예제
- 구매할 상품을 고르고, 특가에 따라 금액을 달리하여 계산하도록 하는 앱
- business logic 요구사항: "특가"이면 원가의 * 0.8만 계산 (useCase에 반영)
- 요구사항에서 사용되는 Entity 정의
struct Product {
let title: String
let price: Double
let isSale: Bool
var isSelected: Bool = false
}
- ProductListUseCase: 요구사항에 따른 business logic이 존재
protocol ProductListUseCase {
func calc(_ productList: [Product]) -> Double
}
final class ProductListUseCaseImpl: ProductListUseCase {
func calc(_ productList: [Product]) -> Double {
var totalFee = 0.0
productList.forEach { product in
totalFee += product.isSale ? product.price * 0.8 : product.price
}
return totalFee
}
}
- ProductListViewModel: UseCase를 가지고 있고 Input, Output에 따라 useCase호출하고 viewController에 값 던져주는 역할
protocol ProductListViewModelInput {
func didTapCalButton()
func didTapBuyButton()
func didTapCellForRow(at index: Int)
}
protocol ProductListViewModelOutput {
var totalFee: BehaviorRelay<String> { get }
var sampleDataSource: [Product] { get }
}
protocol ProductListViewModel: ProductListViewModelInput, ProductListViewModelOutput {}
class ProductListViewModelImpl: ProductListViewModel {
let productListUseCase: ProductListUseCase
init(productListUseCase: ProductListUseCase) {
self.productListUseCase = productListUseCase
}
// Outout
var totalFee: BehaviorRelay<String> = .init(value: "총 금액 = \(0)")
var sampleDataSource = [Product(title: "아이패드 프로 5세대(13`)", price: 300, isSale: true),
Product(title: "아이폰13", price: 200, isSale: false),
Product(title: "애플워치7", price: 100, isSale: false),
Product(title: "아이폰SE", price: 50, isSale: true),
Product(title: "아이폰12", price: 130, isSale: true),
Product(title: "애플워치6", price: 70, isSale: true),]
// Input
func didTapCalButton() {
let resultFee = productListUseCase.calc(sampleDataSource.filter { $0.isSelected })
totalFee.accept("총 금액 = \(resultFee)")
}
func didTapBuyButton() {
totalFee.accept("구매 완료")
}
func didTapCellForRow(at index: Int) {
sampleDataSource[index].isSelected.toggle()
}
}
- DIP를 위하여 Abstract Factory패턴인 ProductListDIContainer
final class ProductListDIContainer {
lazy var productListViewController: ProductListViewController = {
return ProductListViewController.create(with: productListViewModel)
}()
// Private
private lazy var productListUseCaseImpl: ProductListUseCase = {
return ProductListUseCaseImpl()
}()
private lazy var productListViewModel: ProductListViewModel = {
return ProductListViewModelImpl(productListUseCase: productListUseCaseImpl)
}()
}
Test 코드 작성
- 현재 핵심적인 모듈인 ViewModel, UseCase는 protocol로 의존하게 되어있으므로 testable 코드
- ProductListUseCase 테스트 코드 작성: UseCase를 테스트할 땐 UseCase를 두고 나머지 요인을 변경해가며 기대하는 값이 잘 나오는지 체크하는 것
import XCTest
@testable import SOLID
// useCase를 테스트하는 경우 - useCase는 그대로 두고 다른 요인을 바꾸어도 기대하는 값이 나오는지 체크
class ProductListUseCaseTests: XCTestCase {
var sampleDataSource = [Product(title: "아이패드 프로 5세대(13`)", price: 300, isSale: true),
Product(title: "아이폰13", price: 200, isSale: false),
Product(title: "애플워치7", price: 100, isSale: false),
Product(title: "아이폰SE", price: 50, isSale: true),
Product(title: "아이폰12", price: 130, isSale: true),
Product(title: "애플워치6", price: 70, isSale: true),]
func testProductListUseCase_whenAllIsNotSaleProduct_thenCalculateAccuracy() {
// given
let useCase = ProductListUseCaseImpl()
// when
let data = sampleDataSource.map { Product(title: $0.title, price: $0.price, isSale: false) }
// then
let result = useCase.calc(data)
let expectValue = data.map { $0.price }.reduce(0, +)
XCTAssertTrue(expectValue == result)
}
func testProductListUseCase_whenAllIsAllSaleProduct_thenCalculateAccuracy() {
// given
let useCase = ProductListUseCaseImpl()
// when
let data = sampleDataSource.map { Product(title: $0.title, price: $0.price, isSale: true) }
// then
let result = useCase.calc(data)
let expectValue = data.map { $0.price * 0.8 }.reduce(0, +)
XCTAssertTrue(expectValue == result)
}
}
- ViewModel 테스트: ViewModel은 그대로 두고, ViewModel에서 사용하는 UseCase, api, repository등을 변경해가며 viewModel의 input과 ouput을 주면서 기대하는 값이 나오는지 테스트
@testable import SOLID
import XCTest
// ViewModel 테스트 - ViewModel은 그대로 두고, 안에서 사용하는 UseCase를 mock으로 바꾸어 가며 테스트
class ProductListViewModelTests: XCTestCase {
let sampleDataSourceForTest = [Product(title: "아이패드 프로 5세대(13`)", price: 300, isSale: true),
Product(title: "아이폰13", price: 200, isSale: false),
Product(title: "애플워치7", price: 100, isSale: false),
Product(title: "아이폰SE", price: 50, isSale: true),
Product(title: "아이폰12", price: 130, isSale: true),
Product(title: "애플워치6", price: 70, isSale: true),]
class ProductListUseCaseMock: ProductListUseCase {
func calc(_ productList: [Product]) -> Double {
var totalFee = 0.0
productList.forEach { product in
totalFee += product.isSale ? product.price * 0.5 : product.price
}
return totalFee
}
}
func test_whenUseCaseChanged_thenSuccessAccuracy() {
// given
let useCase = ProductListUseCaseMock()
let viewModel = ProductListViewModelImpl(productListUseCase: useCase)
// when (= input)
viewModel.didTapCellForRow(at: 0)
viewModel.didTapCellForRow(at: 1)
viewModel.didTapCalButton()
// then (= output)
XCTAssertEqual(viewModel.totalFee.value, "총 금액 = \(300 * 0.5 + 200)")
}
}
* 이 밖의 ProductViewController, ProductTableViewCell 및 전체 소스 코드: https://github.com/JK0369/SOLID
'Clean Architecture > Clean Architecture 코드' 카테고리의 다른 글
Comments