선생님, 개발을 잘하고 싶어요.

[프로젝트 구조] Repository 를 활용한 데이터 코드와 UI 코드의 분리 본문

개발/android 개발

[프로젝트 구조] Repository 를 활용한 데이터 코드와 UI 코드의 분리

알고싶은 승민 2019. 6. 27. 16:32

 

  필자가 최근에 artic이라는 앱을 개발하기 위해 프로젝트 구조를 설계한 내용을 공유해보고자 한다. 

어떤 프로젝트 구조이고 이 프로젝트 구조를 생각했으며 어떻게 목적을 달성하는지 보여주고자 한다.

목적

  1. 데이터 코드와 UI 코드의 분리
  2. 네트워크 연결성과 UI 코드의 분리

artic이라는 앱은 서버에서 데이터를 받아와서 화면에 뿌려주는 간단하다면 간단한 앱이다. 그런데 통상적으로 네트워크 통신이 들어가는 화면을 검증하는 것은 위험한 일이다. 과연 프론트와 서버 둘 중 무엇이 잘못된 건지 어떻게 장담하는가? 그리고 서버 코드가 나오기 전 까지는 프론트 개발에 손을 때 놓고 있을 수 도 없는 노릇 아닌가?

 

추가적으로 이번 프로젝트에서는 네트워크 연결이 끊겼을 때 에러를 내보네는 앱이 아니라 로컬 데이터베이스나 메모리에 있는 데이터를 유동적으로 보여주어 좋은 사용자 경험을 유지하도록 만들어 보고 싶었다.

 

샘플 프로젝트

서버 혹은 로컬로부터 Github Repository 정보를 받아와 사용자에게 리스트로 보여주는 어플리케이션

Github 주소 : https://github.com/greedy0110/SampleGithubRepo

코드가 이상하거나 더 괜찮은 구조가 생각나면 마음껏 풀리퀘를 날려주기를

 

greedy0110/SampleGithubRepo

koin + rxjava + retrofit 을 활용한 간단한 서버연동, 에러처리 repository 패턴 보여주는 앱 - greedy0110/SampleGithubRepo

github.com


패키지 구조

패키지 이름 패키지 목적
data ui 에 그려지는 data 원형을 정의할 목적
ui activity / fragment / adapter 와 같이 화면을 그려줄 목적
repository 서버에서 혹은 로컬에서 비동기적으로 데이터를 읽고, 쓰고, 변경하는 담당

 

사용 라이브러리

  • rxjava : 비동기 작업을 처리하기 위해서 사용하는 라이브러리
 

ReactiveX/RxJava

RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. - ReactiveX/RxJava

github.com

  • retrofit + gson : 네트워크 restful api 통신을 위해 사용하는 라이브러리 + json 형태의 데이터를 파싱 하기 위해 사용하는 라이브러리
 

Retrofit

A type-safe HTTP client for Android and Java

square.github.io

 

google/gson

A Java serialization/deserialization library to convert Java Objects into JSON and back - google/gson

github.com

 

  • koin : 복잡한 생성자를 가진 repository의 생성 코드를 캡슐화하기 위한 라이브러리
    • 생성 코드 캡슐화를 통해 repository의 생성 변경이 사용하는 ui 코드를 변경하지 않는다.

 

 

InsertKoinIO/koin

KOIN - a pragmatic lightweight dependency injection framework for Kotlin - InsertKoinIO/koin

github.com

 

프로그램 진행 흐름

  안다. github 링크만 꼴랑 있으면 어쩌라는 건지 싶다. 그래서 프로그램 진행 흐름을 따로 설명하고자 한다. 필자가 설명에 필요하다고 생각되는 코드만 따로 정리해서 보여줄 것이다. 코드가 길진 않을 거다. 가보자.

 

UI 코드

 

ui.MainActivity.kt

class MainActivity : BaseActivity() {
    private val repository: GithubRepository by inject() // application 에서 startKoin 으로 등록한 repository 를 받아!

    private val adapter: RepoAdapter by lazy { RepoAdapter(this, listOf()) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ui 관련 코드
        rv_main_repo.adapter = adapter
        rv_main_repo.layoutManager = LinearLayoutManager(this)

        // 데이터를 받아오는(서버든, 로컬이든은 repository 내부에서 결정한다!) 비동기 코드
        repository.getRepo("greedy0110")
            // 어디서든 데이터를 받아오면! 받아온 데이터를 가지고...
            .subscribe { data ->
                // ui 를 갱신해준다.
                adapter.data = data
                adapter.notifyDataSetChanged()
            }.apply { addDisposable(this) }
    }
}
  • activity 코드에서 repository를 주입받는다. (using koin)
private val repository: GithubRepository by inject() // application 에서 startKoin 으로 등록한 repository 를 받아!

 

  • activity 코드에서 repository의 비동기 함수를 호출하고 결과를 처리하는 방법을 선언한다. (using rxjava)
