๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/Architecture

[Design Pattern] MVP ํŒจํ„ด์ด๋ž€?

by Fomagran ๐Ÿ’ป 2021. 8. 11.
728x90
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š” Foma๐Ÿ‘Ÿ ์ž…๋‹ˆ๋‹ค!

 

์˜ค๋Š˜์€ Model - View - Presenter๋กœ ์ด๋ค„์ ธ์žˆ๋Š” M.V.P ๋””์ž์ธ ํŒจํ„ด์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ง€๊ธˆ๋ถ€ํ„ฐ ์ž์„ธํžˆ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


MVP๋ž€?

 

Model - View - Presenter ๋กœ ์ด๋ค„์ง„ ๋””์ž์ธ ํŒจํ„ด์ด๋ฉฐ MVC์—์„œ Controller๊ฐ€ ํ•˜๋Š” ์—ญํ• ์„ 

 

Presenter๊ฐ€ ํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

"๊ทธ๋Ÿฌ๋ฉด MVC ์“ฐ๋ฉด ๋˜์ง€ ์™œ MVP๋ฅผ ์จ?"

 

MVC๋Š” Model๊ณผ View๊ฐ€ ์„œ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด ์˜์กด๊ด€๊ณ„๋ฅผ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ MVP๋Š” Model๊ณผ View ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๊ณ  ์˜ค์ง Presenter๋ฅผ ํ†ตํ•ด์„œ ์ƒํƒœ๋‚˜ ๋ณ€ํ™”๋ฅผ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

 

 

์ด๋ ‡๊ฒŒ View์™€ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์ด ์™„์ „ํžˆ ๋ถ„๋ฆฌ๊ฐ€ ๋˜์–ด ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•ด์ง„๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ทฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๊ณผ์ •์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•˜๋ฉด

 

1. View์— ์‚ฌ์šฉ์ž์˜ ์ธํ„ฐ๋ž™์…˜์ด ๋“ค์–ด์˜จ๋‹ค.

 

2. View๋Š” Presenter์— ์•ก์…˜์ด ๋“ค์–ด์™”๋‹ค๊ณ  ์ „๋‹ฌํ•œ๋‹ค.

 

3. Presenter๋Š” View ์•ก์…˜๋Œ€๋กœ Model์„ ๊ตฌ์„ฑํ•œ๋‹ค. (๋ณดํ†ต Service๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.)

 

4. Update๋œ Presenter์˜ ๋ฐ์ดํ„ฐ๋ฅผ View์— ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ MVP์˜ ๋‹จ์ ์€ View์™€ Model์„ ๋ถ„๋ฆฌ์‹œ์ผœ MVC๋‚˜ Apple์˜ MVC์—์„œ ํ•˜๊ธฐ ํž˜๋“ค์—ˆ๋˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•ด์กŒ์ง€๋งŒ

 

View์™€ Presenter์˜ ์˜์กด๊ด€๊ณ„๊ฐ€ ๊ฐ•ํ•ด์ง€๊ณ  Controller ๋Œ€์‹  Presenter๊ฐ€ ๋ณต์žกํ•ด์ง€๋Š” ๋ฌธ์ œ๋Š” ์—ฌ์ „ํžˆ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค.


Model

 

Character์˜ ์ด๋ฏธ์ง€,์‚ฌ์ง„,์„ฑ๋ณ„,๋‚˜๋ผ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.

 

import UIKit

struct MVP_Character {
    
    //ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•  ๋ฐ์ดํ„ฐ
    let image:UIImage
    let name:String
    let gender:String
    let country:String
    
    //์ดˆ๊ธฐํ™”
    init(name:String,image:UIImage,gender:String,country:String) {
        self.name = name
        self.image = image
        self.gender = gender
        self.country = country
    }
}

Protocol

 

Presenter์— ํ•„์š”ํ•œ ํ•จ์ˆ˜๋‚˜ ๋ณ€์ˆ˜ ๋“ฑ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

