티스토리 뷰

클린 아키텍처?


Robert C Martin이 제시한 소프트웨어 아키텍처로 계층별 역할(관심사)을 분리함으로써 유지보수, 테스트 용이성의 이점을 가져갈 수 있다.

위 이미지를 보면 의존성의 경우 바깥쪽에서 안쪽으로 향하게 되는데, 이 의미는 안쪽 계층의 경우 바깥 계층에 대해 알 수 없다는 의미(영향을 받지 않는다는 의미)이고 안쪽으로 갈 수록 의존성이 옅어짐으로 변경 가능성이 가장 적은 비즈니스 룰이 위치하게 된다.

여기서 비즈니스 룰이란 Domain Layer(Use Cases + Entities)에 적용되는 규칙으로 사업의 핵심적인 서비스와 관련된, 즉 잘 변하지 않는 비즈니스 로직을 적용하는 것을 말한다. 

예를 들어 일기 앱 서비스라고 가정한다면 일기 작성, 삭제등의 기능이 있을 수 있다. 이 로직들은 서비스의 핵심적인 로직이고 미래에도 잘 변하지 않게될 로직이다. 그렇기 때문에 개발팀이 아닌 타 부서 팀원에게 UseCase 리스트를 보여주었을 때 해당 도메인이 무슨 일들을 수행하게 되는건지 이해할 수 있어야 하는 로직들이 들어가게 된다. 

 

[Presenters & Controllers]

앱 개발(MVVM)에서 UI, ViewModels 이 포함되는 계층

ex)

final class DefaultMoviesListViewModel: MoviesListViewModel {

    private let searchMoviesUseCase: SearchMoviesUseCase
    private let actions: MoviesListViewModelActions?

    var currentPage: Int = 0
    var totalPageCount: Int = 1
    var hasMorePages: Bool { currentPage < totalPageCount }
    var nextPage: Int { hasMorePages ? currentPage + 1 : currentPage }

    private var pages: [MoviesPage] = []
    private var moviesLoadTask: Cancellable? { willSet { moviesLoadTask?.cancel() } }
    private let mainQueue: DispatchQueueType
    
    ...
    
    생략 
    
    ...
    
    private func load(movieQuery: MovieQuery, loading: MoviesListViewModelLoading) {
        self.loading.value = loading
        query.value = movieQuery.query

        moviesLoadTask = searchMoviesUseCase.execute(
            requestValue: .init(query: movieQuery, page: nextPage),
            cached: { [weak self] page in
                self?.mainQueue.async {
                    self?.appendPage(page)
                }
            },
            completion: { [weak self] result in
                self?.mainQueue.async {
                    switch result {
                    case .success(let page):
                        self?.appendPage(page)
                    case .failure(let error):
                        self?.handle(error: error)
                    }
                    self?.loading.value = .none
                }
        })
    }
}

 

[UseCases]

서비스 요구사항 (애플리케이션 고유 비즈니스 규칙)을 포함, 아래의 예시에서는 '영화 검색'에 대한 비즈니스 로직 실행

ex)

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {

    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository

    init(
        moviesRepository: MoviesRepository,
        moviesQueriesRepository: MoviesQueriesRepository
    ) {

        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }

    func execute(
        requestValue: SearchMoviesUseCaseRequestValue,
        cached: @escaping (MoviesPage) -> Void,
        completion: @escaping (Result<MoviesPage, Error>) -> Void
    ) -> Cancellable? {

        return moviesRepository.fetchMoviesList(
            query: requestValue.query,
            page: requestValue.page,
            cached: cached,
            completion: { result in

            if case .success = result {
                self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) { _ in }
            }

            completion(result)
        })
    }
}

 

[Entities]

비즈니스 모델

ex)

struct Movie: Equatable, Identifiable {
    typealias Identifier = String
    enum Genre {
        case adventure
        case scienceFiction
    }
    let id: Identifier
    let title: String?
    let genre: Genre?
    let posterPath: String?
    let overview: String?
    let releaseDate: Date?
}