// 데이터를 받아오는(서버든, 로컬이든은 repository 내부에서 결정한다!) 비동기 코드
repository.getRepo("greedy0110")
    // 어디서든 데이터를 받아오면! 받아온 데이터를 가지고...
    .subscribe { 
    	// 비동기 데이터 들어면 이렇게 할거야!!
    }.apply { addDisposable(this) }

 

  • repository 코드
    • 실제 network 통신 발생 (using retrofit)
    • 만약 실제 network 통신에서 에러가 발생한다면 로컬 데이터를 건네준다.
    • network 통신이 완료되면 데이터를 파싱 한다. (using gson converter)
    • actvity 코드에 결과를 전달한다.
  • 비동기적으로 결과가 도착하면 데이터를 이용해 화면을 갱신한다.
.subscribe { data ->
	// ui 를 갱신해준다.
	adapter.data = data
	adapter.notifyDataSetChanged()
}

 

꿈과 희망이 가득한 UI 코드다. 어려워 보이는 코드는 없다. simple is the best! 다음은 repository 코드로 들어가자. 변경이 가장 많이 일어나는 ui 코드를 간단하게 유지하기 위해서 repository에서 하는 활동을 확인하러 가보자. 침 한번 삼키고


Repository 코드

  위에서 repository 코드에서 일어나는 마법 같은 일을 어떤 식으로 처리하는지 확인해보자. 참고로 이 부분을 볼 때 rxjava에 대한 지식이 없으면 이해하기 어렵다.

 

repository.GithubRepository.kt

class GithubRepository (
    private val remote: RemoteSource,
    private val local: LocalSource,
    private val scheduler: GithubScheduler
) {
    fun getRepo(name: String): Observable<List<Repo>> {
        return remote.getRepo(name)
            .subscribeOn(scheduler.io())
            .timeout(3, TimeUnit.SECONDS) // 서버에서 정보를 3초이상 못받아오면 local 데이터를 보여줘볼까? 3초후 onError 발생!
            .doOnError { Log.e("seungmin", "$it 발생") }
            .onErrorResumeNext(local.getRepo(name))
            .observeOn(scheduler.ui())
    }
}

우선 동작 방식을 설명하기에 앞서 멤버 변수들부터 정리하자.

private val remote: RemoteSource

원격 저장소를 의미하는 인터페이스이다. 필자의 프로젝트에서는 retrofit을 이용해 github api를 받아오도록 구성하였다.

class RetrofitRemoteSource: RemoteSource {
    // https://developer.github.com/v3/ 참조
    private val baseUrl = "https://api.github.com"
    private val retrofit: GithubApi by lazy {
        Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(OkHttpClient()) // TODO 이 부분에 대한 정확한 이해 필요. 없으면 http 처리에 문제가 발생
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // Call 객체를 RxJava 로 변환하겠다.
            .addConverterFactory(GsonConverterFactory.create()) // 받아온 데이터를 json 형태로 변환해서 해석하겠다.
            .build().create(GithubApi::class.java)
    }

    override fun getRepo(name: String): Observable<List<Repo>> {
        return retrofit.getRepo(name)
            .map { it.map { repoResponse ->  RepoMapper.toUIData(repoResponse) } } // retrofit response 를 변환해서 data package 에 데이터로 반환해준다.
    }
}

해당 remote soruce에서 결과로 나오는 데이터 형은 ui에서 사용할 data 형태로 변환돼서 나온다. 따라서 서버 명세가 바뀌면 mapper를 적절히 변경해서 원하는 결과가 나오게끔 만들어줘야 한다.

 

private val local: LocalSource

디바이스 내부 저장소를 의미하는 인터페이스이다. 필자의 프로젝트에서는 간단한 mock 구현을 사용해 더미 데이터를 단순 반환하도록 구성하였다.

 

해당 local source는 네트워크 통신 없이 문제 있을 때 repository가 의지하는 데이터 저장소다. (Room을 이용해 내부 DB로 구성한다면 좋을 것으로 생각한다. 아직 안 해봤다.)

 

private val scheduler: GithubScheduler

RxJava Scheduler를 의미한다. 비동기 통신을 위한 io() 스케쥴러와 최종적으로 화면에 그려줄 ui() 스케쥴러를 가지고 있다. repository를 단독으로 테스트하기 위해선 해당 인터페이스를 통해서 스케쥴링을 해야 한다.

 


이제 repository.getRepo() 함수로 들어가 보자.

fun getRepo(name: String): Observable<List<Repo>> {
	return remote.getRepo(name)
	.subscribeOn(scheduler.io())
	.timeout(3, TimeUnit.SECONDS) // 서버에서 정보를 3초이상 못받아오면 local 데이터를 보여줘볼까? 3초후 onError 발생!
	.doOnError { Log.e("seungmin", "$it 발생") }
	.onErrorResumeNext(local.getRepo(name))
	.observeOn(scheduler.ui())
}

몇 가지 포인트로 설명을 하려고 한다.

 

  • Scheduler
subscribeOn(scheduler.io())

