본문 바로가기
개린이 이야기

디자인 패턴 - MVC 패턴에 관하여

by iOS 개린이 2023. 4. 4.

디자인 패턴

-간단하게 말하면 소프트웨어 개발방법을 공식화 한 것이다.

소프트웨어를 개발할 때 자주 발생하는 문제들이 있었고, 이를 해결하기 위해 사용되는 패턴, 구조를 공식화 한 것이다.

 

-디자인 패턴은 소프트웨어를 개발할 때, 발생하는 다양한 문제에 대한 재사용 가능한 템플릿 해결방법이다.

 

-디자인 패턴은 문제 해결에 대해 표준화된 접근 방식을 제공하여 개발자가 복잡한 설계 개념을 더 쉽게 이해하고 전달할 수 있도록 한다.

 

 

디자인 패턴의 장점

 

1. 반복되는 문제 해결

개발 중에 직면하는 문제들에 대해서 이미 입증된 해결책을 가지고 있는 패턴이기 때문에 효과적으로 문제를 해결할 수 있다.

 

2. 향상된 커뮤니케이션

개발자 간 커뮤니케이션에서 "우리는 State 패턴을 사용하고 있어요", "여기서는 Singleton 패턴을 사용하는 것이 어떤가요?" 등 코드의 구조에 대해 공통된 단어를 사용함으로써 아이디어를 쉽게 전달할 수 있다.

 

3. 유지보수 용이

모듈화되고 체계적인 패턴을 가지고 있으면 코드를 이해, 수정 등 유지보수를 하는 것이 용이해진다.

문제점 개선, 업데이트 등의 작업을 할 때 시간이 단축됨.

 

4. 코드 재사용성 용이

디자인 패턴 자체가 다양한 문제를 재사용가능한 템플릿을 통해 해결하는 것이라고 했죠?

디자인 패턴은 코드를 재사용 할 수 있도록 권장하기 때문에 코드의 재사용성이 용이하다.

 

 

-디자인 패턴을 적절하지 않은 상황에 적용한다면 코드가 지나치게 복잡해질 수 있다.

따라서 디자인 패턴이 어떤 상황에서 사용되면 좋은지를 아는 것이 중요하다. 즉, 해결할 문제를 명확하게 정의하고, 그에 맞는 디자인 패턴을 적용하는 것이 효과적으로 디자인 패턴을 사용하는 것이다.

 

 

MVC(Model-View-Controller)

-MVC 패턴은 디자인 패턴이라고 불리기도 하고, 아키텍처 패턴이라고 불리기도 한다. 근데 어차피 디자인 패턴이 아키텍쳐에 속해 있는 개념이고, 둘의 경계가 모호하기 때문에 굳이 구분에 매몰될 필요가 없다.

 

-Model, View, Controller로 구성이 되는 패턴을 말한다.

 

1. Model 

앱이 무엇을 할지 다루는 부분. 데이터를 처리, 비즈니스 로직을 담당하는 함수들이 정의된다. (데이터를 검색 및 저장하고 필요한 처리를 수행)

 

 

2. View

말 그대로 데이터를 기반으로 사용자에게 보이는 영역이다. (스토리보드를 비롯하여 인터페이스를 구축하는 영역)

데이터의 사용자 인터페이스 및 시각적 표현을 나타낸다. (모델의 데이터를 표시하고 컨트롤러로 보낼 사용자 입력을 수신)

 

View를 작성할 때는 재사용성이 강조된다.

화면에 들어가는 여러 요소들을 앱 전반에서 재활용할 수 있게 만드는 것이 중요한 것이다.

예를 들어 앱 전반에서 일관적으로 사용되는 디자인의 버튼이 있을 때, 필요할 때마다 해당 디자인을 코드로 작성하지 않고 MyButton 으로 따로 만들어서 관리해주면 재사용성이 용이해진다.

 

 

3. Controller

Model과 View 사이의 중개자 역할을 한다.

사용자가 View를 통해 상호작용 할 때 발생하는 사용자 입력을 받아 처리하고, 그에 따라 Model을 업데이트 한다.

