왼쪽 사진은 아키텍처를 생각하지 않고 코드를 작성해 자주 변하는 잉크병
에 상대적으로 자주 변하지 않는 것들
이 의존하고 있는 상황입니다.
잉크병은 잉크를 다 쓰거나 다른 색이 필요할 때마다 교체해 줘야 합니다. 잉크병을 교체할 때마다 잉크병에 의존하고 있는 것들은 잉크병의 변화에 영향을 받아 묶인 끈을 풀고 다시 묶어줘야 합니다.
오른쪽 사진처럼 자주 변하지 않는 것들
을 자주 변하는 잉크 병
에 의존하지 않게 아키텍처를 개선한다면 잉크 병을 교체할 때마다 다른 것들에 영향을 미치지 않고 잉크병만 교체할 수 있습니다.
디테일은 다르지만 다음 규칙들을 지키려고 노력하고 있습니다.
- 소프트웨어의 layer를 나누어
관심사의 분리
를 하려고 노력합니다.- 나누어진 layer는
Dependency Rule(의존성 규칙)
에 따라 의존합니다.
위의 규칙들을 지켰을 때 다음과 같은 이점이 있습니다.
외부 요소들 없이 테스트
할 수 있습니다.UI는 시스템의 나머지 부분을 변경하지 않고도 쉽게 변경
할 수 있습니다.business rule에 얽메이지 않고
독립적으로 변경할 수 있습니다.source code 의존성은
안쪽으로만
향할 수 있습니다.
- source code 의존성은 low level에서 high level로만 향할 수 있습니다.
- high level은 low level에 의존할 수 없습니다.
- 외부 원에서 선언된 것은 안쪽 원에서 언급될 수 없고 영향을 줘도 안됩니다.
- 안쪽에 있는 원은 바깥쪽에 있는 원에 대해 아무것도 알 수 없습니다.
High Level | Low Level |
---|---|
자주 변하지 않음 Polycy | 자주 변함 Mechanism |
안쪽 | 바깥쪽 |
추상화된 개념 Abstract, General | 세부적인 개념 Detail |
필요로 하는 데이터 모델
을 의미합니다.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]
}
Entity를 필요로할 때 사용되는 로직
을 의미합니다.protocol SearchMoviesUseCase {
func execute(
requestValue: SearchMoviesUseCaseRequestValue,
cached: @escaping (MoviesPage) -> Void,
completion: @escaping (Result<MoviesPage, Error>) -> Void
) -> Cancellable?
}
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)
})
}
}
struct SearchMoviesUseCaseRequestValue {
let query: MovieQuery
let page: Int
}
Controller, Presenter가 High Level Layer인 Use Case와 소통하고있습니다. Flow of control은 Controller에서 시작해서 Use Case를 통해 이동한 다음 Presenter에서 실행됩니다.
use case가 presenter를 직접 호출하게 되면 상대적으로 high level layer인 Use Case가 low level layer인 Presenter를 의존해 Dependency Rule을 지키지 못해 모순인 상황입니다.
Dependency Inversion Principle(의존성 역전 원칙)
을 사용해 이런 상황을 해결합니다. Use case는 인터페이스(Use Case Output Port)를 호출하고 바깥 원의 Presenter는 인터페이스의 요구사항을 구현합니다. dynamic polymorphism(동적 다형성)을 사용해 flow of control을 원하는 방향으로 유지하고 소스코드 의존성도 Dependency Rule을 위반하지 않게
해줍니다.
경게를 넘어 데이터를 전달할 떄는 항상
안쪽 원의 다루기 쉬운 형식
으로 전달해야합니다.
안쪽 원의 다루기 쉬운 형식으로 변환하지 않으면 안쪽 원이 바깥쪽 원에대해 알아야 하기때문에 dependency rule을 지키지 못하게 됩니다.
// MARK: - Mappings to Domain
extension MoviesResponseDTO {
func toDomain() -> MoviesPage {
return .init(page: page,
totalPages: totalPages,
movies: movies.map { $0.toDomain() })
}
}
extension MoviesResponseDTO.MovieDTO {
func toDomain() -> Movie {
return .init(id: Movie.Identifier(id),
title: title,
genre: genre?.toDomain(),
posterPath: posterPath,
overview: overview,
releaseDate: dateFormatter.date(from: releaseDate ?? ""))
}
}
extension MoviesResponseDTO.MovieDTO.GenreDTO {
func toDomain() -> Movie.Genre {
switch self {
case .adventure: return .adventure
case .scienceFiction: return .scienceFiction
}
}
}
관심사의 분리를 통해 계층으로 나누고 의존성 규칙을 따름으로써 테스트하기 쉽고 Framework에 의존하지 않고 쉽게 교체할 수 있도록 설계할 수 있습니다.
클린 아키텍처의 장점도 많지만 항상 좋다고만은 할 수 없습니다.