observeOn(scheduler.ui())

  subscribeOn 은 Observable에 처리할 값이 발생할 때, 그 처리를 할 초기 쓰레드를 지정한다. 따라서 우리는 이 코드를 작성함으로써 네트워크 통신을 IO 쓰레드에서 처리하도록 설정하는 것이다.

 

 observeOn 은 observeOn 호출 이후에 이어지는 연산들을 처리할 쓰레드를 지정 한다. 따라서 우리는 이 코드를 함수의 반환 마지막에 적어주어서 ui 코드에서는 항상 안드로이드 메인쓰레드에서 작업을 처리할 수 있다. (UI 갱신을 할 수 있다는 의미이다.)

 

생각보다 이해하기 까다롭지만 한번 이해하면 다음부턴 젓가락질처럼 사용할 수 있다. 다음은 공식 문서 링크다. 더 궁금하면 들어가서 읽어보도록 하자.

 

ReactiveX - Scheduler

In RxJS you obtain Schedulers from the Rx.Scheduler object or as independently-implemented objects. The following table shows the varieties of Scheduler that are available to you in RxJS:. Schedulerpurpose Rx.Scheduler.currentThreadschedules work as soon a

reactivex.io

 

  • 에러 처리
.timeout(3, TimeUnit.SECONDS) // 서버에서 정보를 3초이상 못받아오면 local 데이터를 보여줘볼까? 3초후 onError 발생!
.doOnError { Log.e("seungmin", "$it 발생") }
.onErrorResumeNext(local.getRepo(name))

 timeout 을 통해 3초 이상 데이터를 못 받아오면 에러로 판단하여 error 가 발생하게 제작하였다.

 doOnError를 통해 에러 메시지가 발생하면 콘솔 로그를 찍어 디버깅을 용이하게 하였다.

 onErrorResumeNext 이 코드가 필자가 이번 프로젝트에서 가장 재미있게 생각하는 코드인데, 만약 에러가 발생하면 local.getRepo를 반환하는 코드이다. 이로써 네트워크 연결에 문제가 발생하면 로컬에 저장된 정보를 받아 올 수 있다.

 

  핑계 아닌 핑계를 대자면 해당 repository의 구성은 너무나도 불완전하다. 실제로 이런 식으로 코딩을 하는지도 모르겠고 필자의 뇌와 공식문서를 참조해서 코딩한 것이다.

 

  따라서 에러 처리 부분을 더욱 우아하고 옳은 방법으로 하는 코드가 많을 것이고, rxjava를 다양하게 활용해보며 repository를 즉, 데이터를 받아오고, 캐싱하고, 네트워크 에러에 대응하는 코드를 제작할 수 있을 것이다. (마구마구 기대가 되지 않는가? 이 코드가 한심한 당신 바로 풀 리퀘스트로 필자를 혼내주기 바란다. :) )

 

코딩 작업 흐름

  프로그램 동작 흐름은 충분히 봤다. 다음은 코딩할 때 작업 흐름을 보자. 프로젝트에 틀에 맞게 작업을 하려면 예전보다 들춰봐야 하는 파일이 늘어난다. 정말이지 귀찮다. 깔끔하게 코드를 나누면 나누고 정리된 만큼 들춰볼 범위를 잘 알고 있어야 한다는 것이다.

 

그래서 틀을 잡고 시작하는 프로젝트는 아예 코딩을 할 때 통상적으로 진행되는 흐름이 있기 마련이다. 간단히 정리해봤다.

 

  1. ui에서 그려줄 data를 정의한다. (data package)
  2. data를 사용해 ui 코드를 작성한다 (ui package, 서버의 제작 없이도 진행 가능)
  3. repository에 해당 data를 받아올 함수를 정의한다. (repository package, 비동기적으로 데이터를 받아오도록 정의하자)
  4. 더미 데이터를 사용해 정의한 함수를 구현한다.
  5. repository를 koin module에 등록한다. (이미 등록했다면 무시해도 좋다.)
  6. ui 코드에서 repository를 주입(inject) 받는다.
  7. 사용하자! ui코드 뒤치다꺼리는 repository가 해줄 거다

 

결론

  최종 결론은 아마 Artic 개발이 완료된 다음에야 나올 것 같지만, 그래도 샘플 프로젝트로 내가 원하는 바를 잘 얘기했다고 생각한다. 개인적으로 이 구조를 생각하며 느낀 점을 적으며 글을 끝내보자.

 

  • 위와 같은 구조를 사용하면 서버에서 변경이 일어날 때 ui 코드의 변경 없이 repository 만 변경하면 된다.
  • 네트워크 오류에 대해서 ui 코드상 특별히 해줘야 할 코드가 없다. 따라서 네트워크 오류 문제를 변경할 때 repository 정책만 변경하면 된다.
  • koin을 사용해서 repository 생성자에 필요한 요소들이 달라져도 ui 코드의 변경이 필요 없다.
  • retrofit + rxjava + gson의 조합은 json으로 통신하는 restful api 구현에 최적이다. ( :) )
Comments