또한 Model의 정보가 변경될 때 View를 업데이트 한다.

 

Controller는 해당 View마다 하나씩 붙어서 View에 맞는 로직을 포함하고 있기 때문에 View보다 재사용성이 떨어진다.

재사용성이 떨어지니 코드가 길어지는 경우가 많다.

 

 

MVC가 생겨난 이유

-MVC의 배경과 생겨난 이유에 대해서 알게 되면 이 패턴의 핵심이 무엇인지 알 수 있다.

과거의 프로그래머들이 수많은 프로그램들을 만들어보면서 코드가 많아지면 많아질수록 복잡해져서 코드를 파악하기도 힘들고, 나중에 기능을 수정할 때마다 대부분의 코드를 갈아엎어야 하는 문제가 자주 일어났다. 즉, 코드의 유지보수가 불편한 경우가 많았다.

개발자들은 코드를 계속해서 만들다가 어떤 경우에서 코드 구성을 이렇게 해보니 유지보수가 편하다는 것을 느끼고, 그 규칙성(패턴)들이 조금씩 보이기 시작한다. 패턴을 하나의 공식처럼 만들어서 논문으로 발표하게 되었고, 그렇게 MVC 패턴이 탄생한다. 

 

MVC가 탄생하게 된 핵심 이유는 유지보수를 편하게 하기 위해서다.

 

 

MVC의 특징

1. Model에서는 비즈니스 로직을, View에서는 사용자에게 보여지는 인터페이스를, Controller에서는 어떻게 Model을 활용해서 View를 업데이트할 것인지 정의한다. 

이렇게 각 구성 요소에 특정 역할을 부여하기 때문에 코드가 체계적이고 이해하기 쉬워진다.

 

2. 구성 요소의 역할을 분리하기 때문에 기능을 추가, 수정, 제거하는 것이 수월하다.

-> 유지보수가 용이하다.

 

3. 각 구성 요소의 역할 분리하기 때문에 구성 요소들의 재사용이 용이하다. 

예를 들어 같은 데이터 모델을 다른 곳에서도 사용해야 할 때, 모델을 따로 분리해두었기 때문에 재사용이 가능. 

 

 

 

MVC의 계층 간 소통

https://leeari95.tistory.com/52

 

Model과 View

-그림에서도 표현되어 있듯이 Model과 View는 직접 통신을 하지 않고, Controller를 통해 통신한다.

Model은 받아온 데이터에 맞게 저장할 형태를 만드는 것이 중요하고, UI에서 어떻게 보이는지는 신경쓰지 않는 것이다.

예를 들어 구조체 Person 이라는 데이터 모델에 생일 날짜 데이터를 저장하는 것은 괜찮지만, 이 생일 날짜 데이터를 어떤 식으로 파싱하고, 어떤 식으로 화면에 띄워줄지는 고민하지 않아도 되는 것이다.

 

 

View와 Controller

-View와 Controller는 대부분 유저가 View를 통해 발생시킨 이벤트를 Controller와 상호작용하는 경우다.

주로 Delegate, Data Source방식을 이용해서 Controller에 관련 코드를 작성한다. View에서 이벤트가 발생하면, 이벤트의 처리는 delgate를 통해 Cotroller에게 역할을 위임하는 것이다.

또한 View에서 action이 발생하면 target인 Controller에서 유저 action에 따른 필요한 함수를 호출할 수도 있다.

 

ex - 사용자의 입력

View는 사용자로부터 입력(ex - 버튼 클릭, 텍스트 입력 등)을 받으면 Controller로 보낸다. 

Controller는 받은 사용자 입력을 처리하고 적절한 작업을 결정한다.

 

 

Model과 Controller

-View에서 사용자 입력을 받았을 때, Model의 데이터를 업데이트해야 할 경우가 있다.

이 때 Controller는 Model에 있는 적절한 함수를 호출하여 데이터를 CRUD하는 작업을 수행하거나, Model 객체를 생성하여 직접 접근하여 작업을 수행한다.

 

-Model에서 Controller로 전달할 때는 Model의 데이터가 변화했다는 사실을 알려주어야 하는 것인데, 

