๐Ÿ’ป ๊ฐœ๋ฐœ/Architecture

[Architecture] MVVM + Clean Architecture๋ฅผ ์•Œ์•„๋ณด์ž

2023. 1. 7. 12:53
๋ชฉ์ฐจ
  1. Clean Architecture
  2. ์ข…์†์„ฑ ๋ฐฉํ–ฅ
  3. ๋ฐ์ดํ„ฐ ํ๋ฆ„
  4. Domain Layer
  5. Presentation Layer
  6. 1. ViewModel
  7. 2. ViewController
  8. Data Layer

SwiftUI์—์„œ๋Š” @ObservableObject ๋•๋ถ„์— ViewModel(์—ญํ• ์„ ํ•˜๋Š”?)์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋•๋ถ„์— ์‹ค์Šต์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ์ ํŠธ์—์„œ ViewModel๋กœ ๋ถ„๋ฆฌ๋Š” ํ–ˆ์ง€๋งŒ ๋ฌธ์ œ๊ฐ€ ๋งŽ์•˜๋‹ค. ์ผ๋‹จ SwiftUI๊ฐ€ View ์ž์ฒด์ ์œผ๋กœ Data Binding์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ ViewModel์ด ๋…น์•„๋“ค์–ด๊ฐ„ ๋А๋‚Œ์ด๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฒƒ๋ณด๋‹ค๋„ ํ•˜๋‚˜์˜ ViewModel์—์„œ ์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๋‹ค๋ณด๋‹ˆ Massive ViewModel์ด๋ž„๊นŒ? ViewModel์ด ๋น„๋Œ€ํ•ด์ง„ ๋А๋‚Œ์ด ๋“ค์—ˆ๊ณ , ๊ทธ๋ž˜์„œ ํ•ด์ปคํ†ค ๋•Œ๋Š” ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ๊ฑท์–ด๋‚ด๊ณ  ์•„ํ‚คํ…์ณ์ ์œผ๋กœ ์กฐ๊ธˆ ๋” ๊ดœ์ฐฎ์€ ์•ฑ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค. ๋‹คํ–‰ํžˆ๋„ ํŒ€์› ์ค‘ ํ•œ ๋ถ„์ด ํด๋ฆฐ ์•„ํ‚คํ…์ณ์— ๋Œ€ํ•ด ์ž˜ ์•Œ๊ณ  ๊ณ„์…”์„œ ๋„์›€์„ ๋งŽ์ด ๋ฐ›์•˜๊ณ , ์ „๋ณด๋‹ค๋Š” ์–ด๋А์ •๋„ ์™„์„ฑ๋œ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ๋‹ค. 

 

์„œ๋ก ์ด ๊ธธ์—ˆ์ง€๋งŒ ์•„๋ฌดํŠผ ํ•ด์ปคํ†ค์„ ๊ณ„๊ธฐ๋กœ Clean Architecure์— ๋Œ€ํ•ด ๋‹ค์‹œ ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ์–ด์„œ ์ „์— ๋ถ๋งˆํฌํ•ด๋‘” ๊ธ€์„ ๋ณด๋ฉด์„œ ๊ฐœ๋…์„ ์ •๋ฆฌํ•ด๋ณด๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์กŒ๋‹ค.

 

 

Clean Architecture and MVVM on iOS

When we develop software it is important to not only use design patterns, but also architectural patterns. There are many differentโ€ฆ

tech.olx.com

 

 

GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor

Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...

github.com

 

Clean Architecture

์šฐ์„  ์ด๋Ÿฐ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง„๋‹ค.

์˜์กด ๊ด€๊ณ„๋Š” ์•ˆ์ชฝ์œผ๋กœ ํ–ฅํ•˜๊ฒŒ๋” ์„ค๊ณ„๋ฅผ ํ•˜๊ณ , ์•ˆ์ชฝ์— ์žˆ๋Š” ์š”์†Œ๋“ค์€ ๋ฐ”๊นฅ์ชฝ์— ์žˆ๋Š” ์š”์†Œ์˜ ์กด์žฌ๋ฅผ ๋ชจ๋ฅธ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, Presenters์— ํ•ด๋‹นํ•˜๋Š” ViewModel์€ Domain์— ํ•ด๋‹นํ•˜๋Š” Usecase์— ์˜์กด์ ์ด์ง€๋งŒ, (์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ)

