본문 바로가기
안드로이드

[Android] 네트워크 요청을 처리하는 여러 가지 방법 (Retrofit2, 비동기 처리)

by algosketch 2023. 10. 15.

참고사항 : 이 글은 Coroutine 위주로 설명하고 있으며, ReactiveX 라이브러리에 대해서는 설명하지 않는다. Retrofit의 활용 방법 위주로 설명하고 있으며, 코루틴이나 상태 관리 방법에 대해 자세히 다루지는 않는다. 개인적인 의견이 포함되어 있으므로 정답을 찾는다는 생각보다는 참고 코드 정도로 활용하는 것이 좋다.

 

0. 잡담

"안드로이드 retrofit"이라고 검색하면 약 10번째에 과거에 작성했던 내 글이 나온다. ("android retrofit" 검색은 약 40번째) 해당 글은 응애 시절에 작성했던 글로 적절하지 않은 내용을 소개하고 있다. 부끄럽게도 누적 조회수가 1000을 넘겼다. 그래서 더 정확한 내용을 소개하기 위해 새로 작성하게 되었다. 정확하지 않은 내용을 작성하더라도 누군가 지적해주지 않으니 더 신중하게 글을 올려야겠다는 생각이 든다.

원래는 Retrofit2만 다룰 예정이었으나 작성하다 보니 욕심이 생겨 네트워크 요청을 처리하는 방법이라는 제목으로 변경 되었다. 후딱 작성하고 내 할 일 하려고 했는데 왜 이렇게 된 건지 허허... 다만 한 번 작성해 두면 이 글 링크를 주면서 "이거대로 해요" 하기는 좋을 것 같다.

 

1. 기반 코드

https://github.com/HamBP/retrofit-sample

 

GitHub - HamBP/retrofit-sample: 블로그에 올릴 Retrofit2 샘플 코드입니다.

블로그에 올릴 Retrofit2 샘플 코드입니다. Contribute to HamBP/retrofit-sample development by creating an account on GitHub.

github.com

위 리포지토리에서 구현 코드를 확인할 수 있다.

 

  • AAC ViewModel을 사용한다. (viewModelScope를 사용하기 위함)
  • 직렬화 라이브러리는 Gson을 사용한다. (다른 라이브러리를 사용해도 됨)
  • 예시 코드는 실제 서버로 요청하지 않고, interceptor에서 가짜 데이터를 내려준다.

API를 통해 받아올 데이터는 위와 같은 모양이고, ViewModel에서 활용할 예정이다. ViewModel 코드의 10번 라인은 interface로 정의한 Retrofit API 객체를 만들어주는 코드이고, 라이브러리에서 제공하는 객체는 아니다. 중요한 내용은 아니라 구현 코드는 생략한다.

UI의 상태에 관련된 내용은 UI 레이어에 대한 공식 문서나 이 블로그를 참고하면 도움이 될 것이다.

 

2. 콜백 방식

1) enqueue - 콜백으로 호출하기 (fetchArticleUsingCallback1)

반환 타입을 Call<T>로 정의하면 요청 결과를 콜백 방식으로 받을 수 있다. 이 방식은 정의한 interface의 함수 호출이 아닌 enqueue 함수를 호출했을 때 네트워크 요청을 보내게 된다. 인자로 Callback객체를 받으며, 각각 onResponse와 onFailure를 정의하여 성공했을 때와 실패했을 때의 로직을 처리하면 된다. HTTP status code 400과 같이 요청에는 성공했으나 실패 응답이 올 경우 onResponse에 도달하지만 body를 가져올 수 없다. 그 외에 서버에 연결을 실패하는 등의 예외가 발생할 경우 onFailure로 빠지게 된다.

2) execute - Response값 반환하기 (fetchArticleUsingCallback2)