일반적으로 2가지 방법이 있다. (Model은 Controller에 접근하지 않아야 함. Controller가 Model에 접근.)

 

1. Controller에서 Model에 있는 프로퍼티에 Key-Value Observing(KVO)을 이용해서 프로퍼티의 변화를 감지하는 방식을 사용한다. 프로퍼티의 변화가 감지되면 그에 따라 새로운 데이터를 받아올 수 있다.

 

2. Notification을 사용하는 방법. Notification을 이용해서 뭔가 변화했다는 알림을 보내면 Controller에서 알림을 받고 Model에 접근하여 새로운 데이터를 받아오는 방법이다.

 

 

 

 

MVC 패턴을 사용한 예시

 

"https://jsonplaceholder.typicode.com/posts"  <- 에서 JSON 데이터를 가져와 tableview의 cell에 뿌려주는 코드를 MVC 패턴을 적용해서 만들어본다. 

 

다른 MVC 패턴의 예시 코드는 View 부분을 스토리보드로 두고 사용하지만,

필자는 subView의 배치나 레이아웃을 snapkit을 사용하기 때문에 View를 따로 만들어준다.

 

Model

struct Post: Codable {
    let id : Int
    let title : String
    let body : String
}

 

 

Controller가 서버와 네트워크하기 위해 활용하는 서비스 클래스

class NetworkService {
    
    func fetchPosts(completion : @escaping (Result<[Post], Error>) -> Void) {
        //url 생성
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {return}
        //request 생성
        let request = URLRequest(url: url)
        
        //session 생성
        let session = URLSession(configuration: .default)
        
        //task 생성.
        let task = session.dataTask(with: request) { data, response, error in
            //만약 에러가 존재한다면 completion에 에러를 보냄.
            if let error = error {
                completion(.failure(error))
                return
            }
            
            //데이터가 존재하지 않는다면 completion에 에러를 보냄.
            guard let data = data else {
                let error = NSError(domain: "No data", code: 0, userInfo: nil)
                completion(.failure(error))
                return
            }
            
            do {
                let posts = try JSONDecoder().decode([Post].self, from: data)
                
                completion(.success(posts))
                
            }catch {
                completion(.failure(error))
            }
        }
        
        task.resume()
        
    }
}

 

View

class PostTableViewCell: UITableViewCell {
    static let identifier = "PostTableViewCell"

    let titleLabel = UILabel()
    let bodyLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        addSubViews()
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    private func addSubViews() {
        self.backgroundColor = .clear
        
        self.addSubview(titleLabel)
        titleLabel.textColor = .black
        titleLabel.font = .systemFont(ofSize: 16)
        titleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().inset(15)
            make.left.right.equalToSuperview().inset(20)
            make.height.equalTo(20)
        }
        
        self.addSubview(bodyLabel)
        bodyLabel.textColor = .black
        bodyLabel.font = .systemFont(ofSize: 16)
        bodyLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp_bottomMargin).offset(15)
            make.left.right.equalToSuperview().inset(20)
            make.height.equalTo(20)
        }
    }
        
}

 

 

Controller

class PostTableViewController: UITableViewController {

    let networkService = NetworkService()
    var posts : [Post] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(PostTableViewCell.self, forCellReuseIdentifier: PostTableViewCell.identifier)
        
        //closure self 캡쳐로 인한 순환참조 방지를 위해 [weak self]
        networkService.fetchPosts { [weak self] result in
            DispatchQueue.main.async { //UI와 관련된 작업은 메인쓰레드에서 처리.
                switch result {
                case .success(let posts):
                    self?.posts = posts
                    self?.tableView.reloadData()
                case .failure(let error):
                    print("Error : \(error.localizedDescription)")
                }
            }
        }
    }

    // MARK: - Table view data source
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: PostTableViewCell.identifier, for: indexPath) as! PostTableViewCell

        cell.titleLabel.text = posts[indexPath.row].title
        cell.bodyLabel.text = posts[indexPath.row].body
        
        return cell
    }

}

 

