[iOS] MVVM, Clean Architecture에 대한 고찰

eung7_·2022년 5월 21일
1

iOS

목록 보기
16/17
post-thumbnail

Clean Architecture, 필요성을 깨닫다.

나는 주로 코드로 UI를 구현한다. 그래서 많은 UI객체들을 갖고 있는 VC에는 코드가 굉장히 길어지는 경험을 자주 했다.
앱의 버그가 발생하면 이 VC의 방대한 코드를 어디서부터 손을 봐야할지 막막했다.
데이터의 이동이 많아지고 앱이 커짐에 따라서 이 문제는 계속해서 문제가 되었다.

결국 Clean Architecture에 대한 필요성을 절실하게 깨달았고,
앱의 좋은 가독성이 유지 보수와도 연관된다는 것을 알았다.

이번에 상대적으로 방대한 앱을 만들어보았는데,
Clean Architecture라는 것은 앱 개발자로서 굉장히 중요하다는 것을 직접적으로 깨달은 계기가 되었다.
물론 이 Clean Architecture라는 것은 개개인마다 생각의 차이가 있다.
오늘은 MVVM에 대한 보편적인 이야기를 해보려고 한다.


MVVM이란?

  • MVVM이란 Model - View - ViewModel의 약자로써 불려진다.

  • 일반적으로 View는 iOS에서 UIViewController를 의미한다.

  • View와 Model은 서로 소통하지 않는다. 오직 징검다리 역할을 하는 ViewModel을 통해 소통한다.

  • ViewModel이란 단순하게 말하자면 오직 View를 위한 Model이다.

  • 여기서 Model은 단순히 하나를 뜻하지 않는다. 데이터를 가공한 정도에 따라서 Entitiy, Model, ViewModel로 분류할 수 있다.

여기까지가 간략한 MVVM에 대한 설명이다.
하지만 더 깊이 들어가보면 아직 설명해야 할 것들이 많다.

Model은 그 자체로 하나가 아니다.

위 사진은 데이터가 MVVM 구조 내에서 어떻게 변형되어 View까지 전달되는 지에 대한 모식도이다.
밑의 정사각형의 Entity, Model, ViewModel은 모두 Model로 분류된다.

  • Entity는 서버에서 받아온 원본 데이터이다. 즉 가공되지 않는 데이터이다.

  • Model은 Entity를 통해 앱에서 실제로 사용될 데이터로 가공하여 만들어진 데이터이다.

  • ViewModel은 Service를 통해 Model로 부터 만들어진다.

  • Model은 Repository를 통해 Entity로 부터 만들어진다.

나는 그래서 Repository와 Service를 @escaping Closure를 통해서 변형을 했다.
사실 아직 래퍼런스를 많이 찾아보진 않아서 이 방법이 좋다 라고 할 수는 없지만 개인적으로 괜찮은 방법같다.
예시 코드를 한번 보도록 하자.

import Alamofire

/// 서버에서 원본데이터를 가져온다.
class Repository {
    static func fetchMovies(from term: String, completion: @escaping ([Movie]) -> Void) {
        var components = URLComponents(string: "https://itunes.apple.com/search")!
        let search = URLQueryItem(name: "term", value: term)
        let media = URLQueryItem(name: "media", value: "movie")
        let entity = URLQueryItem(name: "entity", value: "movie")
        let limit = URLQueryItem(name: "limit", value: "20")
        components.queryItems =  [ search, media, entity, limit ]
        let url = components.url!
        
        AF
            .request(url)
            .validate()
            .responseDecodable(of: Result.self) { response in
                switch response.result {
                case .success(let result):
                    DispatchQueue.main.async {
                        completion(result.results)
                    }
                case .failure(let error):
                    print("Error! : \(error.localizedDescription)")
                }
            }
            .resume()
    }
}
  • completion이라는 탈출 클로저를 이용하여 원본 데이터의 배열을 전달하고 있다.

  • 여기서 원본 데이터를 Movie라는 객체의 타입이다.

  • 이 데이터를 나중에 Service가 받아서 적절히 변형해줄 것이다.