Domain์€ ViewModel์— ๋Œ€ํ•ด ์•Œ ํ•„์š”๊ฐ€ ์—†๋‹ค.

 

์ข…์†์„ฑ ๋ฐฉํ–ฅ

  • Presentation Layer โ†’ Domain Layer โ† Data Layer
  • Presentation Layer๋Š” Domain Layer์— ์˜์กด
  • Domain Layer๊ฐ€ Data Layer์— ์˜์กด โ†’ ์ถ”์ƒํ™”๋œ Repository์— ์˜์กดํ•˜๋ฏ€๋กœ ์˜์กด์„ฑ ์—ญ์ „

 

 

๋ฐ์ดํ„ฐ ํ๋ฆ„

  • View๋Š” ViewModel์—์„œ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  • ViewModel์€ UseCase๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  • UseCase๋Š” User์™€ Repository์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒฐํ•ฉํ•œ๋‹ค.
  • Repository๋Š” DB, Network์™€ ์—ฐ๊ฒฐ๋˜์–ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ํ•œ ์ชฝ ๋ฐฉํ–ฅ์œผ๋กœ ํ๋ฅธ ํ”Œ๋กœ์šฐ๋Š” ๋‹ค์‹œ ๋ฐ˜๋Œ€๋กœ ํ˜๋Ÿฌ๊ฐ„๋‹ค. (๋‹จ๋ฐฉํ–ฅ, ์ฝœ๋ฐฑ)

 

Domain Layer

  • Entity, Usecase
  • Entity : ์šฐ๋ฆฌ๊ฐ€ ์ง€๊ธˆ๊นŒ์ง€ Model๋กœ ์ž‘์„ฑํ•œ ๊ฒƒ๋“ค
  • Usecase: ์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™์—์„œ ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ์„ค๋ช…ํ•˜๋Š”๋ฐ ๊ฑฐ์˜ ๋น„์Šทํ•˜๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Œ. ์œ„ ์‚ฌ์ง„์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ, ์˜ํ™” ๊ฒ€์ƒ‰(Movie Search) ์œ ์Šค์ผ€์ด์Šค, ์ตœ์‹  ์˜ํ™” ๋ชฉ๋ก?(Fetch Recent Movie Queries) ์œ ์Šค์ผ€์ด์Šค์ฒ˜๋Ÿผ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋‹จ์œ„

 

์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๋ฌธ์„œ์ด์ง€๋งŒ, ์•„ํ‚คํ…์ณ์ ์ธ ๊ด€์ ์—์„œ๋Š” iOS์™€ ๋™์ผํ•˜๋ฏ€๋กœ ์ฒจ๋ถ€ํ–ˆ๋‹ค.

Domain Layer์— ๋Œ€ํ•œ ํŠน์ง•์ด ๋‚˜์™€์žˆ๋‹ค.

 

 ๐Ÿ˜‘ ๊ทผ๋ฐ ์œ ์Šค์ผ€์ด์Šค๋ฅผ ๊ผญ ์จ์•ผํ•˜๋‚˜์š”?

โ†’ Clean Architecture๋ฅผ ์ ‘ํ•˜๊ธฐ ์ „๊นŒ์ง€๋งŒ ํ•˜๋”๋ผ๋„ ๋‚˜๋„ ๊ฐ™์€ ์ƒ๊ฐ์ด์˜€๋‹ค. ์—ฌ๋Ÿฌ ๊ธ€์„ ์ฐธ๊ณ ํ•˜๋ฉด์„œ ๋‚ด๋ฆฐ ๊ฒฐ๋ก ์€ ์ธํ„ฐํŽ˜์ด์Šค์ ์ธ ๊ฐœ๋…์ด ์ ์šฉ๋๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋ฌผ๋ก  ์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ„์ง€ ์•Š๊ณ , Presentation๊ฐ€ Repository๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•ด๋„ ์ƒ๊ด€์€ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ค ์žฅ์ ์ด ์žˆ๊ธฐ์—, ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌ๋ฅผ ํ•œ ๊ฒƒ์ผ๊นŒ?

 

