Upgrade to Pro — share decks privately, control downloads, hide ads and more …

예제에서는 알려주지 않는 Model 이야기.

예제에서는 알려주지 않는 Model 이야기.

in 07/11/2019 Naver Mobile Tech Concert

omjoonkim

July 11, 2019
Tweet

More Decks by omjoonkim

Other Decks in Programming

Transcript

  1. !

  2. M

  3. MVC

  4. MVP

  5. MVI

  6. 모델(model)이란 어떠한 동작을 수행하는 코드를 말한다. 표시 형식 에 의존하지

    않는다. 다시 말해, 사용자에게 어떻게 보일지에 대해 신 경쓰지 않아도 된다. 모델은 순수하게 public 함수로만 이루어진다. 몇몇 함수들은 사용자의 질의(query)에 대해 상태 정보를 제공하고 나머지 함수들은 상태를 수정한다. Definition of Wikipedia (MVC)
  7. 모델(model)이란 어떠한 동작을 수행하는 코드를 말한다. 표시 형식 에 의존하지

    않는다. 다시 말해, 사용자에게 어떻게 보일지에 대해 신 경쓰지 않아도 된다. 모델은 순수하게 public 함수로만 이루어진다. 몇몇 함수들은 사용자의 질의(query)에 대해 상태 정보를 제공하고 나머지 함수들은 상태를 수정한다. Definition of Wikipedia (MVC)
  8. 이름 : 김신입 경력 : 신입 부연 설명 : 신입

    드디어 원하던 회사에 입사하게된 김신입. 들어온지 얼마 안되어 피쳐를 맡게 된다. 서비스의 코드 구조는 MVP이며 김신입도 이에대한 경험이 있어 자신만만이다. 즐겁게 개발을 하는 신입씨. 그렇게 리뷰의 시간이 다가오는데…. Profile
  9. #1 #1 Repository Pattern을 사용하라! - 서버통신을 위한 Retrofit object

    ApiClient { val githubBrowserService: GithubService init { githubBrowserService = makeGithubBrowserService(…) } fun makeGithubBrowserService(): GithubService {…} private fun makeGithubBrowserService(…): GithubService {…} private fun makeOkHttpClient(…) :OkHttpClient{…} private fun makeLoggingInterceptor(…): HttpLoggingInterceptor {…} }
  10. #1 #1 Repository Pattern을 사용하라! - 서버통신을 위한 Retrofit object

    ApiClient { val githubBrowserService: GithubService init { githubBrowserService = makeGithubBrowserService(…) } fun makeGithubBrowserService(): GithubService {…} private fun makeGithubBrowserService(…): GithubService {…} private fun makeOkHttpClient(…) :OkHttpClient{…} private fun makeLoggingInterceptor(…): HttpLoggingInterceptor {…} }
  11. #1 #1 Repository Pattern을 사용하라! - MODEL object RepoDetailModel {

    fun getRepo(userName: String, id: String) :Repo = ApiClient.githubBrowserService.getRepo( userName, id ) }
  12. #1 #1 Repository Pattern을 사용하라! - MODEL object RepoDetailModel {

    fun getRepo(userName: String, id: String) :Repo = ApiClient.githubBrowserService.getRepo( userName, id ) }
  13. #1 #1 Repository Pattern을 사용하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = RepoDetailModel.getRepo(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) } }
  14. #1 #1 Repository Pattern을 사용하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = RepoDetailModel.getRepo(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) } }
  15. Data #1 Repository Pattern을 사용하라! - Repository? 디자인 패턴중의 하나

    데이터 불러오는 로직을 분리시켜 관리 하는것이 목적 추상화를 통해 테스트에 용이해진다. 하나의 Repository는 하나의 Domain을 담당한다.
  16. Data #1 Repository Pattern을 사용하라! - Repository? 디자인 패턴중의 하나

    데이터 불러오는 로직을 분리시켜 관리 하는것이 목적 추상화를 통해 테스트에 용이해진다. 하나의 Repository는 하나의 Domain을 담당한다. branch : model_v1_resolve
  17. #1 #1 Repository Pattern을 사용하라! - Repository class RepoRepositoryImpl( private

    val api : GithubService ) : RepoRepository { override fun getRepo(userName: String, id: String): Repo = api.getRepo(userName, id) }
  18. #1 #1 Repository Pattern을 사용하라! - Repository class RepoRepositoryImpl( private

    val api : GithubService ) : RepoRepository { override fun getRepo(userName: String, id: String): Repo = api.getRepo(userName, id) }
  19. #1 #1 Repository Pattern을 사용하라! - Repository class RepoRepositoryImpl( private

    val api : GithubService ) : RepoRepository { override fun getRepo(userName: String, id: String): Repo = api.getRepo(userName, id) }
  20. #1 #1 Repository Pattern을 사용하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView, private val repoRepository: RepoRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.getRepo(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) } }
  21. #1 #1 Repository Pattern을 사용하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView, private val repoRepository: RepoRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.getRepo(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) } }
  22. #1 Repository Pattern을 사용하라! - Source Source 데이터를 불러올 수

    있는 모든 외부 Server(Http, socket), Local(db, cache) 현재 예제에서는 Http만 사용하기 때문에 나누지 않음.
  23. #1 Repository Pattern을 사용하라! - 정리 Model -> Repository Pattern(추상화)

    적용. 추상화를 통하여 테스트가 용이해지게 되었다.
 (GitHubApiService, RepoRepository) ( 데이터를 불러오고 처리하는 역할을 분리.
  24. #2 Business Logic을 분리하라! - Business Logic 유저의 행동에 따라

    서비스에서 보여주고자 하는 결과를 나타내기 위해 데이터를 가공하는 로직
  25. #1 #2 Business Logic을 분리하라! - ForkRepository class ForkRepositoryImpl( private

    val api: GithubService ) : ForkRepository { override fun gets(userName: String, id: String): List<Fork> = api.getForks(userName, id) }
  26. #1 #2 Business Logic을 분리하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView, private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.get(userName, repoName) val forks = forkRepository.gets(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  27. #1 #2 Business Logic을 분리하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView, private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.get(userName, repoName) val forks = forkRepository.gets(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  28. #1 #2 Business Logic을 분리하라! - PRESENTER class RepoDetailPresenter( view:

    RepoDetailView, private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.get(userName, repoName) val forks = forkRepository.gets(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  29. #1 #2 Business Logic을 분리하라! - Business Logic class RepoDetailPresenter(

    view: RepoDetailView, private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val repo = repoRepository.get(userName, repoName) val forks = forkRepository.gets(userName, repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  30. #1 #2 Business Logic을 분리하라! - Business Logic fun onCreate(userName:

    String, repoName: String) { Single.zip( repoRepository.get(userName, repoName), forkRepository.gets(userName, repoName), BiFunction { t1: RepoModel, t2: List<ForkModel> -> t1 to t2 } ).subscribe({ (repo, forks) -> view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) }, ::printStackTrace) }
  31. #2 Business Logic을 분리하라! - Business Logic Business Logic을 Presentation

    Logic에서 분리해야 하는 이유? 복잡한 화면일수록 Presentation의 코드가 커지게 된다. 그럴수록 복잡성이 증대. 유지보수성 저하 됨. Presentation의 역할을 분리해줄 필요성이 생김. Presentation에서 Business Logic을 분리. Presentation에 집중하게한다. 물론 작은 서비스, 화면, Platform에 따라 분리하지 않아도 괜찮긴 함.
  32. #2 Business Logic을 분리하라! - Service Service Layer Pattern (n-tier,

    3-layer에서 파생). MSA, 작은 서비스에서 쓰일 때 유용함. 혹은 설계가 짜임새있게 되어 있어야 한다. 완벽하게 Presentation에서 Logic을 분리하기 어렵기 때문. 또한 유지보수에 있어 어려움이 있을 수 있다. Business Logic의 집합소 개념. 재사용이 용이하다.
  33. #2 Business Logic을 분리하라! - UseCase 서로(UseCase)에게 독립적. 즉, Business

    Logic들 간의 의존성이 X Service와 마찬가지로 설계가 중요. 유지보수, 변경등의 비용이 저렴하다. 하나의 유저행동에 대한 서비스(Application)의 Business Logic이 담겨있는 객체.
  34. #2 Business Logic을 분리하라! - UseCase를 잘못 사용하는 경우. UseCase를

    Repository처럼 사용하는 경우. UseCase를 Service처럼 사용하는 경우. (강조) 보통의 경우에는 유저의 행동(Business Logic)과 1:1로 매칭된다. (강조)
  35. #1 #2 Business Logic을 분리하라! - UseCase Template abstract class

    SingleUseCase<T, in Params>( private val schedulersProvider: SchedulersProvider ) { protected abstract fun buildUseCaseSingle( params: Params ): Single<T> fun get(params: Params) = buildUseCaseSingle(params) .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.ui()) }
  36. #1 #2 Business Logic을 분리하라! - UseCase Template abstract class

    UseCase<T, in Params> { protected abstract fun buildUseCase(params: Params): T fun get(params: Params) = buildUseCase(params) }
  37. #1 #2 Business Logic을 분리하라! - Business Logic class GetRepoDetail(

    private val repoRepository: RepoRepository, private val forkRepository: ForkRepository, schedulersProvider: SchedulersProvider ) : SingleUseCase<Pair<Repo, List<Fork>>, Pair<String, String>>( schedulersProvider) { override fun buildUseCaseSingle(params: Pair<String, String>) : Single<Pair<Repo, List<Fork>>> = Single.zip( repoRepository.get(params.first, params.second), forkRepository.gets(params.first, params.second), BiFunction { t1: Repo, t2: List<Fork> -> t1 to t2 } ) }
  38. #1 #2 Business Logic을 분리하라! - Business Logic class GetRepoDetail(

    private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : UseCase<Pair<Repo, List<Fork>>, Pair<String, String>>() { override fun buildUseCase( params: Pair<String, String> ): Pair<Repo, List<Fork>> { val repo = repoRepository.get(params.first, params.second) val forks = forkRepository.gets(params.first, params.second) return repo to forks } }
  39. #1 #2 Business Logic을 분리하라! - Business Logic class GetRepoDetail(

    private val repoRepository: RepoRepository, private val forkRepository: ForkRepository ) : UseCase<Pair<Repo, List<Fork>>, Pair<String, String>>() { override fun buildUseCase( params: Pair<String, String> ): Pair<Repo, List<Fork>> { val repo = repoRepository.get(params.first, params.second) val forks = forkRepository.gets(params.first, params.second) return repo to forks } }
  40. #1 #2 Business Logic을 분리하라! - Presentation class RepoDetailPresenter( view:

    RepoDetailView, private val getRepoDetail: GetRepoDetail ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val (repo, forks) = getRepoDetail.get(userName to repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  41. #1 #2 Business Logic을 분리하라! - Presentation class RepoDetailPresenter( view:

    RepoDetailView, private val getRepoDetail: GetRepoDetail ) : BasePresenter<RepoDetailView>(view) { fun onCreate(userName: String, repoName: String) { val (repo, forks) = getRepoDetail.get(userName to repoName) view.setName(repo.name) view.setDescription(repo.description ?: "") view.setStarCount(repo.starCount) view.refreshForks(forks) } }
  42. #2 Business Logic을 분리하라! - 정리 Presentation에서 Business Logic을 분리.

    ( Domain 시스템이 복잡해져도 유연하게 대처할 수 있게 되었다.
  43. #1 #3 Exception handling - Presenter fun onCreate(userName: String, repoName:

    String) { getRepoDetail.get( userName to repoName ).subscribe({ (repo, forks) -> … }, ::handleException) } private fun handleException(throwable: Throwable) { when (throwable) { is HttpException -> { if (throwable.code() == 403) view.toastRateLimitError() else view.toastNetworkError() } else -> view.toastUnexpectedError() } } }
  44. #1 #3 Exception handling - Presenter fun onCreate(userName: String, repoName:

    String) { getRepoDetail.get( userName to repoName ).subscribe({ (repo, forks) -> … }, ::handleException) } private fun handleException(throwable: Throwable) { when (throwable) { is HttpException -> { if (throwable.code() == 403) view.toastRateLimitError() else view.toastNetworkError() } else -> view.toastUnexpectedError() } } }
  45. #1 #3 Exception handling - Business Logic fun onCreate(userName: String,

    repoName: String) { getRepoDetail.get( userName to repoName ).subscribe({ (repo, forks) -> … }, ::handleException) } private fun handleException(throwable: Throwable) { when (throwable) { is HttpException -> { if (throwable.code() == 403) view.toastRateLimitError() else view.toastNetworkError() } else -> view.toastUnexpectedError() } } }
  46. #3 Exception handling - Business Logic Presenter의 Exception Handling이 Business

    Logic인 이유. HttpException, 403 code 체킹은 Presentation로직이 아니다. Presenter에선 Http, 403등에 대한 개념을 모르기 때문. 또한 유저의 행동에 따라 발생하는 예외 상황이기 때문에 Business Logic으로 볼 수 있다. 그러나 관점에 따라서는 다르게 처리 하는 방법도 있을 수 있다.
  47. #3 Exception handling - Resolve HttpNetworkException 403 Forbidden Rate Limit

    Toast Api call Business Logic Convert to using in domain
  48. #1 #3 Exception handling - Source override fun getForks( userName:

    String, id: String ): Single<List<Fork>> = githubBrowserAppService .getForks(userName, id) .map { it.map { forkEntityMapper.mapFromRemote(it) } } .composeDomain() internal fun <T> Single<T>.composeDomain() = compose(NetworkExceptionSingleTransformer()) Single.error( if (it is HttpException) NetworkException(it.message(), it.code()) else it )
  49. #1 #3 Exception handling - UseCase override fun buildUseCaseSingle(params: Pair<String,

    String>) : Single<Pair<Repo, List<Fork>>> = Single.zip( repoRepository.getRepo(params.first, params.second), forkRepository.getForks(params.first, params.second), BiFunction { t1: Repo, t2: List<Fork> -> t1 to t2 } ).onErrorResumeNext { if (it is NetworkException && it.code == 403) Single.error(RateLimitException()) else Single.error(it) }
  50. #1 #3 Exception handling - Presenter private fun handleException(throwable: Throwable)

    { when(throwable){ is RateLimitException -> view.toastRateLimitError() is NetworkException -> view.toastNetworkError() else -> view.toastUnexpectedError() } }
  51. #3 Exception handling - 정리 Exception Handling도 Business Logic으로 볼

    수 있다. 도메인(UseCase)에서 사용할 수 있도록 관련된 곳 (Data)에서 Exception을 변환 해주어야 할 필요가 있다. (
  52. (

  53. (