struct MoviesPage: Equatable {
    let page: Int
    let totalPages: Int
    let movies: [Movie]
}

 

# 예시에 사용된 코드는 아래 프로젝트를 인용했습니다.

https://github.com/kudoleh/iOS-Clean-Architecture-MVVM

 

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

 

각각의 계층에 대해 알아봤으므로, 그 안쪽 원들에 대한 의존성을 조금 더 구체적으로 나타내면 아래와 같이 나타낼 수 있다.

의존성 방향의 경우 Presentation Layer -> Domain Layer -> Data Repositories Layer 방향이 되지만, Repositories의 경우 Interface(protocol)를 사용해 의존성을 역전시킨다. 

역전 시킨 결과: Presentation Layer -> Domain Layer <- Data Repositories Layer

결국 Domain Layer는 의존성이 없는 Layer가 되고 그에 따라 사업의 핵심적인 서비스가 변경되지 않는 이상 변경될 가능성이 거의 없는 Layer가 된다. 

 

이러한 구조를 기반으로, 특정 이벤트에 대해 다음과 같은 순서로 로직이 수행되게 된다.

  1. 특정 이벤트 발생
  2. Presenter(ViewModel)은 이벤트에 따라 UseCase를 실행
  3. UseCase는 하나 혹은 다수의 Repository를 결합, 실행
  4. Repository는 API Service(Network Request), DB로 부터 데이터 fetch & 반환
  5. UI 업데이트: Repository -> UseCase -> ViewModel -> View(UI)

 

Usecase와 Repository 네이밍 룰


네이밍 룰은 말 그대로 규칙이기 때문에 프로젝트 별로 다르게 적용될 수 있는 부분이고 정답이라고 할 것 이 없지만, 클린 아키텍처 도입 시 많은 Usecase와 Repository 네이밍을 해야 하는 만큼 참고할 만한 네이밍 규칙이 있어서 소개하고자 한다. (아래의 규칙은 구글 안드로이드 개발자 가이드를 참고 했습니다.)

[UseCase 네이밍 규칙]

현재 시제의 동사 + 명사/대상(선택사항) + UseCase.

ex) FormatDataUseCase, LogOutUserUseCase, GetLatestNewsUseCase, MakeLoginRequestUseCase 등..

 

[Repository 네이밍 규칙]

데이터 유형 + Repository

ex) NewsRepositoryMoviesRepository, PaymentsRepository 등.. 

 

클린 아키텍처 장단점


이렇게 계층을 분리함으로써 생기는 장단점들이 있는데, 장단점은 아래와 같다. 

장점

  • 패키지와 폴더 구성이 계층별로 일목요연한 트리구조를 이루기 때문에 소스코드 전반에 대한 파악이 용이
  • 특정 계층에 대한 수정이 다른 계층에 영향을 거의 주지 않음 -> 기능 추가, 수정 용이 (프로젝트 유지보수 용이)
  • UseCase 사용함으로써 ViewModel이 비대해지는 것을 일부 개선할 수 있음
  • 기능을 명확하게 나누게 됨, 각 기능에 대한 테스트 용이

단점

  • 아키텍처에 대한 이해 필요
  • 많은 부가적인 class가 추가될 수 있음

 

이러한 장, 단점 때문에 거의 모든 도메인 로직이 백엔드 서버에 있어서 딱히 비즈니스 로직이랄 것이 없는 경우 다른 아키텍처가 더 나을 수 있다. 

 

실제 프로젝트에 적용하면서 생겼던 의문점과 느낀 점들


의문 1. Usecase의 역할

클린 아키텍처를 학습하고, 실제로 프로젝트에 적용해 보면서 들었던 의문 중 하나는 Domain Layer의 Usecase의 역할이었다. 사용하면서 느낀 건, Usecase의 역할이 단순히 Repository를 실행(래핑) 하는 역할 말고는 하는 역할이 없는 것처럼 보였다는 것이다. 그러다 보니 "크게 필요 없는 Layer 아닌가?", "뷰모델에서 직접 Repository를 사용해도 될 것 같은데?"라는 생각이 들기도 했는데. 이와 관련해 아래의 글들이 큰 도움이 되어서 소개하고자 한다!