์ผ๋‹จ, ์ง๊ด€์ ์ด๋‹ค. ViewModel๋งŒ ๋”ฑ ๋ดค์„ ๋•Œ, ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ๋ถ„๋ฅ˜ํ•ด๋†“์€ Usecase๋ฅผ ๋ณด๊ณ  ๋ฐ”๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๋‹ค์–‘ํ•œ Repository Layer๋ฅผ ์ฐธ์กฐํ•˜์ง€ ์•Š๊ณ , ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ตœ์†Œ ๋‹จ์œ„์ธ Usecase ์ฐธ์กฐํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ViewModel์—์„œ ์–ด๋–ค ๊ธฐ๋Šฅ(์„œ๋น„์Šค)๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

 

๋˜ํ•œ ๊ธฐ๋Šฅ์ด ํ™•์žฅ๋จ์— ๋”ฐ๋ผ ํ•˜๋‚˜์˜ Usecase์—์„œ ๋‹ค์–‘ํ•œ Repository๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค. ํ•˜์ง€๋งŒ ViewModel์€? Usecase๊ฐ€ ์–ด๋–ค Repository์™€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ์•Œ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๊ทธ๋ƒฅ ๋‚ด๊ฐ€ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ๊ธฐ๋Šฅ์— ํ•ด๋‹นํ•˜๋Š” Usecase๋ฅผ ๋ฐ”๋ผ๋ณด๋ฉด ๋œ๋‹ค.

 

๊ฐ€๋ น, ํŠน์ • ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋‚ด๋ถ€์ ์ธ ๋กœ์ง์ด ์กฐ๊ธˆ ๋ณ€๊ฒฝ๋  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. Usecase๊ฐ€ ์•„๋‹Œ, Repository ๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด Repository ์ˆ˜์ •์— ๋”ฐ๋ฅธ ViewModel์˜ ์ˆ˜์ •์ด ๋ถˆ๊ฐ€ํ”ผํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Usecase๋ฅผ ์ฐธ์กฐํ•œ๋‹ค๋ฉด? VIewModel์€ ๊ทธ๋ƒฅ ๊ฐ€๋งŒํžˆ ์žˆ์–ด๋„ ๋œ๋‹ค. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๊ณ  ์žˆ๋Š” Usecase ๋‚ด์—์„œ ์ˆ˜์ •์ด ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์„ค๋ช…์ด ๊ธธ์—ˆ์ง€๋งŒ ๊ฒฐ๋ก ์€ ๋‹ค๋ฅธ Layer๋“ค์ด ์–ด๋–ค ์ผ์„ ํ•˜๊ณ  ์žˆ๋Š”์ง€ ์•Œ ํ•„์š”๊ฐ€ ์ „ํ˜€ ์—†๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์ง์ ‘์ ์œผ๋กœ ๋งž๋‹ฟ์•„์žˆ๋Š” ์•ˆ์ชฝ Layer๋งŒ ์•Œ๊ณ  ์žˆ์œผ๋ฉด ๋œ๋‹ค.

๋‚ด ์œ—์ง‘, ์˜†์ง‘์—  ๋ˆ„๊ฐ€ ์‚ด๊ณ  ์žˆ๋Š”์ง€๋งŒ ์•Œ๋ฉด ๋œ๋‹ค๋Š” ์ 

 

๊ฒฐ๋ก ,

  • ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด
  • ์ง๊ด€์ ์ธ ์š”๊ตฌ์‚ฌํ•ญ
  • ์˜์กด์„ฑ ์ตœ์†Œํ™”

 

Presentation Layer

  • ViewModel์—๋Š” import UIkit, SwiftUI๊ฐ€ ์—†์–ด์•ผ ํ•œ๋‹ค. (๋ชจ๋“  UI ํ”„๋ ˆ์ž„์›Œํฌ์™€์˜ ๋‹จ์ ˆ)
  • ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•˜๋ฉด UIKit โ†’ SwiftUI ์ „ํ™˜์ด ๋งค์šฐ ์šฉ์ดํ•ด์ง„๋‹ค. ๋‹ค๋ฅธ ๋ถ€๋ถ„์€ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๊ณ  UI๋งŒ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

