티스토리 뷰
MVC의 구현방법부터 테스트코드까지 다뤄보겠습니다.
https://eastbyeden.tistory.com/8
[Swift][MVC] MVC는 무엇일까?
MVC패턴은 ios개발에 사용되는 아키텍쳐(MVC, MVP, MVC-N, MVVM, VIPER) 중 하나입니다. 먼저 아키텍쳐의 사용이유는 - 개체들의 책임을 균형있게 분리하기 위해 (Solid의 Single Responsibility Principle - 단일..
eastbyeden.tistory.com
이전의 MVC소개글 입니다.
MVC보다 MVVM을 채택하는 이유는 무엇일까요?
라고 하면 당장 떠오르는 이유는 2가지입니다.
1. Testable하지 않다.
2. Massive ViewController.
Testable하지 않은 이유.
-> MVVM이 Testable한 이유. -> ViewModel이 변경되어, 어떤 작업이 실행된 결과를 확인하기 좋음.
그렇다면 MVC에서도 Model이 ViewModel을 갖도록 하면 어떨까? : 꼭 ViewModel이 아니더라도 상태값을 저장할 수 있다면 OK.
Massive ViewController가 되는 이유.
-> Massive ViewController가 MVC의 잘못일까..? 그 정도 규모의 프로젝트를 해당방식과 같이 개발한다면 ViewModel도 Massive해지지 않을까요..?
그렇다면 객체를 책임에 따라 더 나눠보면 해결할 수 있지 않을까?
이와 같은 구조로 구현해보겠습니다.
View에서는 +, - 버튼과 Label이 있습니다.
+가 눌리면 Label의 수는 더해지고, -가 눌리면 Label의 수는 줄어듭니다.
ViewController는 View, Usecase를 구체타입으로 소유하고 있습니다.
View와 Usecase는 ViewController에 delegate를 이용하여 ViewController의 함수를 호출합니다.
import UIKit
protocol CountViewDelegate: AnyObject {
func plusButtonTapped()
func minusButtonTapped()
}
final class CountView: UIView {
private var plusButton = UIButton()
private var minusButton = UIButton()
private var countLabel = UILabel()
weak var delegate: CountViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
layout()
attribute()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func updateCountLabel(with count: Int) {
countLabel.text = "\(count)"
}
private func attribute() {
plusButton.addAction(UIAction(handler: { [weak self] action in
self?.delegate?.plusButtonTapped()
}), for: .touchUpInside)
minusButton.addAction(UIAction(handler: { [weak self] action in
self?.delegate?.minusButtonTapped()
}), for: .touchUpInside)
countLabel.text = "0"
plusButton.setTitle("+", for: .normal)
plusButton.setTitleColor(.black , for: .normal)
minusButton.setTitle("-", for: .normal)
minusButton.setTitleColor(.black, for: .normal)
}
}
- View
autolayout을 잡아주는 코드는 생략하였습니다. ( layout() )
버튼들의 이벤트만 ViewController로 전달하고 있습니다.
import Foundation
protocol CountUsecaseDelegate: AnyObject {
func countUpdated(count: Int)
}
final class CountUsecase {
var count = 0 {
willSet(val) {
delegate?.countUpdated(count: val)
}
}
weak var delegate: CountUsecaseDelegate?
func pluseCount() {
count += 1
}
func minusCount() {
count -= 1
}
}
- Usecase ( Model )
버튼이 더해지는 로직을 처리하고, count값을 저장합니다.
count값은 ViewModel과 같은 역할을 합니다.
import UIKit
class ViewController: UIViewController {
private lazy var countView = CountView(frame: view.frame)
private var usecase = CountUsecase()
override func viewDidLoad() {
super.viewDidLoad()
countView.delegate = self
usecase.delegate = self
view = countView
}
}
extension ViewController: CountViewDelegate {
func plusButtonTapped() {
usecase.pluseCount()
}
func minusButtonTapped() {
usecase.minusCount()
}
}
extension ViewController: CountUsecaseDelegate {
func countUpdated(count: Int) {
countView.updateCountLabel(with: count)
}
}
- Controller
View에서의 이벤트를 받고, 그에 맞는 로직을 Usecase에게 시킵니다.
Usecase에서 Controller가 요청한 로직이 완료되면, Delegate를 통해 Controller에게 알려줍니다.
해당 데이터를 View에게 업데이트하라고 요청합니다.
여기까지 MVC 사용방법이였습니다.
- 테스트
MVVM의 장점은 ViewModel이 View에게서 독립적이기에 따로 테스트가 가능하다.
저는 Usecase를 View와 독립적으로 구현했기에 Usecase의 동작들을 추상화하여 Usecase만 테스트가 가능합니다!
그럼 먼저 Usecase를 추상화 하겠습니다.
protocol CountManagable {
var count: Int { get set }
func pluseCount()
func minusCount()
}
그럼 stub객체를 생성해볼까요?
import Foundation
@testable import WhatIsMVC
final class CountUsecaseStub: CountManagable {
var count: Int = 0
func pluseCount() {
count += 1
}
func minusCount() {
count -= 1
}
}
실 객체와 다를게 없지요.
이번 예시는 간단하게 +, - 로직만 있습니다.. 하지만 실제 개발 시 서버에 요청하는 작업이 있다면, 실서버에 접근하면 안되기에 행동을 추상화한 Stub객체를 사용하였습니다.
이렇게 되면 Usecase만 따로 테스트가 가능하겠네요!
import XCTest
@testable import WhatIsMVC
class WhatIsMVCTests: XCTestCase {
var usecase: CountManagable!
func test_usecase에서_minusCount가_호출되었을때_count가_줄어들어_업데이트된다() throws {
//give
usecase = CountUsecaseStub()
//when
usecase.minusCount()
//then
XCTAssertEqual(usecase.count, -1)
}
}
감사합니다.
'iOS > 스위프트' 카테고리의 다른 글
[iOS][CS,OS] class, struct의 차이는 무엇일까? (0) | 2022.09.05 |
---|---|
[Swift][MVVM] MVVM의 진가는 테스트에서 나온다. (0) | 2022.07.22 |
[Swift][DI] 의존성 주입 라이브러리. (0) | 2022.07.12 |
[iOS][Moya] Moya 네트워크 테스트 (with: Rx를 곁들인) (0) | 2022.06.29 |
[iOS][HIG] HIG란 무엇일까? (0) | 2022.05.12 |