💡 Intro
장바구니 미션 Step 1&2 단계 피드백 수업과 함께 Repository 패턴에 대해 배웠다. 하지만 이게 무엇인지 이해하지 못한 상태였고, 이대로는 미션에 어떻게 적용하지 못하겠다는 생각이 들었다. 안드로이드 장인 멘데루에게 도움을 요청해 1:1 명강의를 들었고, 내가 이해하고 적용한 Repository 패턴에 대해 정리하려고 한다.
❓ 안드로이드 아키텍쳐 계층
Repository 패턴에 대해 알아보기 전에 안드로이드 아키텍처 계층에 대해 간단하게 알아보자.
안드로이드 아키텍쳐는 Ui Layer(Presentation Layer), Domain Layer, Data Layer 총 3가지 계층으로 나눠진다. 그리고 각 계층이 하는 일은 다음과 같다.
Ui Layer(Presentation Layer) :
- UI 컴포넌트 (액티비티, 프래그먼트, 뷰) 를 포함한다.
- 사용자와 상호작용하고 UI를 표시하는 역할을 한다.
- 사용자의 입력을 처리하고, 데이터를 화면에 표시하며, UI의 갱신을 관리한다.
- 일반적으로 MVP, MVVM 패턴을 사용하여 구현한다.
Domain Layer :
- 비즈니스 로직을 포함한다.
- 애플리케이션의 핵심 로직을 처리하고, 데이터 처리 규칙을 정의한다.
- UI Layer(Presentation Layer)와 Data Layer 사이에서 중재자 역할을 한다.
- 도메인 모델, 유효성 검사, 비즈니스 규칙 등을 포함한다.
Data Layer :
- 데이터 소스와의 상호작용을 담당한다.
- 로컬 데이터베이스, 네트워크, 캐시, 외부 API 등과의 데이터 액세스를 처리한다.
- 데이터베이스, 네트워크 통신, 데이터 변환 등의 로직을 포함한다.
- 데이터의 로컬 캐싱, 동기화, 캐시 관리 등의 기능을 제공한다.
그리고 그림에서도 알 수 있듯이 Data Layer에서 Repository 패턴이 사용된다.
❓ Repository 패턴
그럼 이제 진짜 Repository 패턴이 무엇인가에 대해 알아보자.
Repository 패턴은 데이터 액세스를 추상화하는 디자인 패턴이다. 이는 데이터베이스, 외부 API와 같은 다양한 데이터 소스에 대한 액세스를 캡슐화하고, 비즈니스 로직 계층과 데이터 액세스 계층을 분리하는데 목적이 있다.
Repository는 애플리케이션의 데이터 액세스 로직에 대한 인터페이스를 제공하며, 이를 구현하는 클래스는 실제 데이터 소스와 상호작용한다. 이렇게 함으로써 비즈니스 로직은 데이터 액세스에 종속되지 않고, 단순히 Repository 인터페이스를 통해 데이터에 액세스 할 수 있다.
앞에서 Repository 패턴은 데이터 액세스를 추상화하는 디자인 패턴이라고 했다. 즉, Repository는 Local에 있는 데이터에 접근하는 것인지, Remote에 있는 데이터에 접근하는 것인지 알지 못한채 단지 데이터에 어떻게 접근하고 처리하는지에 대해서만 명세하는 것이다.
이를 통해 데이터를 가져오는 방법과 저장하는 방법에 대한 구체적인 구현을 추상화할 수 있다.
// Product Repository 에서는 상품의 데이터를 요청하는 작업을 한다.
interface ProductRepository {
fun requestAll(): List<Product>
}
그리고 여기서 DataSource 라는 개념이 등장한다.
❓ DataSource
DataSource는 실제로 데이터에 액세스 하여 가져오거나 저장하는 역할을 한다. 이 때 말하는 데이터란 로컬 데이터베이스, 원격 서버 등 다양한 DataSource가 될 수 있다. 그리고 이는 데이터 작업을 위해 애플리케이션과 시스템을 연결해주는 다리 역할을 한다.
다른 레이어에서 DataSource에 직접 액세스해서는 안된다. 반드시 DataSource의 진입점은 Repository여야 하며, 이러한 방법은 아키텍처의 다양한 레이어를 독립적으로 확장할 수 있도록 도와준다.
때문에 Repository는 이러한 DataSource들과 함께 상호작용한다. DataSource에서는 Entity 또는 DTO(공식문서에서는 *ApiModel 이라고 한다.)와 같은 객체들을 활용하여 데이터를 가져오는 작업을 하고, Repository에서 적절히 가공하여 원하는 VO의 형태로 반환한다. 이로써 Repository에서는 로컬 데이터베이스에서 데이터를 가져오는지, 원격 서버에서 데이터를 가져오는지 모르도록 추상화할 수 있는 것이다.
Repository가 어떤 DataSource에서 데이터를 가져오는지 모르게 추상화한다는 말은 DataSource 역시 인터페이스라는 말이다. DataSource는 단지 데이터의 조회, 삽입, 삭제와 같은 작업을 명세할 뿐이다. 그리고 실제로 LocalDataSource 혹은 RemoteDataSource에서 구체적인 구현을 하게 된다.
// Product DataSource 에서는 상품의 데이터를 요청하는 작업을 한다.
interface ProductDataSource {
fun requestAllData(): List<Product>
}
// ProductLocalDataSourceImpl 에서는 로컬 데이터베이스로부터 상품의 데이터를 요청하는 작업을 한다.
class ProductLocalDataSourceImpl(
dbHelper: ProductDbHelper,
) : ProductDataSource {
private val db = dbHelper.writableDatabase
override fun requestAllData(): List<Product> {
...
}
private fun getCursor(): Cursor {
...
}
}
// ProductRemoteDataSourceImpl 에서는 원격 서버에서 상품의 데이터를 요청하는 작업을 한다.
class ProductRemoteDataSourceImpl : ProductDataSource {
init {
// Mock Server
ProductMockWebServer.startMockWebServer()
}
override fun requestAllData(): List<Product> {
...
}
private fun parseProductsFrom(jsonArray: JSONArray): List<Product> {
...
}
private fun parseProductFrom(jsonObject: JSONObject): Product {
...
}
companion object {
private const val URL = "http://localhost:"
private const val PATH_GET = "/products"
}
}
/* ProductRepositoryImpl 에서는 DataSource가 로컬 데이터베이스에서 가져오는 것인지
* 원격 서버에서 가져오는 것인지 관심이 없고, 원하는 VO의 형태로 데이터를 반환하기만 한다.
*/
class ProductRepositoryImpl(
private val productDataSource: ProductDataSource,
) : ProductRepository {
override fun requestAll(): List<Product> {
return productDataSource.requestAllData()
}
}
구현을 하다 보니 이 부분에서 한 가지 의문점이 들었다.
Repository와 DataSource 모두 인터페이스이고, 데이터 액세스에 대한 명세이기 때문에 둘의 메서드의 이름과 기능이 완전히 같다. 메서드명을 다르게 지어야 할까? 아니면 똑같이 지어도 상관없을까?
아직 이 의문에 대한 답은 찾지 못했다.
❓ Repository 패턴의 장점
Repository 패턴의 장점은 다음과 같다.
1. 데이터 액세스 로직과 비즈니스 로직의 분리 - Repository는 데이터 액세스에 대한 인터페이스를 제공하여 비즈니스 로직 계층과 분리된다. 이를 통해 비즈니스 로직은 데이터 액세스의 세부 사항을 알 필요 없이 데이터를 요청하고 저장할 수 있다.
2. 테스트 용이성 - Repository는 인터페이스로 추상화되어 있으므로 테스트하기 쉽다. 테스트 시 가짜 또는 메모리 기반의 DataSource를 사용하여 Repository를 구현할 수 있다.
3. 확장성 - Repository는 데이터 액세스 계층의 구체적인 구현을 캡슐화하므로, 필요에 따라 다른 데이터 소스로의 전환 또는 새로운 데이터 소스 추가가 상대적으로 간편한다.
❗️ 정리
아래 그림은 멘데루의 명강의를 듣고 정리한 그림이다. 잘못된 내용이 있을 수 있지만 이해에 도움이 될지도 몰라 첨부한다.
- DataSource는 인터페이스이며 Repository에서 데이터를 Local에서 가져올지, Remote에서 가져올지, 아니면 다른 곳에서 가져올지 모르도록 추상화할 수 있다.
- DataSource에서는 Entity 혹인 DTO(공식문서에서는 *ApiModel 이라고 한다.)를 사용하여 데이터를 가져오고, Repository에서 원하는 VO의 형태로 가공하여 반환한다.
- Data Layer에서 VO의 형태로 Domain Layer로 전달되면 적절한 비즈니스 로직을 거쳐 Ui Layer(Presentation Layer)로 전달해 준다.
- Repository 패턴은 데이터 액세스와 비즈니스 로직을 분리할 수 있고, 유지보수성과 테스트의 용이성을 향상할 수 있는 디자인 패턴이다.
- Repository 패턴은 애플리케이션의 데이터 액세스를 관리하기 위한 일관된 방법을 제공한다.
더 나아가서..
우테코 레벨2 마지막 미션인 주문 미션에서 추가적으로 OrderRepository, PointRepository를 만들었다. 이들은 각각 사용자의 전체 주문에 대한 조회와 단일 주문에 대한 조회, 사용자의 가용 포인트 조회에 대해 명세가 되어있다.
이전에 Repository 패턴을 적용한다고 패키지 구조를 바꾸고, 레이어를 나눴을 때 100% 서버로부터 받아오는 데이터일텐데 굳이 DataSource 레이어가 필요할까? 에 대한 의문이 들었지만 Repository 패턴에 익숙해지고 경험해보자고 생각해서 레이어를 모두 나눴다.
이렇게 한 번 구현하고나니 이번 미션에서는 DataSource가 필요하지 않을 것 같다는 판단이 가능해졌고, 그렇게 구현했다.
하지만 확신이 들지 않아 PR을 요청하며 리뷰어님께 다음과 같은 질문아닌 질문을 남겼다.
이번 미션에서 추가한 Repository들은 DataSource를 가지고 있지 않도록 구현했어요. DataSource가 필요한 이유는 로컬에서 온 데이터인지, 리모트에서 온 데이터인지 알 필요가 없도록 하기 위함이라고 생각해요. 그런데, 장바구니에 대한 정보를 받아오는 방법에 대해서는 추후에도 변경될 것 같지 않기 때문에 DataSource를 갖지 않도록 하고, 서비스를 갖고 동작하도록 구현했습니다!
라고 남겼고 이에 대해 리뷰어님은 이렇게 말해주셨다.
좋습니다. DataSource가 필요성을 갖는 순간이 언젠가는 오겠지만, 저도 지금 구조에서는 Repository만으로 충분하다고 생각해요. 소신을 갖고 구현해주셨네요 ㅋㅋㅋ 👍
(뿌듯)
그리고 두 번째 리뷰 요청에서는 이런 코멘트가 달렸다. 이 방법도 좋은 방법인 것 같다.
https://developer.android.com/topic/architecture/data-layer
위 문서를 읽어보시길 바랍니다!
예를 들어, 다음과 같은 코드가 있다면 개발자는 ExampleRepository에 요청을 보낼 때 해당 데이터가 DB에서 오는지, 서버에서 오는지 알 필요가 없습니다.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
그리고 추천해주신 공식문서를 보면
종종 저장소가 단일 데이터 소스만 포함하고 다른 저장소에 종속되지 않는 경우가 있는데 이때는 개발자가 저장소와 데이터 소소의 책임을 Repository 클래스에 병합합니다. 이 경우 저장소가 최신 버전의 앱에서 다른 소스의 데이터를 처리해야 할 때는 기능을 분할해야 합니다.
라고 되어있다. 즉 저장소 클래스는 여러 데이터 소스를(Local or Remote)를 갖도록 구현할 수 있으며, 만약 단 하나의 데이터 소스를 가진다면 데이터 소스 레이어를 나눌 필요가 없다는 말과 같다. 그러니 필요에 따라 잘 구분해서 사용하는 편이 좋겠다.
참고