1. ViewModel

protocol MoviesListViewModelInput {
    func didSearch(query: String)
    func didSelect(at indexPath: IndexPath)
}

protocol MoviesListViewModelOutput {
    var items: Observable<[MoviesListItemViewModel]> { get }
    var error: Observable<String> { get }
}

protocol MoviesListViewModel: MoviesListViewModelInput, MoviesListViewModelOutput { }
  • ํ”„๋กœํ† ์ฝœ ๋จผ์ € ์„ ์–ธํ•ด์ฃผ๊ณ , ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜์—ฌ ์ •์˜ํ•œ๋‹ค.

 

final class DefaultMoviesListViewModel: MoviesListViewModel {
    
    private let searchMoviesUseCase: SearchMoviesUseCase
    private let actions: MoviesListViewModelActions?
    
    private var movies: [Movie] = []
    
    // MARK: - OUTPUT
    let items: Observable<[MoviesListItemViewModel]> = Observable([])
    let error: Observable<String> = Observable("")
    
    init(searchMoviesUseCase: SearchMoviesUseCase,
         actions: MoviesListViewModelActions) {
        self.searchMoviesUseCase = searchMoviesUseCase
        self.actions = actions
    }
    
    private func load(movieQuery: MovieQuery) {
        
        searchMoviesUseCase.execute(movieQuery: movieQuery) { result in
            switch result {
            case .success(let moviesPage):
                // Note: We must map here from Domain Entities into Item View Models. Separation of Domain and View
                self.items.value += moviesPage.movies.map(MoviesListItemViewModel.init)
                self.movies += moviesPage.movies
            case .failure:
                self.error.value = NSLocalizedString("Failed loading movies", comment: "")
            }
        }
    }
}
  • ์ƒ์„ฑ์ž๋กœ Domain Layer(Usecase)์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค.

 

// Note: This item view model is to display data and does not contain any domain model to prevent views accessing it
struct MoviesListItemViewModel: Equatable {
    let title: String
}

extension MoviesListItemViewModel {
    init(movie: Movie) {
        self.title = movie.title ?? ""
    }
}
  • ๋˜ํ•œ Model์„ ์ง์ ‘์ ์œผ๋กœ ์ฐธ์กฐํ•˜๋ฉด ์•ˆ๋œ๋‹ค. View๊ฐ€ ์ง์ ‘์ ์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉด ์•ˆ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. (์ฒ˜์Œ์— ๊ทธ๋ฆผ์„ ์ƒ๊ฐํ•  ๊ฒƒ)

 

2. ViewController

import UIKit

final class MoviesListViewController: UIViewController, StoryboardInstantiable, UISearchBarDelegate {
    
    private var viewModel: MoviesListViewModel!
    
    final class func create(with viewModel: MoviesListViewModel) -> MoviesListViewController {
        let vc = MoviesListViewController.instantiateViewController()
        vc.viewModel = viewModel
        return vc
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        bind(to: viewModel)
    }
    
    private func bind(to viewModel: MoviesListViewModel) {
        viewModel.items.observe(on: self) { [weak self] items in
            self?.moviesTableViewController?.items = items
        }
        viewModel.error.observe(on: self) { [weak self] error in
            self?.showError(error)
        }
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let searchText = searchBar.text, !searchText.isEmpty else { return }
        viewModel.didSearch(query: searchText)
    }
}
  • UI๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง์— ์ง์ ‘์ ์œผ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๊ณ , ํ•ญ์ƒ ViewModel์—๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์•„๊นŒ ๋งํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ ViewModel์— Entity์— ํ•ด๋‹นํ•˜๋Š” ํ”„๋กœํผํ‹ฐ(items)๋ฅผ ๋ณ„๋„๋กœ ๋‘ฌ์„œ View์—์„œ Entity์— ๋Œ€ํ•ด ์•Œ ํ•„์š”๊ฐ€ ์—†๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.
  • ๋˜ํ•œ View์—์„œ ViewModel์„ Observingํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.
  • weak self๋ฅผ ํ†ตํ•ด ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.

 

