일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 다익스트라 이해
- 2022 부스트캠프
- Clean swift
- Java
- iOS Static Library 사용하는방법
- NSSortDescriptor
- persistentStoreCoordinator
- NSManagedObject SubClass
- Associated Value
- Persistent store Coordinator
- iOS Static Library
- 스위프트 클로저
- 일급 객체
- NSPredicates
- 1009번
- Swift
- Swift closure
- expensive operation
- leetcode #01
- LightWeight Migration
- Swift LinkedList
- Raw value and Associated value
- CoreData Stack
- codability
- 트레일링 클로저
- CoreData
- CoreData Filter
- Swift 고차함수
- CoreData Concurrency
- dateFormatter
- Today
- Total
하루를살자
[iOS] MVVM + Clean Architecture [2] - Domain Layer 본문
Domain Layer
예제 프로젝트에서 Domain Layer 에 속하는 Domain 폴더를 살펴보자.
크게 3가지 분류: Entities, UseCase, Repository 의 Interface 로 나눠서 볼수 있다.
Entity : 프로젝트에 사용되는 도메인 정보.
Usecase :
- 어플리케이션 안에서 사용자가 사용할수 있는 메인 기능 의 인터페이스와 구현체.
- UseCase 는 또 다른 Usecase 에 의존할수 있다
- UseCase 는 interactor 라고도 부른다
Repository Interface : Dependency Inversion 을 위한 인터페이스.
(Dependency Inversion 은 이 아티클에서 잘 설명해줌)
protocol SearchMoviesUseCase {
func execute(requestValue: SearchMoviesUseCaseRequestValue,
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,
completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
return moviesRepository.fetchMoviesList(query: requestValue.query, page: requestValue.page) { result in
if case .success = result {
self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) { _ in }
}
completion(result)
}
}
}
// Repository Interfaces
protocol MoviesRepository {
func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable?
}
protocol MoviesQueriesRepository {
func fetchRecentsQueries(maxCount: Int, completion: @escaping (Result<[MovieQuery], Error>) -> Void)
func saveRecentQuery(query: MovieQuery, completion: @escaping (Result<MovieQuery, Error>) -> Void)
}
SeachMoviesUsecase,DefaultSearchMoviesUseCase :
SeachMoviewsUsecase 로 어떤 Usecase 의 인터페이스를 정의하고 DefaultSeachMoviesUsecase 에 구현한다.
이 구현체에는 Repository 의 인터페이스 타입을 가지는 프로퍼티를 할당하여 Dependency inversion 에 사용하는것 을 볼수있다.
또한 Usecase 를 두가지 방법으로 설계를 할수 있다고 설명한다.
1.0 각각의 Usecase 를 interface 로 만들고 Conform 해주는 구현체를 만드는 방법
2.0 Start() 라는 메소드를 가진 Usecase Interface 를 Conform 하는 구현체를 만드는 방법.
진행중인 Picteresting 프로젝트 에서 리팩토링 가능한 요소들 @Domain Layer
분석
View 에 필요한 데이터 요청을 처리하고 동시에 ImageConfigurable 이라는 Usecase 를 채택하고 Repository 또한 프로퍼티로 가지고 있다. 즉, ViewModel 이 Usecase 와 Viewmodel 역할을 동시에 하고 있다.
final class SaveViewModel: ImageConfigurable {
var didUpdateLikeStatusAt: ((Int) -> Void)?
var imageList: Observable<[ImageEntity]> = Observable([])
let repository = HomeRepository()
subscript(index: IndexPath) -> ImageEntity? {
return imageList.value[index.row]
}
func resetList() {
//...
}
func updateLikeStatus() {
//...
}
func resetLikeStatus() {
//...
}
func fetchImages() {
//...
}
func toogleLikeState(item entity: ImageEntity, completion: @escaping ((Error?) -> Void)) {
//...
}
}
protocol ImageConfigurable {
var imageList: Observable<[ImageEntity]> {get set}
func fetchImages()
func resetList()
func updateLikeStatus()
func resetLikeStatus()
func toogleLikeState(item entity: ImageEntity, completion: @escaping ((Error?) -> Void))
subscript(index: IndexPath) -> ImageEntity? { get }
}
문제점
Usecase
1.0 ImageConfigurable 이라는 Usecase 를 채택하고 있던 ViewModel 들은 해당 화면에 필요없는 기능까지 구현해야했어야 했다.
2.0 두 Viewmodel 에 합당한 ImageConfigurable 이라는 모호한 Usecase 네이밍을 사용해야했기 때문에 프로젝트의 정확한 기능을 이해하기가 어렵다.
3.0 Usecase 를 테스트 하려면 ViewModel 까지 만들어야 한다. (유닛 테스트가 길고 복잡해진다)
Repository
HomeRepository 의 경우 인터페이스 사용을 하지 않고 구현체 하나에서 메소드를 여러개 사용하고 있다. 각 메소드 하나하나가 Repository 의 역할을 하게 되는데 ViewModel (Presenter) 에 Usecase 와 같은 비즈니스로직 이 embed 되어 있으면 어떤 Repository 가 있는지 다른개발자가 봤을때 직관적으로 아키텍처의 흐름과 기능을 이해하기 힘들다. 반면에 Repository 를 따로 인터페이스로 만들어두면 똑같은 인터페이스의 사용이 많이질시 재사용성이 높아진다. 또한 Unit Testing 을 진행시 짧고 간결한 테스트를 진행할수 있는 장점이 생긴다.
개선 사항
1.0 ImageConfigurable 의 프로토콜 분리작업
- Presentation Layer 를 공부한이후에 살펴볼 내용이지만, ImageConfigurable 은 너무 많은 책임을 가지고 있다. 사용자의 Input 에 따른 Output 작업 + Usecase 의 책임 등 을 가지고 있다. 지금은 어떤 인터페이스를 Usecase 로 뺄수 있을지에 대한 집중을 먼저 해봤다.
현재 ImageConfigurable 사용하고 있는 인터페이스
- fetchImages(): 사용자는 어플리케이션을 시작하면 Home 화면에 이미지를 볼수있다. 첫 Home 화면이 메모리에 할당되는 순간, 어플리케이션은 "이미지를 가지고와" 라는 사용자의 행위를 대신 처리하게 된다. 따라서 "FetchImageUsecase" 라는 Usecase 를 만들것이다.
- resetList(): Repository 를 사용해서 처리하는 메소드가 아니고 Viewmodel 의 "imageList" 프로퍼티를 사용하기 때문에 Usecase 로 따로 빼진 않을것 같다.
- resetLikeStatus(): ViewController 의 willDisappear 메소드에서 호출되고, ImageEntity 의 isLiked 의 상태가 true 인 경우 false 로 만들어준다. 이 인터페이스 또한 Repository 를 사용하지 않고 ViewModel 의 프로퍼티를 사용하기 때문에 Usecase 로 사용하지 않을것 같다.
- updateLikeStatus(): ViewController 의 willAppear 메소드에서 호출되고 Core data 에 저장된 이미지 정보들을 가져와서 ImageList 에 있는 imageEntity 의 id 와 비교하여 매칭되는 entity 의 isliked 상태를 반전시킨다 (false -> true). 사용자의 Input 에 의해 직접적인 영향을 미치는 기능이 아니기 때문에 보류 해둔다. (Presentation Layer 에서 Output 분류로 빠질수 있을것같다)
- toogleLikeState(): 사용자가 별모양 버튼을 눌렀을때 해당 entity 의 isLiked 의 상태를 바꾸고 Core data 와 disk cache 된 이미지 데이터를 추가/삭제한다. 사용자가 이미지를 선택하면 저장/삭제 한다는 행위 가 분명히 들어났기 때문에 "ChangeImageLikeState" 라는 인터페이스를 하나만들고, 이것의 구현체인 "SaveImageUsecase" , "UndoSaveImageUsecase" 라는 Usecase 를 만들것이다.
2.0 Repository 분리작업
- 문제점에서 발견했듯이 Repository 는 프로토콜 을 사용하지 않고 하나의 Repository 클래스에 여러개의 책임을 부여해서 singleton 처럼 사용하고 있었다. 이때 문제점은 다른클래스들 간의 결합도가 높아지는데, 이는 개방 폐쇄 원칙 을 위반하여 유닛 테스트와 수정이 힘들어진다.
- 따라서 Repository 현재 메소드 를 분석하고 어떤 interface 로 나눌수 있을지에 대해서 중심적으로 고민 해볼것이다.
현재 Repository 클래스에서 사용하고 있는 메소드
fetchImages(endPoint: EndPoint, completion: @escaping (Result<[ImageEntity], NetworkError>) -> Void)
NetworkService 를 이용하여 주어진 이미지 정보 API endPoint 로 request 를 보내고 response 를 받아 decoding 및 EntityMapping 까지 진행한다. 이 메소드 는 이미지 데이터를 받아오는 핵심 기능을 cache 되어 있는 이미지 데이터를 찾거나 네트워크 요청을 보내기 때문에 "ImageRepository" 라는 Repository Protocol 을 만들고 해당 메소드를 인터페이스로 뺄 예정이다.
fetchSavedImageData() -> [ImageData]
ImageManager 를 사용하여 CoreData 에 저장되어있는 ImageData 를 fetch 해온다. 이 함수는 SaveImageUsecase 에 사용될 것이다. "savedImageRepository" 라고 새로 인터페이스를 만들어 줄지 아니면 "imageRepository" 에 같이 사용할지 고민을 했다. Usecase 가 다르다고 repository 를 새로 만들면 "savedImageRepository","undoSaveImageRepository" 이런식으로 리포지토리의 개수가 많이질 것이다. 하지만 이 Repository 들 안에서 사용되는 모델은 imageEntity 하나뿐이고, 같은 모델을 사용하는 Repository 가 많아진다면 더 헷갈릴것 같다는 생각이들었다. 따라서 "ImageEntity" 라는 모델 을 사용하는 Repository 는 "ImageRepository" 하나만 사용하는게 더 직관적이라는 생각이 들었고 아래 메소드 들도 같은 리포지토리의 인터페이스로 구현 할것이다.
saveImage(imageEntity: ImageEntity, completion: @escaping ((Error?) -> Void))
1.0 ImageManager 를 사용하여 매개변수로 들어온 ImageEntity 의 이미지를 특정 디렉토리에 저장시킨다.
2.0 ImageData 로 변환뒤 1.0 처리가 완료되면 CoreDataManager 로 imageData 를 저장한다.
deleteImage(imageEntity: ImageEntity, completion: @escaping ((Error?) -> Void))
1.0 fileManager 를 사용해 ImageEntity 가 저장된 디렉토리에서 이미지 데이터를 삭제 시킨다.
2.0 CoreDataManager 로 imageData 를 삭제 시킨다.
resetRepository(completion: @escaping ((Error?) -> Void)
1.0 CoreDataManager 로 CoreData 에 저장되어있던 ImageData 들을 가져온다.
2.0 모든 ImageData 의 이미지를 fileManager 를 통해서 저장된 디렉토리를 구한다음 삭제 한다.
3.0 CoreDataManager 로 CoreData 에 저장되어있던 모든 ImageData 를 삭제 한다.
'Architecture' 카테고리의 다른 글
[iOS] MVVM + Clean Architecture [3-1] - Presentation Layer 적용 (2) | 2022.09.12 |
---|---|
[iOS] MVVM + Clean Architecture [3] - Presentation Layer (0) | 2022.08.22 |
[iOS] MVVM + Clean Architecture [1] (0) | 2022.08.15 |
Clean code (0) | 2022.06.10 |