티스토리 뷰

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)
    }
}

 

 

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
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
글 보관함