protocol MoviesSearchFlowCoordinatorDependencies  {
    func makeMoviesListViewController() -> UIViewController
    func makeMoviesDetailsViewController(movie: Movie) -> UIViewController
}

final class MoviesSearchFlowCoordinator {
    
    private weak var navigationController: UINavigationController?
    private let dependencies: MoviesSearchFlowCoordinatorDependencies

    init(navigationController: UINavigationController,
         dependencies: MoviesSearchFlowCoordinatorDependencies) {
        self.navigationController = navigationController
        self.dependencies = dependencies
    }
    
    func start() {
        // Note: here we keep strong reference with actions closures, this way this flow do not need to be strong referenced
        let actions = MoviesListViewModelActions(showMovieDetails: showMovieDetails)
        let vc = dependencies.makeMoviesListViewController(actions: actions)
        
        navigationController?.pushViewController(vc, animated: false)
    }
    
    private func showMovieDetails(movie: Movie) {
        let vc = dependencies.makeMoviesDetailsViewController(movie: movie)
        navigationController?.pushViewController(vc, animated: true)
    }
}
  • View์™€ ViewModel ์‚ฌ์ด์— Coordinator๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ViewModel์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ๋ทฐ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด ์ค€๋‹ค.
  • ๋˜ํ•œ ViewController์— ๋Œ€ํ•œ ์ฑ…์ž„์„ ์ค„์—ฌ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • Delegate๋ž‘ ๋น„์Šทํ•œ๋“ฏ? Coordinator๋Š” ์ข€ ๋” ์•Œ์•„๋ด์•ผ๊ฒ ๋‹ค.

 

Data Layer

final class DefaultMoviesRepository {
    
    private let dataTransferService: DataTransfer
    
    init(dataTransferService: DataTransfer) {
        self.dataTransferService = dataTransferService
    }
}

extension DefaultMoviesRepository: MoviesRepository {
    
    public func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
        
