Repository Pattern
Repository Pattern의 필요성
Repository Pattern을 사용하지 않은 앱은 VC or MV이 직접 데이터들을 가지게 된다.
이에 따라오는 문제가 무엇일까?
바로 유지보수이다. 앱이 커지고 화면이 많아져서 개발자가 늘어난다면??
만약 로컬DB에 저장하기 위해 CoreData를 사용하다가 realm으로 갈아타던지(NSManagedObject이 붙은 자료들),
API, JSON등 Networking의 예시로 Codable을 채택해서 만든 객체를 앱 전역에서 사용중인데 서버에서 API에 내려주는 구조가 바뀐다면..?
ViewModel의 객체들을 하나 하나 전부 수정해야한다.
만약 다음과 같은 이름과 몸무게를 포함하는 구조체를 사용하고 있다고 하자.
struct A {
let name: String
let weight: Int
}
근데 만약 A구조체가 아래처럼 바뀌면? weight가 Float로 바뀌었다.
struct A {
let name: String
let weight: Float
}
이런 상황에 ViewModel에서 몸무게에 100을 더하는 로직이 있다고 치자
원래는 Int형이기 때문에 100을 더해주면 됐지만..
// 변경 전
let a: A = datasource.fetch()
button.text = "\(a.weight + 100)"
// 변경 후
let a: A = datasource.fetch()
button.text = "\(a.weight + Float(100))"
이처럼 모든 viewModel에서 a에 자료형이 바뀌어서 생기는 문제들을 전부 하나하나 수정해야한다.
여기서 생기는 문제가 이것이다. 유지보수성이 떨어지고, 변하는 자료형에 대해 수정해야 하는 문제들이 타고 타서 많은 문제를 발생시킬 수 있다.
여기서 고안된 방법이 Repository 패턴이다!!
Repository pattern은?
Domain Object와 Repository를 분리해야한다.
여기서 말하는 Domain Object는 앱에서 보여질 때 실제로 사용할 객체이며, Repository는 Service를 활용해 remote, local에서 데이터를 가져올 때 사용될 객체이다.
Repository로 가져온 객체를 Domain에서 사용할 객체로 Mapping해주는 과정이 추가된다면 Json의 구조가 바뀌더라도 변화에 유연하게 대처할 수 있다.
이렇게 해주면 API 변경, CoreData > Realm같은 상황에서 Repository만 수정해주면 되고 Domain Object는 수정해줄 필요가 사라진다. -> 즉, 앱 내부에서 사용중인 객체를 하나 하나 수정할 필요가 사라진다.
이런 구조를 만들 수 있는 것이다 !! Codable과 NSManagedObject는 Repository에서만, 이외에는 Domain Object로 사용되는 영역에 따라 Object를 분리할 수 있다. 즉 결합도가 낮아지며, Data구조에 대한 의존성이 낮아진다.
여기서 Repository의 역활을 정리해보자면
- 데이터 소스에 접근하기 위해 캡슐화된 구성 요소
- Data에 접근하는 기능을 한군데 집중시켜 유지보수에 용이하고, Data구조에 대한 의존성을 낮춘다.
- 앱이 데이터를 다루는 곳과, 데이터를 표현하는 곳이 명확하게 나뉜다.
Repository Pattern을 그림으로 나타내보자면 아래와 같을것이다.
사실 Domain Layer는 Data Layer를 알아선 안된다. 하지만 그대로 Repository를 가져오게되면 DataLayer를 알게된다. 그래서 Domain Layer는 Repository의 프로토콜을 소유하고있고, 이 프로토콜을 통해 Data를 가져오게된다. Solid 원칙의 DIP(의존성 역전원칙)을 사용하는 부분이다.
Domain Layer -> RepositoryProtocol <- DataLayer
그래서 Repository Pattern을 사용하는 프로젝트 같은 경우에는 다음과 같은 구조를 가진다.
다음은 내가 처음으로 만들어본 Clean Archithecture의 폴더구조의 일부분이다.
DataLayer는 실제 Repository를 가지고 있다. Domain Layer의 Repository는 프로토콜만을 소유한다.
그래서 UseCase를 보면 UseCase는 Repository Protocol을 사용하여 DataLayer에 의존하는 것이 아닌 Domain Layer의 Repository의 프로토콜에 의존한다. 그래서 Domain Layer가 Data Layer를 모를 수 있게된다.
Domain Layer -> RepositoryProtocol <- DataLayer
UseCase를 보자.
이처럼 UseCase는 Repository Protocol을 사용해 Local, Remote에서 Data를 가져온다.
Repository를 보자.
잘 보면 upload에서는 CommentRequestDTO를 사용하고있으며, fetch 내부의 DataSource에선 ResponseDTO를 사용한다. 각 post와 get 상황에서 유지보수성을 높이기 위해 Domain, Data Layer만 분리한 것이 아닌 Data Layer에서도 Response냐, Request냐에 따라서도 나누어 유지보수성을 더욱 높인다.
또한 fetch에서 보면 $0.toDomain()이란 것을 실행시키고 있다. 이는 Repository(Data Layer)에서 사용하는 CommentResponseDTO를 UseCase(Domain Layer)에서 앱에서 보여질 Comment(Entity) 데이터로 변환해주는 과정이다.
다음은 RequestDTO, ResponseDTO, Entity를 보자.
- RequestDTO, ResponseDTO는 DataLayer의 DataMapping에 존재한다.
- RequestDTO(DataLayer)는 Entity(DomainLayer)를 받아 RequestDTO로 변환해 주는 과정도 필요하다.
업로드에는 Entity -> Request
이를 Init 생성자가 담당한다.
- ResponseDTO(DataLayer)가 앱에서 실제 사용될 Entity(DomainLayer)로 변환해야 함.
이를 toDomain()이 담당한다.
StringValue.. 등등은 FireBase Rest API를 사용하기 때문에 만든 구조체이며 실제론 그냥 String이라고 생각하면 좋다.
현재까지 보인 예시는 RequestDTO, ResponseDTO, Entity 모두 같은 구조이다. 왜냐하면 Repository패턴을 적용시킨 것이지 API 구조의 변경이 없고 이점을 살리지 못한것이다. 하지만 이런 방식으로 개발을 해놓는다면 추후에 발생할 어떠한 변동사항에 대해 유연하고 빠르게 대처할 수 있다.
결론적으로 Data를 가져오는곳(Data Layer)과 Data를 표현하는곳(Domain Layer)이 강하게 결합되는 문제를Data Object 와 Domain Object로 디자인하고 Repository에서 Domain Object를 반환함으로써 Decoupling을 할 수 있게 된다.