/// Repository를 이용해 Entity -> Model로 변형
class Service {
    static func fetchStarMovies(_ from: String, completion: @escaping ([StarMovie]) -> Void) {
        Repository.fetchMovies(from: from) { movies in
            let starMovies = movies.map { return StarMovie(poster: $0.poster, movieName: $0.movieName, trailer: $0.trailer, isStar: false)}
            DispatchQueue.main.async {
                completion(starMovies)
            }
        }
    }
}
  • 여기서 정의된 StarMovie는 Model에 해당한다.

  • Repository에서 콜백함수로 받은 데이터를 실제 앱에서 사용할 데이터로 변형하는 것을 코드에서 볼 수 있다.

여기서 둘다 전역 함수로 설정한 이유는 데이터를 받아오는 행위는 여러 화면에서 쓰일 수 있는 가능성을 열어둔 것이다.
바로 다음은 이 점에 대해서 알아보도록 하자.


MVVM 유의점

앞서 MVVM에 구조를 살펴봤지만 몇 가지 유의할 점이 있는 것 같아 적어본다.
처음 MVVM을 접했을 때 정해진 틀에 내 코드를 끼워맞추는 것이 너무 힘이 들었는데,
사실 이렇게 하지 않아도 무방하다는 것을 알았다. 정해진 게 없는 것이 아키텍쳐라고 생각한다.
내가 실제로 MVVM을 구현하고 몇 가지 깨달은 점을 적어볼까 한다.

  • Entity와 Model 사이에 차이가 없다면 Entity를 Model로 취급해도 상관없다.

  • 앱의 여러 곳에서 중복되는 메서드들이 있다면 그것을 싱글톤 객체로 따로 만들어 관리하는 것도 하나의 방법이다.

class StarMovieManager {
    static let shared = StarMovieManager()
}

extension StarMovieManager {
    func verifyInStarMovies(_ selectedMovie: StarMovie) -> StarMovie {
        if let starMovie = StarMovie.movies.first(where: { $0.trailer == selectedMovie.trailer }) {
            return starMovie
        } else {
            return selectedMovie
        }
    }
    
    func saveStarMovies() {
        let userdefaults = UserDefaults.standard
        let data = try? JSONEncoder().encode(StarMovie.movies)
        userdefaults.set(data, forKey: "StarMovies")
    }
    
    func loadStarMovies() {
        let userdefaults = UserDefaults.standard
        guard let starMovies = try? JSONDecoder().decode([StarMovie].self, from: userdefaults.data(forKey: "StarMovies") ?? Data()) else { return }
        StarMovie.movies = starMovies
    }
}

위의 StarMovieManager객체는 중복 메서드를 따로 모아놓은 것이다.
이것을 싱글톤 객체로 만들어서 접근하게 만들었다.
이것은 물론 메서드뿐만 아니라 프로퍼티도 위의 처럼 만들 수 있다.
여러 화면에서 쓰일 수 있는 공통 데이터가 필요할 수 있으니 말이다 !

  • 간단한 앱은 오히려 MVC가 더 편리하다.

  • 데이터 바인딩 같은 경우 구현하기 쉽지 않다는 점.

데이터 바인딩과 연관해서 MVVM이 함수형 프로그래밍인 RxSwift, Combine과 같은 것들과 융합해서 많이 쓰이는 것은 널리 알려진 사실이다.
이런 라이브러리들 없이 데이터 바인딩을 구현할 수 있지만, 쉽지 않다는 점?


마치며

이번 처음으로 제대로 MVVM을 리팩토링 해보고 느낀점을 적어봤다.
아직도 MVVM에 대해 배울 점이 많다고 생각한다.
앞으로 RxSwift를 배워서 MVVM과 접목시켜 리팩토링을 할 예정이다.

개인적으로 가장 중요하게 생각했던 것은
아키텍쳐 구현에 있어서 생각의 유연성 이라고 생각한다.
구현을 할 때 정답을 찾으려고 하면 오히려 더 막히는 느낌이 들었다.
그래서 많은 래퍼런스를 참고하고 자신에게 맞는 방식으로 Clean Architecture를 구현하면 되지 않을까 싶은 것이 내 생각이다.

profile
안녕하세요. iOS 개발자 eung7입니다.

0개의 댓글