        let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
                                                                     page: page))
        return dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
            switch response {
            case .success(let moviesResponseDTO):
                completion(.success(moviesResponseDTO.toDomain()))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

// MARK: - Data Transfer Object (DTO)
// It is used as intermediate object to encode/decode JSON response into domain, inside DataTransferService
struct MoviesRequestDTO: Encodable {
    let query: String
    let page: Int
}

struct MoviesResponseDTO: Decodable {
    private enum CodingKeys: String, CodingKey {
        case page
        case totalPages = "total_pages"
        case movies = "results"
    }
    let page: Int
    let totalPages: Int
    let movies: [MovieDTO]
}
...
// MARK: - Mappings to Domain
extension MoviesResponseDTO {
    func toDomain() -> MoviesPage {
        return .init(page: page,
                     totalPages: totalPages,
                     movies: movies.map { $0.toDomain() })
    }
}
...
  • Usecase๊ฐ€ ์˜์กดํ•˜๊ณ  ์žˆ๋Š” Repository๊ฐ€ ์žˆ๋‹ค.
  • DTO(Data Transfer Object)๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋‹ค.
  • DB, Network๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , Domain์— ๋งคํ•‘๋œ๋‹ค. (completion์œผ๋กœ ์ „๋‹ฌ, Usecase๋ฅผ ๊ฑฐ์ณ ์ตœ์ข…์ ์œผ๋กœ ViewModel๊นŒ์ง€)

๊ฐœ๋…์ ์œผ๋กœ๋Š” ์ดํ•ด๋ฅผ ํ–ˆ์ง€๋งŒ ์‹ค์ œ๋กœ ์จ๋ณด๋ฉด์„œ ์š”๋†ˆ๋“ค์ด๋ž‘ ๋” ์นœํ•ด์ ธ์•ผ๊ฒ ๋‹ค.

์ €์ž‘์žํ‘œ์‹œ ๋น„์˜๋ฆฌ ๋ณ€๊ฒฝ๊ธˆ์ง€ (์ƒˆ์ฐฝ์—ด๋ฆผ)
  1. Clean Architecture
  2. ์ข…์†์„ฑ ๋ฐฉํ–ฅ
  3. ๋ฐ์ดํ„ฐ ํ๋ฆ„
  4. Domain Layer
  5. Presentation Layer
  6. 1. ViewModel
  7. 2. ViewController
  8. Data Layer
kodo_o
kodo_o
iOS ๊ฟ€์žผ!
kodo_o
๐ŸŽ๐Ÿ
kodo_o
์ „์ฒด
์˜ค๋Š˜
์–ด์ œ
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (149)
    • ๐Ÿ”จ ํ”„๋กœ์ ํŠธ (0)
      • TP 1 (0)
      • WhiteHCCTV (0)
      • FootPrint (0)
    • ๐Ÿ’ป ๊ฐœ๋ฐœ (63)
      • iOS (30)
      • Android (6)
      • Kotlin (4)
      • Flutter (9)
      • Node.js (5)
      • Architecture (1)
      • ์˜ค๋Š˜์˜ ์‚ฝ์งˆ (7)
      • ์—๋Ÿฌ์™€์˜ ๋™์นจ (1)
    • โœ๏ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜ (6)
      • Graph (6)
      • String (0)
      • Sort (0)
    • โœ๏ธ ์ฝ”ํ…Œ ์ค€๋น„ (44)
      • Math (1)
      • Implementation (3)
      • String (3)
      • Brute Force (5)
      • Back Tracking (7)
      • Greedy (0)
      • Dynamic Programming (13)
      • Binary Search (1)
      • DFS, BFS (5)
      • Shortest Path (2)
      • Two Pointer (4)
      • MST (0)
    • ๐Ÿ“š CS (6)
      • Operating System (6)
    • โ›น๏ธ ๋ผ์ดํ”„ (30)
      • 2020 ๊ฒจ์šธ๋ฐฉํ•™ ๋ชจ๊ฐ์ฝ”(๊ฐœ์ธ) (12)
      • 2021 ์—ฌ๋ฆ„๋ฐฉํ•™ ๋ชจ๊ฐ์ฝ”(๊ฐœ์ธ) (6)
      • ์ฝ”๋”ฉ ํ…Œ์ŠคํŠธ (1)
      • ํšŒ๊ณ  (10)

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ํ™ˆ
  • ๊นƒํ—ˆ๋ธŒ

์ธ๊ธฐ ๊ธ€

์ตœ๊ทผ ๊ธ€

์ตœ๊ทผ ๋Œ“๊ธ€

hELLO ยท Designed By ์ •์ƒ์šฐ.
kodo_o
[Architecture] MVVM + Clean Architecture๋ฅผ ์•Œ์•„๋ณด์ž
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”

๋‹จ์ถ•ํ‚ค

๋‚ด ๋ธ”๋กœ๊ทธ

๋‚ด ๋ธ”๋กœ๊ทธ - ๊ด€๋ฆฌ์ž ํ™ˆ ์ „ํ™˜
Q
Q
์ƒˆ ๊ธ€ ์“ฐ๊ธฐ
W
W

๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€

๊ธ€ ์ˆ˜์ • (๊ถŒํ•œ ์žˆ๋Š” ๊ฒฝ์šฐ)
E
E
๋Œ“๊ธ€ ์˜์—ญ์œผ๋กœ ์ด๋™
C
C

๋ชจ๋“  ์˜์—ญ

์ด ํŽ˜์ด์ง€์˜ URL ๋ณต์‚ฌ
S
S
๋งจ ์œ„๋กœ ์ด๋™
T
T
ํ‹ฐ์Šคํ† ๋ฆฌ ํ™ˆ ์ด๋™
H
H
๋‹จ์ถ•ํ‚ค ์•ˆ๋‚ด
Shift + /
โ‡ง + /

* ๋‹จ์ถ•ํ‚ค๋Š” ํ•œ๊ธ€/์˜๋ฌธ ๋Œ€์†Œ๋ฌธ์ž๋กœ ์ด์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํ‹ฐ์Šคํ† ๋ฆฌ ๊ธฐ๋ณธ ๋„๋ฉ”์ธ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.