https://medium.com/@justfaceit/clean-architecture는-모바일-개발을-어떻게-도와주는가-1-경계선-계층을-정의해준다-b77496744616

 

Clean Architecture는 모바일 개발을 어떻게 도와주는가? - (1) 경계선: 계층 나누기

How Clean Architecture Assists Mobile Development - Part 1. Boudaries: Defining Layers

medium.com

https://heegs.tistory.com/58

 

[Android] Clean Architecture - UseCase 란 ?

처음 학습하면서 작성한 글입니다. 필요시 추후 내용을 수정할 예정입니다. 틀린 부분이 있으면 언제든 지적해주면 감사하겠습니다 :) Clean Architecture 를 공부하는 도중에, UseCase 라는 것을 domain l

heegs.tistory.com

위 글들을 종합해서 정리해 보자면, 정답은 없지만  Usecase의 역할과 장점들을 생각해 보고 자신의 프로젝트 condition과 어디에 더 가치를 두는지에 맞춰서 Usecase 생략 유무를 결정하면 된다! 이다.

Usecase의 장점들은 아래와 같은데, 만약 Usecase가 비즈니스 로직이 거의 없어 presentation Layer와 Data Layer 간의 중계 역할밖에 하지 못하고 Usecase가 주는 장점을 크게 체감하지 못하겠다는 생각이 되면 UseCase를 생략하고 ViewModel에 통합시켜도 괜찮다고 생각한다. 

[Usecase의 장점]

1. Usecase를 사용하는 ViewModel이 어떤 것을 하고자 하는지 직관적으로 파악이 가능해진다. (해당 도메인이 무슨 일을 하는지 쉽게 파악 가능하기 때문)

2. 코드 중복을 방지하고, 책임을 분할하여 뷰모델이 비대해지는 것을 방지할 수 있다.

3. 재사용이 용이하고, 변경에 쉽게 대처 가능하다. 

 

의문 2. 다수의 Presenter(ViewModel)에서 사용되는 공통 로직이 있을 때 이 로직을 Usecase로 빼도 괜찮을까?

이 부분 역시, 정답이 있는 것은 아니지만 구글 안드로이드 개발@saryongkang 님의 생각을 빌리자면, 다음과 같이 말하고 있다.

앱의 특성상 각 프리젠터에서 자주 사용되는 공통의 로직이 꽤 발생하는 경우가 있습니다. 이 경우, 엄밀히 얘기하면 도메인 로직이라고 할 수는 없지만, ViewUseCase 형태의 클래스로 분리해서 프레젠테이션 계층에 추가하는 것은 좋은 방법이라고 생각합니다.

 

의문 3. Usecase가 다른 Usecase를 포함해도 될까?

전혀 문제 될 게 없다. 왜 이런 의문을 가졌었는지는 정확히 기억나지 않지만(아마 과녁 이미지에서 화살표가 안쪽으로 향하는 dependency 때문에 그런 것 같다.) Usecase가 다른 Usecase를 의존하는 데 있어 문제 될 만한 것은 없어 보인다.

실제로 구글 안드로이드 개발자 가이드 문서를 보면 아래와 같은 사용 사례를 소개하고 있다.

Usecase는 재사용 가능한 로직을 포함하기 때문에 다른 Usecase에 의해 사용될 수도 있습니다. Domain Layer에 여러 Usecase가 있는 것은 정상입니다. 예를 들어 아래 예에 정의된 Usecase는 UI 레이어의 여러 Class가 시간대를 사용하여 화면에 적절한 메시지를 표시하는 경우 FormatDateUseCase를 사용할 수 있습니다.

 

 

댓글
링크
최근에 올라온 글
최근에 달린 댓글