Model - 구조체 Post로 서버와 통신하여 가지고 올 데이터 모델 정의.

View - PostTableViewCell

Controller - PostTableViewController로 네트워킹을 통해 가져온 데이터를 View 부분인 tableview cell에 뿌려준다.

 

 

 

MVC 패턴 적용규칙

-MVC에 대한 기본적인 이론에 대해서 알고 있는 것도 중요하지만

실전에서 MVC를 지키면서 코딩 하는 방법에 대해서 알고 있는 것이 더 중요하다. (실전에서 사용하지 못하는 이론은 죽어있는 이론일뿐.)

따라서 MVC 패턴을 지키면서 코딩하는 방법에 대한 몇 가지 규칙을 알아보자.

 

1. Model은 Controller와 View에 의존하지 않아야 한다.

Model은 데이터와 관련된 부분이다 보니 언제든지 깔끔한 데이터를 꺼내 쓸 수 있도록 해야 한다.

따라서 Model 내부 코드에 Controller와 View에 관련된 코드가 존재하면 안된다.

 

MVC 예제 코드에서도 Model 내에는 데이터에 대한 정보만 존재하고, Controller나 View에 관련된 코드는 없었죠? 

 

 

2. View를 독립적으로 유지하자.

View는 데이터를 처리하거나 저장하는 것이 아니라 데이터를 표시하고, 사용자를 통한 이벤트를 전달하는데에 집중한다.

 

MVC 예제 코드에서도 View 부분은 유저에게 보여주는 내용에 대해서만 정의했죠? 

 

3. Controller는 Model과 View에 의존해도 된다.

Controller는 Model과 View의 중개자 역할이기 때문에 Controller 내부 코드에는 Model과 View에 관련된 코드가 있을 수 있다.

 

반대로 Model과 View가 Controller에 의존하는 것은 가능하지만, 의존해서는 안된다.(패턴에 어긋남.)

 

4. View가 Model로부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다.

MVC 예제에서 네트워킹을 통해 받아온 데이터를 View가 직접 받지 않았고, Controller가 받아온 데이터를 View에 뿌려주었죠?

 

 

 

MVC 패턴의 한계점

 

1. Model은 독립성을 유지하지만, View와 Controller는 밀접한 관계를 가지고 있다.

(예를 들어 UIViewController에서 보면 View의 수명주기를 관리(ex - viewDidLoad 등)하는 등)

View와 Controller가 밀접한 관계를 가지고 있으면 재사용성이 떨어지고, View와 Controller를 따로 테스트하는 것이 어려워진다.

 

2. View와 Model을 Controller가 혼자 관리하기 때문에 Controller에 과부하가 올 수 있다.

프로젝트의 규모가 커졌을 경우에 Life Cycle 관리, Delegate, Data Source 관리, 네트워크 요청, 데이터 요청 등 Controller가 너무 많은 책임을 맡게 된다.

Controller는 Model과 View 간의 통신을 관리하는 데 집중해야 하고, 이상적인 Controller는 슬림해야 한다.

이런 한계점으로 인해 MVC 패턴은 비교적 큰 규모의 프로젝트에서 사용하는 것이 힘들다.

 

 

 

 

 

 

 

Reference

-https://leeari95.tistory.com/52

-[iOS] MVC 디자인 패턴 정리 및 예제코드 (tistory.com)

-swift - MVVM 패턴 (feat. MVC) — MangDic_IOS (tistory.com)

-iOS 개발 - MVC 패턴과 UIKit의 ViewController | by Heechan | HcleeDev | Medium

-[10분 테코톡] 🧀 제리의 MVC 패턴 - YouTube

-생성 패턴 - 야곰닷넷 (yagom.net)

 

'개린이 이야기' 카테고리의 다른 글

Swifty한 Swift 코드에 관하여  (0) 2023.05.30
Kingfisher 라이브러리에 관하여  (0) 2023.05.28
SingleTon(싱글톤)에 관하여  (0) 2023.02.03
Infinite Carousel 구현  (0) 2023.01.21
Custom Splash 화면  (0) 2023.01.17