* 진행 코드는 https://github.com/jeongju9216/Jetflix에서 볼 수 있고, PR에서 에피소드 단위로 코드를 확인할 수 있습니다.
서론
원래는 UseCase와 View 개선을 함께 다루려고 했지만 UseCase만 먼저 작성하기로 했습니다.
Clean Architecture에서 UseCase를 구현할 때 가장 애매하고 고민이 많아지는 것 같습니다.
Clean Architecture 학습을 목표로 제 프로젝트에 맞게 고민하면서 코드를 작성하고 있으나 잘 구현하고 있는지...조차 잘 모르겠습니다.
혹시나 보시는 고수분이 계신다면 댓글로 가르침 주시면 정말 감사할듯 합니다 ㅎㅎ
개선 내용은 아래와 같습니다.
- UseCase를 구현하여 ViewModel과 Repository의 연결을 끊음
- UseCase는 변화가 적도록 Save, Get, Delete 등 동작을 기준으로 분리 구현함
- ContentUseCase는 ContentUseCaseProtocol을 채택하게 해서 ContentRepositoryProtocol을 필수로 가지도록 구현
세 개로 나눠 작성했지만 모두 UseCase를 구현했다는 내용입니다.
1.
기존에는 ViewModel에서 Repository를 가지고 직접 데이터를 얻어 사용했습니다.
그러다보니 Repository에 변화가 생길 때 Presentation 레이어의 ViewModel에도 영향을 주는 불편함이 있었습니다.
또한 UseCase가 없으니 ViewModel이 어떤 동작을 하는지 한 눈에 파악하기가 (상대적으로) 어려웠습니다.
왜냐하면 Repository의 모든 동작을 ViewModel이 가지고 있기 때문입니다.
Save만 하는지, Delete만 하는지 아니면 모든 동작을 처리하는지 알 수 없었습니다.
UseCase를 구현하여 ViewModel이 UseCase를 통해 Repository에게 데이터를 얻도록 수정했습니다.
HomeViewModel은 Contents를 얻고 저장하는 동작을 한다는 것을 바로 알 수 있습니다.
만약 레이어를 모듈로 만들어 import하는 형태가 된다면 꼭 필요한 부분만 접근할 수 있어 객체의 결합도가 낮아진다는 장점도 생깁니다.
2.
UseCase는 Save, Delete, Fetch 등 동작을 기준으로 분리해서 구현했습니다.
UseCase는 Entity의 바로 밖에 위치합니다. 따라서 수정이 최소화되어야 합니다.
만약 메서드를 하나로 통일한다면 동작이 추가될 때마다 수정이 발생하므로 동작마다 분리하는 선택을 했습니다.
근데 이렇게 구현하니 UseCase의 양이 많아졌습니다.
간단한 CRUD만 해도 UseCase가 4개가 생겨버립니다.
그래서 과연 이게 맞는 방향일지 더 고민해봐야겠습니다.
3.
2번 고민 과정에서 그래도 공통적인 특징은 프로토콜로 묶어보자고 생각했습니다.
Content와 관련된 UseCase는 ContentRepository를 가져야 합니다.
protocol ContentUseCaseProtocol {
var repository: ContentRepositoryProtocol { get }
}
그래서 ContentUseCaseProtocol을 만들어서 repository를 프로퍼티로 넣었습니다.
ContentUseCase가 되기 위해서는 ContentRepository 프로퍼티를 가져야 하도록 구현했습니다.
protocol GetContentUseCaseProtocol: ContentUseCaseProtocol {
associatedtype RequestType
associatedtype ResponseType
func excute(requestValue: RequestType) async throws -> ResponseType
}
protocol FetchDownloadContentUseCaseProtocol: ContentUseCaseProtocol {
associatedtype ResponseType
func excute() throws -> ResponseType
}
protocol SaveContentUseCaseProtocol: ContentUseCaseProtocol {
associatedtype RequestType
func excute(requestValue: RequestType) throws
}
protocol DeleteContentUseCaseProtocol: ContentUseCaseProtocol {
associatedtype RequestType
func excute(requestValue: RequestType) throws
}
protocol SearchContentUseCaseProtocol: ContentUseCaseProtocol {
associatedtype RequestType
associatedtype ResponseType
func excute(requestValue: RequestType) async throws -> ResponseType
}
ContentUseCase는 총 5가지가 있습니다.
(2번에서 말한 내용이 왜 고민인지 알겠죠..?
이정도로 UseCase가 많아지는게 정상은 아닌 거 같지만 어떻게 수정해야할지 아직 잘 모르겠습니다.)
아무튼, 이들은 ContentuseCaseProtocol을 채택하고 있습니다.
struct GetContentUseCase: GetContentUseCaseProtocol {
typealias RequestType = ContentType
typealias ResponseType = [Content]
var repository: ContentRepositoryProtocol
func excute(requestValue: RequestType) async throws -> ResponseType {
return try await repository.getContents(type: requestValue)
}
}
struct FetchDownloadContentUseCase: FetchDownloadContentUseCaseProtocol {
typealias ResponseType = [Content]
var repository: ContentRepositoryProtocol
func excute() throws -> ResponseType {
return try repository.fetchDownloadsContents()
}
}
struct SaveContentUseCase: SaveContentUseCaseProtocol {
typealias RequestType = Content
var repository: ContentRepositoryProtocol
func excute(requestValue: RequestType) throws {
try repository.saveWith(content: requestValue)
}
}
구현부에서 repository를 생성해서 사용하고 있습니다.
마무리
UseCase를 구현하면서 "잘 되고 있다"가 아니라 "이게 맞나..?"라는 생각이 더 많이 들었던 거 같습니다.
UseCase 도입은 잘 했지만 제대로 구현이 안 되고 있다고 느껴집니다.
그래도 이건 경험으로 해결되는 문제라고 생각하고 꾸준히 학습하고 고민하려고 합니다.
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.