Call 객체는 또한 execute 함수도 사용할 수 있다. 이 함수를 사용하면 반환 값으로 Response<T>를 받을 수 있다. 다만, 이 함수는 메인 스레드(참고: ViewModelScope도 메인 스레드이다.)에서 네트워크 작업을 하여, 직접 호출할 경우 NetworkOnMainThreadException이 발생한다. 반드시 별도의 스레드나 코루틴으로 감싼 뒤 호출해야 함에 주의하자. 예시에서는 작성하지 않았지만 예외 처리 로직도 추가로 작성해야 한다.

3) 단점

콜백 방식은 Retrofit2를 사용하는 기본적인 방법으로, 안드로이드를 처음 공부할 때 쉽게 접할 수 있는 방법이다. 설명을 위해 간단한 예시 코드를 작성했음에도 불구하고 그리 간결한 느낌은 들지 않는다. 특히 콜백 방식의 비동기 처리 특성상 콜백을 연속으로 호출해야 할 경우 코드가 매우 복잡해진다. (콜백 지옥이라 부른다.) 그래서 위에서 소개한 방식은 잘 사용되지 않는다.

4) 장점

좋은 경험이었다.

 

3. Coroutine

Retrofit은 위와 같이 메서드 앞에 suspend를 붙여주어 쉽게 코루틴을 사용할 수 있다. 내부적으로 스레드를 변경해주고 있으므로 일반적인 용례에서는 withContext로 디스패처를 설정해줄 필요가 없다. T 타입과 Response<T> 타입 모두 사용 가능하며, Response는 앞에서 본 예시와 같이 isSucessful로 성공 여부를 확인하거나 상태 코드를 확인하는 등 HTTP 응답 값과 관련된 정보를 확인할 수 있다.

 

suspend + Response 를 사용하면 ViewModel에서 위 코드처럼 사용할 수 있다.

 

여기까지가 Retrofit2에 대한 기본적인 사용 방법이다. 실제 개발에서는 조금 더 복잡(?)한 방법으로 사용하는데, 어떻게 활용할 수 있을 지 확인해 보자.

 

API interface의 suspend 함수를 각각 Result와 Flow로 래핑하는 방법을 살펴볼 예정이다.

우선, 제대로된 사용을 위해 위와 같이 API와 Repository 사이에 Repository를 추가했다. Repository는 데이터 레이어의 구체적인 로직을 UI 레이어가 알지 못하게 추상화하는 역할을 한다. 대신 UI는 API를 더 간편하게 사용할 수 있다. 데이터 레이어에 대한 더 자세한 내용은 공식문서를 참고하자.

 

 

4. Kotlin Result로 감싸기

Result는 Kotlin 1.3버전부터 사용가능한, 언어에서 지원해 주는 예외 처리 관련 클래스이다. 예시에서는 일부 함수만 사용했지만 다양한 메서드를 지원한다.

runCatching 함수는 코드 블록을 try-catch로 감싸서 성공과 실패 여부를 처리하는 간단한 로직으로 이루어져 있다. isSuccess와 같은 프로퍼티를 통해 성공 여부를 확인할 수 있다. 예시 코드에서는 kotlin.runCatching처럼 패키지명이 붙었는데, kotlin을 제거하면 ArticleRepository를 receiver로 갖는 확장 함수가 호출된다. 현재 코드의 동작에는 차이가 없으니 상황에 따라 적절한 함수를 호출하면 된다.

 

Result 객체를 사용하는 쪽에서는 onSuccess와 onFailure와 같은 함수 체이닝을 통해 사용할 수 있다. Result에는 getOrThrow()와 같은 메서드도 있어 단순히 값을 가져오는 것도 가능하다.

 

5. Flow로 감싸기

개인적으로 이해하기 어려웠던 부분이 Flow로 감싸는 부분이다. 안드로이드 공식문서에서는 one shot 요청에 대해서는 suspend를 사용하고, 지속적으로 값을 갱신하는 경우에는 Flow를 사용한다. 하지만 다른 코드를 읽다보면 one shot 요청에 대해서도 Flow를 사용하는 코드를 어렵지 않게 찾을 수 있다. 이 경우 Flow는 하나의 값만 생산하고 종료되는 임시 Flow를 만들어 사용한다고 이해하면 된다. 즉, 사용된 한 번 값을 받아온 Flow 객체는 재활용되지 않는다.