protocol MVP_CharacterPresenterProtocol {
    var character:MVP_Character? { get set }
    func decrementIndex()
    func incrementIndex()
}

Presenter

 

Protocol์—์„œ ์ •์˜ํ•œ ํ•จ์ˆ˜์™€ ๋ณ€์ˆ˜๋“ค์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

 

๋ณดํ†ต Service๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  View์—์„œ ๋ฐ›์€ ์ธํ„ฐ๋ž™์…˜๋Œ€๋กœ ์š”์ฒญํ•˜์—ฌ Presenter์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

 

(๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋ฅผ ์œ„ํ•ด ๋ฏธ๋ฆฌ ๋ชจ๋ธ๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” characters๋ฅผ ๋งŒ๋“ค์–ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.)

 

View์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ character๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  View์—์„œ ์ผ์–ด๋‚˜๋Š” ์•ก์…˜์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๊ฒŒ๋” ๊ตฌํ˜„ํ•ด๋†“์Šต๋‹ˆ๋‹ค.

 

//์ „์ฒด ๋ชจ๋ธ ๋ฐ์ดํ„ฐ
let characters:[MVP_Character] = [
    MVP_Character(name:"์Šคํฐ์ง€๋ฐฅ",image: #imageLiteral(resourceName: "แ„‰แ…ณแ„‘แ…ฉแ†ซแ„Œแ…ตแ„‡แ…กแ†ธ"), gender: "๋‚จ์ž", country:"๋ฏธ๊ตญ"),
    MVP_Character(name:"๋šฑ์ด",image: #imageLiteral(resourceName: "แ„„แ…ฎแ†ผแ„‹แ…ต"), gender: "๋‚จ์ž", country:"๋ฏธ๊ตญ"),
    MVP_Character(name:"์ง•์ง•์ด",image: #imageLiteral(resourceName: "แ„Œแ…ตแ†ผแ„Œแ…ตแ†ผแ„‹แ…ต"), gender: "๋‚จ์ž", country:"๋ฏธ๊ตญ"),
    MVP_Character(name:"ํ”Œ๋žญํฌํ†ค",image: #imageLiteral(resourceName: "แ„‘แ…ณแ†ฏแ„…แ…กแ†ผแ„แ…ณแ„แ…ฉแ†ซ"), gender: "๋‚จ์ž", country:"๋ฏธ๊ตญ"),
    MVP_Character(name:"ํํ์—ฌ์‚ฌ",image: #imageLiteral(resourceName: "แ„‘แ…ฉแ†ผแ„‘แ…ฉแ†ผแ„‡แ…ฎแ„‹แ…ตแ†ซ"), gender: "์—ฌ์ž", country:"๋ฏธ๊ตญ")
]

class MVP_CharacterPresenter:MVP_CharacterPresenterProtocol {
    
    var character: MVP_Character?
    
    var index:Int = 0
    
    func incrementIndex() {
        //์ด์ „ ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ
        if index > 0 {
            index -= 1
        }
        character = characters[index]
    }
    
    func decrementIndex() {
        //๋‹ค์Œ ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ
        if index < 4 {
            index += 1
        }
        character = characters[index]
    }
}

View

 

์•„๋ž˜์™€ ๊ฐ™์ด ์บ๋ฆญํ„ฐ์˜ ์ด๋ฏธ์ง€,์„ฑ๋ณ„,์ด๋ฆ„,๋‚˜๋ผ๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ๋‹ค์Œ ์บ๋ฆญํ„ฐ๋กœ ๋„˜์–ด๊ฐ€๋Š” Next๋ฒ„ํŠผ๊ณผ

 

์ด์ „ ์บ๋ฆญํ„ฐ๋กœ ๋Œ์•„๊ฐ€๋Š” Previous ๋ฒ„ํŠผ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

 

View๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด Presenter๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณค Button์˜ ์•ก์…˜์„ Presenter์—์„œ ๊ตฌํ˜„ํ•ด๋†“์€ ๊ธฐ๋Šฅ์— ๋งž๊ฒŒ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

 

์•ก์…˜์— ๋”ฐ๋ผ ๋ฐ”๋€ Presenter character์˜ ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ View๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค/

 

import UIKit

class MVP_CharacterViewController: UIViewController {
    
    //MARK:- IBOutlets
    
    @IBOutlet weak var country: UILabel!
    @IBOutlet weak var name: UILabel!
    @IBOutlet weak var gender: UILabel!
    @IBOutlet weak var image: UIImageView!
    @IBOutlet weak var nextButton: UIButton!
    @IBOutlet weak var previousButton: UIButton!
    
    //MARK:- Properties
    
    let presenter:MVP_CharacterPresenterProtocol = MVP_CharacterPresenter()
    
    //๋ทฐ ์—…๋ฐ์ดํŠธ
    private func updateView() {
        //์ƒˆ๋กœ์šด ๋ชจ๋ธ์— ๋งž๊ฒŒ ๋ทฐ ๋ณ€๊ฒฝ
        self.image.image = presenter.character?.image
        self.name.text = presenter.character?.name
        self.gender.text = presenter.character?.gender
        self.country.text = presenter.character?.country
    }
    
    @IBAction func tapPreviousButton(_ sender: Any) {
        presenter.decrementIndex()
        updateView()
    }
    
    @IBAction func tapNextButton(_ sender: Any) {
        presenter.incrementIndex()
        updateView()
    }
}

 

์‹คํ–‰ํ™”๋ฉด

 


Test

 

View์™€ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์ด ๋ถ„๋ฆฌ๊ฐ€ ๋˜์—ˆ์œผ๋ฏ€๋กœ Test๊ฐ€ ์šฉ์ดํ•˜๋‹ค๊ณ  ์•ž์„œ ๋ง์”€๋“œ๋ ธ์Šต๋‹ˆ๋‹ค.

 

๋˜ํ•œ Protocol๋กœ Presenter์˜ ๊ธฐ๋Šฅ์˜ ์ •์˜ํ–ˆ๋˜ ๊ฒƒ๋„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ์˜€๋Š”๋ฐ์š”.

 

์•„๋ž˜์™€ ๊ฐ™์ด Mock ๋ฒ„์ „์˜ Presenter๋ฅผ Protocol์„ ์ฑ„ํƒํ•ด ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

import Foundation
@testable import Architecture_Example

class Mock_CharacterPresenter:MVP_CharacterPresenterProtocol {
    var character: MVP_Character?
    
    var index:Int = 3
    
    func incrementIndex() {
        //๋‹ค์Œ ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ
        if index < 4 {
            index += 1
        }
    }
    
    func decrementIndex() {
        //์ด์ „ ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ
        if index > 0 {
            index -= 1
        }
    }
}

 

์œ„์˜ Mock Presenter๋ฅผ ์ด์šฉํ•ด์„œ ํ”„๋ ˆ์  ํ„ฐ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

import XCTest
@testable import Architecture_Example

class CharacterPresenterTests: XCTestCase {

    var sut:Mock_CharacterPresenter!
    
    override func setUpWithError() throws {
       sut = Mock_CharacterPresenter()
    }

    override func tearDownWithError() throws {
        sut = nil
    }

    func testSignupPresenter_WhenTapNextButton_ShouldIndexIncrement1() throws {
        let index = sut.index
        sut.incrementIndex()
        XCTAssertEqual(sut.index, index+1)
    }
    
    func testSignupPresenter_WhenIndexGreaterThanZero_ShouldIndexDecrement1() throws {
        let index = sut.index
        sut.decrementIndex()
        XCTAssertEqual(sut.index, index-1)
    }
}
728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€