따라서 위 코드에서는 하나의 article을 emit하면 종료되는 Flow 객체를 반환한다. 사용하는 쪽에서도 하나의 값만 emit하도록 코드를 작성했다고 가정하고 사용하겠지만, Flow 자체는 여러 값을 생산할 수 있도록 설계되었음에 유의하자.

 

Flow를 활용하면 이 코드처럼 다양한 상황에 대한 로직을 메서드를 체이닝하여 작성할 수 있다. 기존 코드에서는 성공, 실패, 로딩 과 같은 값을 설정해 주는 로직이 하나의 함수 안에 섞여 있었는데, Flow의 메서드를 활용하면 람다 코드 블럭으로 분리되어 각 로직이 어떤 상황에 필요한 것인지 파악하기 쉬워졌다. Flow는 예시에서 사용한 메서드 외에도 여러 가지 메서드(연산자)를 지원한다. 다만 Flow 자체는 여러 개의 값을 생산할 수 있으며, 단순히 suspend를 사용할 때보다 많은 코드가 작성되는 번거로움이 있다. 

예시에서는 launchIn메서드를 사용했지만 viewModelScope + collect의 조합으로 사용해도 된다. 한 가지 주의해야할 점은 flow { } 빌더로 만들어진 객체는 cold stream이다. 소비자가 없으면 값을 생산하지 않는다. 이 경우엔 네트워크 요청이 발생하지 않는다. 따라서 반드시 collect와 같은 소비함수를 호출해야 한다. (launchIn 내부적으로 collect를 호출)

 

 

6. 그 외

다른 라이브러리와 함께 Retrofit2을 사용하는 방법도 있다. 위에서 소개한 방법 외에도 커스텀으로 API 결과를 sealed class로 래핑하여 반환하는 경우도 있다. 한국에서 가장(?) 유명한 안드로이드 오픈소스 개발자의 Sandwich라는 라이브러리도 이와 같은 래핑을 도와준다. 다음 글을 추천한다.

https://velog.io/@skydoves/retrofit-api-handling-sandwich

 

안드로이드 Retrofit + Coroutines의 API 응답 및 에러 핸들링 - Sandwich

데이터 커뮤니케이션 횟수가 증가함에 따라 애플리케이션 아키텍처의 복잡성도 함께 증가합니다. 오픈소스 라이브러리 Sandwich를 활용하여 multi-layered 아키텍처에서 API 응답 및 에러 핸들링을 하

velog.io

 

7. 결론

개인적으로는 라이브러리를 잘 안 쓰는 편이라 6번 방법을 제외하고 모든 방법을 다 사용해 봤다. (라이브러리 사용 여부와는 별개로 위 링크의 글은 읽을 만한 가치가 있다) 사용했던 시간의 순서로 작성했기 때문에 지금은 5번 방법을 주로 사용하지만, 어떤 방법이 정답이라고 할 수는 없다. 단지 내 경우에는 점점 다양한 케이스에서도 적용 가능한 방법을 찾다보니 복잡하지만 많은 연산자를 지원해 주는 Flow를 사용하게 되었다.

프로젝트 상황에 맞는 방법을 선택하는데 참고가 되었으면 좋겠다.

 

8. 참고 문서

글을 쓸 때 참고한 문서는 아니지만, 과거에 참고했던 글 목록이다.

https://kotlinlang.org/docs/flow.html#flows-are-cold

 

Asynchronous Flow | Kotlin

 

kotlinlang.org

https://thdev.tech/kotlin/2021/01/12/Retrofit-Coroutines/

 

Retrofit2와 Coroutines 사용 시 스케줄러는 어떻게 처리할까? - 내부 코드로 알아보자. |

I’m an Android Developer.

thdev.tech

https://toss.tech/article/kotlin-result

 

에러 핸들링을 다른 클래스에게 위임하기 (Kotlin 100% 활용)

Kotlin의 Result로 MSA에서 에러가 전파되지 않는 안전한 환경을 만드는 방법을 알아봅니다.

toss.tech