인터넷에서 데이터 가져오기

1. 시작하기 전에

시중에 나와 있는 Android 앱은 대부분 백엔드 서버에서 이메일, 메시지 또는 다른 정보를 가져오는 등의 네트워크 작업을 실행하기 위해 인터넷에 연결합니다. 인터넷에 연결하여 사용자 데이터를 표시하는 앱의 예로 Gmail, YouTube, Google 포토와 같은 앱을 들 수 있습니다.

이 Codelab에서는 오픈소스 및 커뮤니티 기반 라이브러리를 사용하여 데이터 영역을 빌드하고 백엔드 서버에서 데이터를 가져옵니다. 이렇게 하면 데이터를 가져오는 작업이 크게 간소화되고 앱이 Android 권장사항(예: 백그라운드 스레드에서 작업 실행)을 따르는 데도 도움이 됩니다. 또한, 인터넷을 사용할 수 없거나 속도가 느린 경우 오류 메시지를 표시하여 네트워크 연결 문제에 관해 지속적으로 사용자에게 알립니다.

기본 요건

  • 구성 가능한 함수를 만드는 방법에 관한 기본 지식
  • Android 아키텍처 구성요소인 ViewModel의 사용 방법에 관한 기본 지식
  • 장기 실행 작업에 코루틴을 사용하는 방법에 관한 기본 지식
  • build.gradle.kts에 종속 항목을 추가하는 방법에 관한 기본 지식

학습할 내용

  • REST 웹 서비스의 정의
  • Retrofit 라이브러리를 사용하여 인터넷상의 REST 웹 서비스에 연결하고 응답 받는 방법
  • 직렬화(kotlinx.serialization) 라이브러리를 사용하여 JSON 응답을 데이터 객체로 파싱하는 방법

실행할 작업

  • 웹 서비스 API 요청을 실행하고 응답을 처리하도록 시작 앱을 수정합니다.
  • Retrofit 라이브러리를 사용하여 앱의 데이터 영역을 구현합니다.
  • kotlinx.serialization 라이브러리를 사용하여 웹 서비스의 JSON 응답을 앱의 데이터 객체 목록으로 파싱하고 이를 UI 상태에 연결합니다.
  • Retrofit의 코루틴 지원을 사용하여 코드를 단순화합니다.

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터
  • Mars Photos 앱의 시작 코드

2. 앱 개요

화성 표면의 이미지를 표시하는 Mars Photos라는 앱을 사용하여 작업합니다. 이 앱은 웹 서비스에 연결하여 화성 사진을 검색하고 표시합니다. 이러한 이미지는 NASA의 화성 탐사 로봇이 화성에서 촬영한 실제 사진입니다. 다음 이미지는 최종 앱의 스크린샷으로 이미지 그리드를 포함합니다.

68f4ff12cc1e2d81.png

이 Codelab에서 빌드하는 앱 버전에는 시각적 플래시가 많지 않습니다. 인터넷에 연결하고 웹 서비스를 사용하여 원시 속성 데이터를 다운로드하는 앱의 데이터 영역 부분에 초점을 맞춘 Codelab이기 때문입니다. 앱이 이 데이터를 올바르게 검색하고 파싱하는지 확인하려면 Text 컴포저블에 백엔드 서버에서 수신한 사진 수를 출력하면 됩니다.

a59e55909b6e9213.png

3. MarsPhotos 시작 앱 살펴보기

시작 코드 다운로드하기

시작하려면 시작 코드를 다운로드하세요.

GitHub 저장소를 클론하여 코드를 가져와도 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout starter

Mars Photos GitHub 저장소에서 코드를 찾아볼 수 있습니다.

시작 코드 실행하기

  1. Android 스튜디오에서 다운로드한 프로젝트를 엽니다. 프로젝트의 폴더 이름은 basic-android-kotlin-compose-training-mars-photos입니다.
  2. Android 창에서 app > kotlin + java를 펼칩니다. 앱에 ui라는 패키지 폴더가 있습니다. 이 폴더는 앱의 UI 레이어입니다.

de3d8666ecee9d1c.png

  1. 앱을 실행합니다. 앱을 컴파일하고 실행하면 다음과 같이 가운데에 자리표시자 텍스트가 있는 화면이 표시됩니다. 이 Codelab을 마칠 때면 자리표시자 텍스트가 가져온 사진 수로 업데이트됩니다.

95328ffbc9d7104b.png

시작 코드 둘러보기

이 작업에서는 프로젝트 구조를 숙지합니다. 다음 목록을 통해 프로젝트의 중요 파일과 폴더를 둘러봅니다.

ui\MarsPhotosApp.kt:

  • 이 파일에는 화면에 상단 앱 바 및 HomeScreen 컴포저블과 같은 콘텐츠를 표시하는 MarsPhotosApp 컴포저블이 포함되어 있습니다. 이전 단계에 있던 자리표시자 텍스트가 이 컴포저블에 표시됩니다.
  • 다음 Codelab에서 이 컴포저블은 화성 사진 백엔드 서버에서 수신된 데이터를 표시합니다.

screens\MarsViewModel.kt:

  • 이 파일은 MarsPhotosApp에 대응되는 뷰 모델입니다.
  • 이 클래스에는 marsUiState라는 MutableState 속성이 포함되어 있습니다. 속성의 값을 업데이트하면 화면에 표시되는 자리표시자 텍스트가 업데이트됩니다.
  • getMarsPhotos() 메서드는 자리표시자 응답을 업데이트합니다. Codelab의 후반부에서는 이 메서드를 사용하여 서버에서 가져온 데이터를 표시합니다. 이 Codelab의 목표는 인터넷에서 가져온 데이터를 사용하여 ViewModel 내의 MutableState를 업데이트하는 것입니다.

screens\HomeScreen.kt:

  • 이 파일에는 HomeScreenResultScreen 컴포저블이 포함되어 있습니다. ResultScreen에는 Text 컴포저블의 marsUiState 값을 표시하는 간단한 Box 레이아웃이 있습니다.

MainActivity.kt:

  • 이 활동의 유일한 작업은 ViewModel을 로드하고 MarsPhotosApp 컴포저블을 표시하는 것입니다.

4. 웹 서비스 소개

이 Codelab에서는 백엔드 서버와 통신하고 필요한 데이터를 가져오는 네트워크 서비스 레이어를 만듭니다. 작업을 구현하기 위해 Retrofit이라는 서드 파티 라이브러리를 사용합니다. 이 내용은 나중에 자세히 알아봅니다. ViewModel은 데이터 영역과 통신하며 앱의 나머지 부분은 이 구현에 투명합니다.

76551dbe9fc943aa.png

MarsViewModel은 네트워크를 호출하여 화성 사진 데이터를 가져옵니다. ViewModel에서는 MutableState를 사용하여 데이터가 변경될 때 앱 UI를 업데이트합니다.

5. 웹 서비스 및 Retrofit

화성 사진 데이터는 웹 서버에 저장됩니다. 이 데이터를 앱으로 가져오려면 인터넷상의 서버와 연결을 설정하고 통신해야 합니다.

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

오늘날 대부분의 웹 서버는 REST(REpresentational State Transfer의 약자)라는 일반적인 스테이트리스(Stateless) 웹 아키텍처를 사용해 웹 서비스를 실행합니다. 이 아키텍처를 제공하는 웹 서비스를 RESTful 서비스라고 합니다.

URI(Uniform Resource Identifier)를 통해 표준화된 방식으로 RESTful 웹 서비스에 요청을 전송합니다. URI는 리소스의 위치 또는 리소스에 액세스하는 방법을 암시하지 않고 이름으로 서버의 리소스를 식별합니다. 예를 들어, 이 과정의 앱에서는 다음 서버 URI를 사용하여 이미지 URL을 가져옵니다. (이 서버는 화성의 부동산 및 화성 사진을 모두 호스팅합니다.)

android-kotlin-fun-mars-server.appspot.com

URL(Uniform Resource Locator)은 리소스가 존재하는 위치와 리소스를 가져오는 메커니즘을 지정하는 URI의 하위 집합입니다.

예:

다음 URL은 사용 가능한 화성 부동산 속성 목록을 모두 가져옵니다.

https://android-kotlin-fun-mars-server.appspot.com/realestate

다음 URL은 화성 사진의 목록을 가져옵니다.

https://android-kotlin-fun-mars-server.appspot.com/photos

이러한 URL은 HTTP(Hypertext Transfer Protocol, http:)를 통해 네트워크에서 가져올 수 있는 식별된 리소스(예: /realestate 또는 /photos)를 참조합니다. 이 Codelab에서는 /photos 엔드포인트를 사용합니다. 엔드포인트는 서버에서 실행되는 웹 서비스에 액세스할 수 있는 URL입니다.

웹 서비스 요청

각 웹 서비스 요청은 URI를 포함하고 있으며 Chrome과 같은 웹브라우저에서 사용하는 것과 동일한 HTTP 프로토콜을 사용하여 서버에 전송됩니다. HTTP 요청은 해야 할 일을 서버에 알리는 작업을 포함하고 있습니다.

일반적인 HTTP 작업에는 다음이 포함됩니다.

  • GET: 서버 데이터를 가져옵니다.
  • POST: 서버에 새 데이터를 만듭니다.
  • PUT: 서버에 있는 기존 데이터를 업데이트합니다.
  • DELETE: 서버에서 데이터를 삭제합니다.

앱이 화성 사진에 관한 HTTP GET 요청을 서버에 보내면 서버는 이미지 URL을 포함하는 응답을 앱에 반환합니다.

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

웹 서비스의 응답은 XML(eXtensible Markup Language) 또는 JSON(JavaScript Object Notation)과 같은 일반적인 데이터 형식 중 하나로 형식이 지정됩니다. JSON 형식은 키-값 쌍으로 구조화된 데이터를 나타냅니다. 앱은 JSON을 사용하여 REST API와 통신하며, 이에 관해서는 이후 작업에서 자세히 알아봅니다.

이 작업에서는 서버에 네트워크 연결을 설정하고 서버와 통신하고 JSON 응답을 받습니다. 이미 작성되어 있는 백엔드 서버를 사용합니다. 이 Codelab에서는 서드 파티 라이브러리인 Retrofit 라이브러리를 사용하여 백엔드 서버와 통신합니다.

외부 라이브러리

외부 라이브러리 또는 서드 파티 라이브러리는 핵심 Android API의 확장 프로그램과 같습니다. 이 과정에서 사용하는 라이브러리는 오픈소스로 전 세계 대규모 Android 커뮤니티의 공동 참여 활동을 통해 개발되고 유지됩니다. 이러한 리소스는 Android 개발자가 더 나은 앱을 빌드하는 데 도움이 됩니다.

Retrofit 라이브러리

이 Codelab에서 RESTful Mars 웹 서비스와 통신하기 위해 사용하는 Retrofit 라이브러리는 잘 지원되고 유지되는 라이브러리의 좋은 예입니다. 이러한 사실은 Retrofit의 GitHub 페이지를 둘러보고 미해결된 문제와 종료된 문제(일부는 기능 요청임)를 살펴보면 알 수 있습니다. 개발자가 정기적으로 문제를 해결하고 기능 요청에 응답한다면 라이브러리가 잘 유지될 가능성이 높고 앱에서 사용하기에 적합한 후보입니다. Retrofit 문서를 참고하여 라이브러리에 관해 자세히 알아볼 수도 있습니다.

Retrofit 라이브러리는 REST 백엔드와 통신합니다. Retrofit에서 코드를 생성하지만, Retrofit에 전달하는 매개변수에 따라 웹 서비스의 URI를 제공해야 합니다. 이 주제에 관해서는 이후 섹션에서 자세히 알아봅니다.

26043df178401c6a.png

Retrofit 종속 항목 추가하기

Android Gradle을 사용하면 프로젝트에 외부 라이브러리를 추가할 수 있습니다. 라이브러리 종속 항목 외에 라이브러리가 호스팅되는 저장소도 포함해야 합니다.

  1. 모듈 수준 gradle 파일 build.gradle.kts (Module :app)를 엽니다.
  2. Retrofit 라이브러리의 dependencies 섹션에 다음 줄을 추가합니다.
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

두 라이브러리는 함께 작동합니다. 첫 번째 종속 항목은 Retrofit2 라이브러리 자체를 위한 것이며, 두 번째 종속 항목은 Retrofit 스칼라 변환기를 위한 것입니다. Retrofit2는 Retrofit 라이브러리의 업데이트 버전입니다. 이 스칼라 변환기를 사용하면 Retrofit이 JSON 결과를 String으로 반환할 수 있습니다. JSON은 클라이언트와 서버 간에 데이터를 저장하고 전송하기 위한 형식입니다. JSON에 관해서는 이후 섹션에서 자세히 알아보겠습니다.

  1. Sync Now를 클릭하여 새 종속 항목으로 프로젝트를 다시 빌드합니다.

6. 인터넷에 연결하기

Retrofit 라이브러리를 사용하여 Mars 웹 서비스와 통신하고 원시 JSON 응답을 String으로 표시합니다. Text 자리표시자는 반환된 JSON 응답 문자열을 표시하거나 연결 오류를 나타내는 메시지를 표시합니다.

Retrofit은 웹 서비스의 콘텐츠를 기반으로 앱의 네트워크 API를 만듭니다. 웹 서비스에서 데이터를 가져오고 데이터를 디코딩하여 객체 형식(예: String)으로 반환하는 방법을 알고 있는 별도의 변환기 라이브러리를 통해 데이터를 라우팅합니다. Retrofit에는 XML 및 JSON과 같이 많이 사용되는 데이터 형식을 위한 지원이 내장되어 있습니다. Retrofit은 궁극적으로 이 서비스를 호출하고 소비하는 코드를 만들며, 여기에는 중요한 세부정보가 포함됩니다(예: 백그라운드 스레드에서 요청 실행).

8c3a5c3249570e57.png

이 작업에서는 웹 서비스와 통신하기 위해 ViewModel이 사용하는 Mars Photos 프로젝트에 데이터 영역을 추가합니다. 다음 단계에 따라 Retrofit 서비스 API를 구현합니다.

  • 데이터 소스인 MarsApiService 클래스를 만듭니다.
  • 기본 URL과 문자열을 변환하는 변환기 팩토리가 포함된 Retrofit 객체를 만듭니다.
  • Retrofit이 웹 서버와 통신하는 방법을 설명하는 인터페이스를 만듭니다.
  • Retrofit 서비스를 만들고 API 서비스에 관한 인스턴스를 앱의 나머지 부분에 노출합니다.

위 단계를 다음과 같이 구현합니다.

  1. Android 프로젝트 창에서 com.example.marsphotos 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Package를 선택합니다.
  2. 팝업에서, 제안된 패키지 이름 끝에 network를 추가합니다.
  3. 새 패키지 아래에 새 Kotlin 파일을 만듭니다. 파일 이름을 MarsApiService로 지정합니다.
  4. network/MarsApiService.kt를 엽니다.
  5. 웹 서비스의 기본 URL에 다음 상수를 추가합니다.
private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. 이 상수 바로 아래에 Retrofit 빌더를 추가하여 Retrofit 객체를 빌드하고 만듭니다.
import retrofit2.Retrofit

private val retrofit = Retrofit.Builder()

Retrofit에서는 웹 서비스의 기본 URI 및 변환기 팩토리가 있어야 웹 서비스 API를 빌드할 수 있습니다. 변환기는 웹 서비스에서 얻은 데이터로 해야 할 일을 Retrofit에 알립니다. 이 경우에는 Retrofit이 웹 서비스의 JSON 응답을 가져와 String으로 반환하려고 합니다. Retrofit에는 문자열과 기타 기본 유형을 지원하는 ScalarsConverter가 있습니다.

  1. ScalarsConverterFactory 인스턴스를 사용하여 빌더에서 addConverterFactory()를 호출합니다.
import retrofit2.converter.scalars.ScalarsConverterFactory

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. baseUrl() 메서드를 사용하여 웹 서비스의 기본 URL을 추가합니다.
  2. build()를 호출하여 Retrofit 객체를 만듭니다.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. Retrofit 빌더 호출 아래에 Retrofit이 HTTP 요청을 사용하여 웹 서버와 통신하는 방법을 정의하는 MarsApiService라는 인터페이스를 정의합니다.
interface MarsApiService {
}
  1. getPhotos()라는 함수를 MarsApiService 인터페이스에 추가하여 웹 서비스에서 응답 문자열을 가져옵니다.
interface MarsApiService {
    fun getPhotos()
}
  1. @GET 주석을 사용하여 Retrofit에 이 요청이 GET 요청임을 알리고 웹 서비스 메서드의 엔드포인트를 지정합니다. 이 경우 엔드포인트는 photos입니다. 이전 작업에서 언급했듯이 이 Codelab에서는 /photos 엔드포인트를 사용합니다.
import retrofit2.http.GET

interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

getPhotos() 메서드가 호출되면 Retrofit은 요청을 시작하는 데 사용된 기본 URL(Retrofit 빌더에서 정의함)에 엔드포인트 photos를 추가합니다.

  1. 함수의 반환 유형을 String에 추가합니다.
interface MarsApiService {
    @GET("photos")
    fun getPhotos(): String
}

객체 선언

Kotlin에서 객체 선언은 싱글톤 객체를 선언하는 데 사용됩니다. 싱글톤 패턴은 객체의 인스턴스가 단 하나만 생성되도록 보장하며 이 객체에 대해 하나의 전역 액세스 포인트를 가집니다. 객체 초기화는 스레드로부터 안전하며 첫 액세스 시 실행됩니다.

다음은 객체 선언 및 액세스의 예입니다. 객체 선언에는 항상 object 키워드 뒤에 이름이 있습니다.

예:

// Example for Object declaration, do not copy over

object SampleDataProvider {
    fun register(provider: SampleProvider) {
        // ...
    }
​
    // ...
}

// To refer to the object, use its name directly.
SampleDataProvider.register(...)

Retrofit 객체에서 create() 함수를 호출하면 메모리, 속도, 성능 측면에서 비용이 많이 듭니다. 앱에는 Retrofit API 서비스의 인스턴스가 하나만 필요하므로 객체 선언을 사용하여 앱의 나머지 부분에 서비스를 노출합니다.

  1. MarsApiService 인터페이스 선언 외부에서 MarsApi라는 공개 객체를 정의하여 Retrofit 서비스를 초기화합니다. 이 객체는 앱의 나머지 부분에서 액세스할 수 있는 공개 싱글톤 객체입니다.
object MarsApi {}
  1. MarsApi 객체 선언 내부에서는 MarsApiService 유형의 retrofitService라는 지연 초기화 retrofit 객체 속성을 추가합니다. 최초 사용 시 초기화되도록 하기 위해 이러한 지연 초기화를 사용합니다. 다음 단계에서 해결할 수 있는 오류는 무시합니다.
object MarsApi {
    val retrofitService : MarsApiService by lazy {}
}
  1. MarsApiService 인터페이스를 사용하여 retrofit.create() 메서드로 retrofitService 변수를 초기화합니다.
object MarsApi {
    val retrofitService : MarsApiService by lazy {
       retrofit.create(MarsApiService::class.java)
    }
}

Retrofit 설정이 완료되었습니다. 앱이 MarsApi.retrofitService를 호출할 때마다 호출자는 첫 번째 액세스에서 생성된 MarsApiService를 구현하는 싱글톤 Retrofit 객체와 동일한 객체에 액세스합니다. 다음 작업에서는 구현한 Retrofit 객체를 사용합니다.

MarsViewModel에서 웹 서비스 호출하기

이 단계에서는 REST 서비스를 호출한 다음 반환된 JSON 문자열을 처리하는 getMarsPhotos() 메서드를 구현합니다.

ViewModelScope

viewModelScope은 앱의 각 ViewModel을 대상으로 정의된 기본 제공 코루틴 범위입니다. 이 범위에서 실행된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소됩니다.

viewModelScope를 사용하여 코루틴을 실행하고 백그라운드에서 웹 서비스를 요청할 수 있습니다. viewModelScopeViewModel에 속하므로 앱이 구성 변경을 진행해도 요청이 계속됩니다.

  1. MarsApiService.kt 파일에서 getPhotos()를 정지 함수로 만들어 함수를 비동기식으로 만들고 호출 스레드를 차단하지 않도록 합니다. 이 함수는 viewModelScope 내에서 호출합니다.
@GET("photos")
suspend fun getPhotos(): String
  1. ui/screens/MarsViewModel.kt 파일을 엽니다. 아래로 스크롤하여 getMarsPhotos() 메서드를 찾습니다. 상태 응답을 "Set the Mars API Response here!"로 설정하는 줄을 삭제하여 getMarsPhotos() 메서드를 비웁니다.
private fun getMarsPhotos() {}
  1. getMarsPhotos() 내부에서 viewModelScope.launch를 사용하여 코루틴을 실행합니다.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

private fun getMarsPhotos() {
    viewModelScope.launch {}
}
  1. viewModelScope 내부에서 싱글톤 객체 MarsApi를 사용하여 retrofitService 인터페이스에서 getPhotos() 메서드를 호출합니다. 반환된 응답을 listResult라는 val에 저장합니다.
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. 백엔드 서버에서 받은 결과를 marsUiState에 할당합니다. marsUiState는 최근의 웹 요청 상태를 나타내는 변경 가능한 상태 객체입니다.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
  1. 앱을 실행합니다. 그러면 앱이 즉시 닫히고 오류 팝업이 표시될 수도 있고 표시되지 않을 수도 있습니다. 이는 앱이 비정상 종료된 것입니다.
  2. Android 스튜디오에서 Logcat 탭을 클릭하고 로그에서 '------- beginning of crash'와 같은 줄로 시작하는 오류를 확인합니다.
    --------- beginning of crash
22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.example.android.marsphotos, PID: 22803
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)
...

이 오류 메시지는 앱에 INTERNET 권한이 없을 수도 있음을 나타냅니다. 다음 작업에서는 앱에 인터넷 권한을 추가하고 이 문제를 해결하는 방법을 설명합니다.

7. 인터넷 권한 및 예외 처리 추가하기

Android 권한

Android에서 권한의 목적은 Android 사용자의 개인 정보를 보호하는 것입니다. Android 앱은 연락처, 통화 기록과 같은 민감한 사용자 데이터와 카메라, 인터넷과 같은 특정 시스템 기능에 액세스할 수 있는 권한을 선언하거나 요청해야 합니다.

앱이 인터넷에 액세스하려면 INTERNET 권한이 필요합니다. 인터넷에 연결하면 보안 문제가 발생할 수 있으므로 앱은 기본적으로 인터넷에 연결되어 있지 않습니다. 앱이 인터넷에 액세스해야 한다고 명시적으로 선언해야 합니다. 이 선언은 일반 권한으로 간주됩니다. Android 권한과 권한 유형에 관해 자세히 알아보려면 Android의 권한을 참고하세요.

이 단계에서는 앱이 AndroidManifest.xml 파일에 <uses-permission> 태그를 포함하여 필요한 권한을 선언합니다.

  1. manifests/AndroidManifest.xml을 엽니다. <application> 태그 바로 앞에 다음 줄을 추가합니다.
<uses-permission android:name="android.permission.INTERNET" />
  1. 앱을 다시 컴파일하고 실행합니다.

인터넷에 연결되면 화성 사진과 관련된 데이터를 포함하는 JSON 텍스트가 표시됩니다. 모든 이미지 레코드마다 idimg_src가 어떻게 반복되는지 관찰합니다. JSON 형식은 Codelab 후반부에서 자세히 알아봅니다.

b82ddb79eff61995.png

  1. 기기 또는 에뮬레이터에서 뒤로 버튼을 탭하여 앱을 닫습니다.

예외 처리

코드에 버그가 있습니다. 버그를 확인하려면 다음 단계를 따르세요.

  1. 기기 또는 에뮬레이터를 비행기 모드로 전환하여 네트워크 연결 오류를 시뮬레이션합니다.
  2. 최근 항목 메뉴에서 앱을 다시 열거나 Android 스튜디오에서 앱을 실행합니다.
  3. Android 스튜디오에서 Logcat 탭을 클릭하고 로그에서 다음과 같은 심각한 예외를 확인합니다.
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.marsphotos, PID: 3302

이 오류 메시지는 애플리케이션이 연결을 시도했다가 타임아웃되었음을 나타냅니다. 이러한 예외는 실시간으로 매우 자주 나타납니다. 권한 문제와 달리 이 오류는 해결할 수 없지만 처리할 수는 있습니다. 다음 단계에서는 이러한 예외를 처리하는 방법을 알아봅니다.

예외

예외는 컴파일 시간이 아닌 런타임 시 발생할 수 있는 오류로, 사용자에게 알리지 않고 갑자기 앱을 종료합니다. 이로 인해 사용자 환경이 저하될 수 있습니다. 예외 처리는 앱이 갑자기 종료되지 않도록 하는 메커니즘이며 이러한 상황을 사용자 친화적인 방법으로 처리합니다.

예외가 발생하는 이유는 0으로 나누기 또는 네트워크 연결 오류만큼 단순한 것일 수 있습니다. 이러한 예외는 이전 Codelab에서 설명한 IllegalArgumentException과 유사합니다.

서버에 연결하는 동안 발생할 수 있는 문제는 예를 들면 다음과 같습니다.

  • API에 사용된 URL 또는 URI가 잘못됨
  • 서버를 사용할 수 없어 앱을 서버에 연결할 수 없음
  • 네트워크 지연 문제가 있음
  • 기기의 인터넷 연결이 불안정하거나 기기가 인터넷에 연결되지 않음

이러한 예외는 컴파일 시간에 처리할 수 없지만, try-catch 블록을 사용하여 런타임에 처리할 수 있습니다. 자세한 내용은 예외를 참고하세요.

try-catch 블록 구문 예

try {
    // some code that can cause an exception.
}
catch (e: SomeException) {
    // handle the exception to avoid abrupt termination.
}

try 블록 내에 예외가 예상되는 코드를 추가합니다. 이 앱에서는 네트워크 호출입니다. catch 블록에 앱이 갑작스럽게 종료되는 것을 방지하는 코드를 구현해야 합니다. 예외가 있는 경우 앱이 갑자기 종료되는 대신 오류에서 복구되도록 catch 블록이 실행됩니다.

  1. getMarsPhotos()launch 블록 내에 MarsApi 호출을 감싸도록 try 블록을 추가하여 예외를 처리합니다.
  2. try 블록 뒤에 catch 블록을 추가합니다.
import java.io.IOException

viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}
  1. 앱을 한 번 더 실행합니다. 이번에는 앱이 비정상 종료되지 않습니다.

상태 UI 추가하기

MarsViewModel 클래스에서 최신 웹 요청 상태(marsUiState)가 변경 가능한 상태 객체로 저장됩니다. 그러나 이 클래스에는 로드 중, 성공, 실패와 같은 다양한 상태를 저장할 수 있는 기능이 없습니다.

  • Loading 상태는 앱이 데이터를 기다리고 있음을 나타냅니다.
  • Success 상태는 웹 서비스에서 데이터를 성공적으로 가져왔음을 나타냅니다.
  • Error 상태는 네트워크 오류 또는 연결 오류를 나타냅니다.

애플리케이션에서 이러한 세 가지 상태를 나타내려면 봉인 인터페이스를 사용합니다. sealed interface를 사용하면 가능한 값을 제한하여 상태를 관리하기가 쉬워집니다. Mars Photos 앱에서는 marsUiState 웹 응답을 다음 코드와 같이 로드 중, 성공, 오류, 세 가지 상태(데이터 클래스 객체)로 제한합니다.

// No need to copy over
sealed interface MarsUiState {
   data class Success : MarsUiState
   data class Loading : MarsUiState
   data class Error : MarsUiState
}

위 코드 스니펫에서는 성공적으로 응답된 경우 서버로부터 화성 사진 정보가 수신됩니다. 데이터를 저장하려면 Success 데이터 클래스에 생성자 매개변수를 추가합니다.

LoadingError 상태의 경우 새 데이터를 설정하고 새 객체를 만들 필요가 없습니다. 단지 웹 응답을 전달하는 것입니다. data 클래스를 Object로 변경하여 웹 응답용 객체를 만듭니다.

  1. ui/MarsViewModel.kt 파일을 엽니다. import 문 다음에 MarsUiState 봉인 인터페이스를 추가합니다. 이 인터페이스를 추가하면 MarsUiState 객체가 가질 수 있는 값이 완전해집니다.
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. MarsViewModel 클래스 내에서 marsUiState 정의를 업데이트합니다. 유형을 MarsUiState로 변경하고 MarsUiState.Loading을 기본값으로 합니다. marsUiState에 쓰는 것을 보호하기 위해 setter를 비공개로 설정합니다.
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  1. 아래로 스크롤하여 getMarsPhotos() 메서드를 찾습니다. marsUiState 값을 MarsUiState.Success로 업데이트하고 listResult를 전달합니다.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
  1. catch 블록 내부에서 실패 응답을 처리합니다. MarsUiStateError로 설정합니다.
catch (e: IOException) {
   marsUiState = MarsUiState.Error
}
  1. try-catch 외부에서 marsUiState 할당을 해제할 수 있습니다. 완성된 함수는 다음 코드와 같습니다.
private fun getMarsPhotos() {
   viewModelScope.launch {
       marsUiState = try {
           val listResult = MarsApi.retrofitService.getPhotos()
           MarsUiState.Success(listResult)
       } catch (e: IOException) {
           MarsUiState.Error
       }
   }
}
  1. screens/HomeScreen.kt 파일에서 marsUiState에 관한 when 표현식을 추가합니다. marsUiStateMarsUiState.Success이면 ResultScreen을 호출하고 marsUiState.photos를 전달합니다. 지금은 이 오류를 무시합니다.
import androidx.compose.foundation.layout.fillMaxWidth

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )
    }
}
  1. when 블록 내에 MarsUiState.LoadingMarsUiState.Error 검사를 추가합니다. 앱에 LoadingScreen, ResultScreen, ErrorScreen 컴포저블을 표시하도록 합니다. 이 컴포저블은 나중에 구현합니다.
import androidx.compose.foundation.layout.fillMaxSize

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )

        is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
    }
}
  1. res/drawable/loading_animation.xml을 엽니다. 이 드로어블은 이미지 드로어블 loading_img.xml을 중심점을 축으로 회전시키는 애니메이션입니다. (이 애니메이션은 미리보기에 표시되지 않습니다.)

92a448fa23b6d1df.png

  1. screens/HomeScreen.kt 파일의 HomeScreen 컴포저블 아래에 다음의 구성 가능한 함수 LoadingScreen을 추가하여 로드 중 애니메이션을 표시합니다. loading_img 드로어블 리소스는 시작 코드에 포함됩니다.
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image

@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
    Image(
        modifier = modifier.size(200.dp),
        painter = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.loading)
    )
}
  1. LoadingScreen 컴포저블 아래에 다음의 구성 가능한 함수 ErrorScreen을 추가하여 앱이 오류 메시지를 표시할 수 있도록 합니다.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding

@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
        )
        Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
    }
}
  1. 비행기 모드를 사용 설정한 채로 앱을 다시 실행합니다. 이번에는 앱이 갑자기 닫히지 않고 다음과 같은 오류 메시지가 표시됩니다.

28ba37928e0a9334.png

  1. 휴대전화 또는 에뮬레이터에서 비행기 모드를 사용 중지합니다. 앱을 실행하고 테스트하면 모든 것이 제대로 작동하는 것을 확인할 수 있고 JSON 문자열을 볼 수 있습니다.

8. kotlinx.serialization을 사용하여 JSON 응답 파싱하기

JSON

요청된 데이터는 일반적으로 XML 또는 JSON과 같은 일반적인 데이터 형식 중 하나로 지정됩니다. 호출할 때마다 구조화된 데이터가 반환되며, 앱은 이 구조에 관해 알아야 응답에서 데이터를 읽을 수 있습니다.

예를 들어 이 앱은 https://android-kotlin-fun-mars-server.appspot.com/photos 서버에서 데이터를 가져오고 있습니다. 이 URL을 브라우저에 입력하면 ID 목록과 화성 표면 이미지 URL이 JSON 형식으로 표시됩니다.

샘플 JSON 응답 구조

키 값과 JSON 객체 표시

JSON 응답의 구조에는 다음과 같은 특성이 있습니다.

  • JSON 응답은 대괄호로 표시된 배열입니다. 이 배열에는 JSON 객체가 포함됩니다.
  • JSON 객체는 중괄호로 묶여 있습니다.
  • 각 JSON 객체에는 쉼표로 구분된 키-값 쌍 집합이 포함됩니다.
  • 하나의 쌍에서 키와 값은 콜론으로 구분합니다.
  • 이름은 따옴표로 묶여 있습니다.
  • 값은 숫자, 문자열, 불리언, 배열, 객체(JSON 객체) 또는 null일 수 있습니다.

예를 들어 img_src는 문자열인 URL입니다. 웹브라우저에 URL을 붙여넣으면 화성 표면 이미지가 표시됩니다.

b4f9f196c64f02c3.png

이제 앱에서 화성 웹 서비스의 JSON 응답을 받게 되며, 이는 훌륭한 출발입니다. 그러나, 이미지를 실제로 표시하기 위해 필요한 것은 Kotlin 객체이지 긴 JSON 문자열이 아닙니다. 이 프로세스를 역직렬화라고 합니다.

직렬화는 애플리케이션에서 사용하는 데이터를 네트워크를 통해 전송할 수 있는 형식으로 변환하는 프로세스입니다. 직렬화와 반대로 역직렬화는 외부 소스(예: 서버)에서 데이터를 읽어 런타임 객체로 변환하는 프로세스입니다. 둘 다 네트워크를 통해 데이터를 교환하는 대부분의 애플리케이션에서 필수 구성요소입니다.

kotlinx.serialization은 JSON 문자열을 Kotlin 객체로 변환하는 일련의 라이브러리를 제공합니다. Retrofit과 호환되도록 커뮤니티에서 개발한 서드 파티 라이브러리로 Kotlin 직렬화 변환기가 있습니다.

이 작업에서는 kotlinx.serialization 라이브러리를 사용하여 웹 서비스에서 받은 JSON 응답을 화성 사진을 나타내는 유용한 Kotlin 객체로 파싱합니다. 앱이 원시 JSON을 표시하는 대신 반환되는 화성 사진의 개수를 표시하도록 앱을 변경합니다.

kotlinx.serialization 라이브러리 종속 항목 추가하기

  1. build.gradle.kts (Module :app)를 엽니다.
  2. plugins 블록에 kotlinx serialization 플러그인을 추가합니다.
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
  1. kotlinx.serialization 종속 항목을 포함하도록 dependencies 섹션에 다음 코드를 추가합니다. 이 종속 항목은 Kotlin 프로젝트에 JSON 직렬화를 제공합니다.
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
  1. dependencies 블록에서 Retrofit 스칼라 변환기를 나타내는 줄을 찾아 kotlinx-serialization-converter를 사용하도록 변경합니다.

다음 코드를

// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

아래와 같이 바꿉니다.

// Retrofit with Kotlin serialization Converter

implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
  1. Sync Now를 클릭하여 새 종속 항목으로 프로젝트를 다시 빌드합니다.

화성 사진 데이터 클래스 구현하기

웹 서비스에서 가져오는 JSON 응답의 샘플 항목은 다음과 같으며, 앞서 본 것과 유사합니다.

[
    {
        "id":"424906",
        "img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
    },
...]

위의 예에서 각 화성 사진 항목에는 다음과 같은 JSON 키와 값 쌍이 있습니다.

  • id: 문자열로 된 속성 ID입니다. 따옴표(" ")로 래핑되므로 Integer가 아닌 String 유형입니다.
  • img_src: 문자열로 된 이미지 URL입니다.

kotlinx.serialization은 이 JSON 데이터를 파싱하여 Kotlin 객체로 변환합니다. 이렇게 하려면 kotlinx.serialization에 파싱된 결과를 저장할 Kotlin 데이터 클래스가 있어야 합니다. 이 단계에서는 MarsPhoto 데이터 클래스를 만듭니다.

  1. network 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin File/Class를 선택합니다.
  2. 대화상자에서 Class를 선택하고 클래스 이름으로 MarsPhoto를 입력합니다. 이 작업으로 network 패키지에 MarsPhoto.kt라는 새 파일이 생성됩니다.
  3. 클래스 정의 앞에 data 키워드를 추가하여 MarsPhoto를 데이터 클래스로 만듭니다.
  4. 중괄호 {}를 괄호 ()로 변경합니다. 데이터 클래스에는 하나 이상의 속성이 정의되어 있어야 하므로 이 변경으로 인해 오류가 발생합니다.
data class MarsPhoto()
  1. 다음 속성을 MarsPhoto 클래스 정의에 추가합니다.
data class MarsPhoto(
    val id: String,  val img_src: String
)
  1. MarsPhoto 클래스를 직렬화할 수 있도록 @Serializable로 주석을 지정합니다.
import kotlinx.serialization.Serializable

@Serializable
data class MarsPhoto(
    val id: String,  val img_src: String
)

MarsPhoto 클래스의 각 변수는 JSON 객체의 키 이름에 대응합니다. 특정 JSON 응답의 유형과 일치하도록 하려면 모든 값에 String 객체를 사용합니다.

kotlinx serialization은 JSON을 파싱할 때 이름과 일치하는 키를 찾아 데이터 객체를 적절한 값으로 채웁니다.

@SerialName 주석

JSON 응답의 키 이름이 Kotlin 속성을 혼란스럽게 만들거나 권장 코딩 스타일과 일치하지 않을 수 있습니다. 예를 들어 JSON 파일에서 img_src 키는 밑줄을 사용하지만 속성의 Kotlin 규칙은 대문자와 소문자(카멜 표기법)를 사용합니다.

데이터 클래스에 JSON 응답의 키 이름과 다른 변수 이름을 사용하려면 @SerialName 주석을 사용합니다. 다음 예에서 데이터 클래스의 변수 이름은 imgSrc입니다. @SerialName(value = "img_src")를 사용하여 변수를 JSON 속성 img_src에 매핑할 수 있습니다.

  1. img_src 키에 관한 줄을 아래에 나온 줄로 바꿉니다.
import kotlinx.serialization.SerialName

@SerialName(value = "img_src")
val imgSrc: String

MarsApiService 및 MarsViewModel 업데이트하기

이 작업에서는 kotlinx.serialization 변환기를 사용하여 JSON 객체를 Kotlin 객체로 변환합니다.

  1. network/MarsApiService.kt를 엽니다.
  2. ScalarsConverterFactory에 미해결 참조 오류가 있음을 알 수 있습니다. 이 오류는 이전 섹션에서 Retrofit의 종속 항목을 변경하여 발생했습니다.
  3. ScalarConverterFactory의 import를 삭제합니다. 다른 오류는 나중에 수정합니다.

삭제:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. retrofit 객체 선언에서 ScalarConverterFactory 대신 kotlinx.serialization을 사용하도록 Retrofit 빌더를 변경합니다.
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType

private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

이제 kotlinx.serialization을 가져왔으므로 JSON 문자열을 반환하는 대신 JSON 배열에서 MarsPhoto 객체 목록을 반환하도록 Retrofit에 요청할 수 있습니다.

  1. String을 반환하는 대신 MarsPhoto 객체 목록을 반환하도록 Retrofit의 MarsApiService 인터페이스를 업데이트합니다.
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}
  1. viewModel을 비슷하게 변경합니다. MarsViewModel.kt를 열고 getMarsPhotos() 메서드를 찾아 아래로 스크롤합니다.

getMarsPhotos() 메서드에서 listResult는 더 이상 String이 아니라 List<MarsPhoto>입니다. 이 목록의 크기는 수신되어 파싱된 사진의 개수입니다.

  1. 가져온 사진 수를 출력하려면 marsUiState를 다음과 같이 업데이트합니다.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. 기기 또는 에뮬레이터에서 비행기 모드가 사용 중지되어 있는지 확인합니다. 앱을 컴파일하고 실행합니다.

이번에는 메시지에 긴 JSON 문자열이 아닌 웹 서비스에서 반환된 속성 개수가 표시됩니다.

a59e55909b6e9213.png

9. 솔루션 코드

완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

이 Codelab의 솔루션 코드는 GitHub에서 확인하세요.

10. 요약

REST 웹 서비스

  • 웹 서비스는 인터넷을 통해 제공되는 소프트웨어 기반 기능으로, 앱은 이 기능을 사용하여 요청을 실행하고 데이터를 다시 가져올 수 있습니다.
  • 일반적인 웹 서비스는 REST 아키텍처를 사용합니다. REST 아키텍처를 제공하는 웹 서비스를 RESTful 서비스라고 합니다. RESTful 웹 서비스는 표준 웹 구성요소 및 프로토콜을 사용하여 빌드됩니다.
  • URI를 통해 표준화된 방법으로 REST 웹 서비스에 요청을 전송합니다.
  • 웹 서비스를 사용하려면 앱은 네트워크 연결을 설정하고 서비스와 통신해야 합니다. 그런 다음 앱은 사용할 수 있는 형식으로 응답 데이터를 수신하고 파싱해야 합니다.
  • Retrofit 라이브러리는 앱의 REST 웹 서비스 요청을 지원하는 클라이언트 라이브러리입니다.
  • 변환기를 사용하여 웹 서비스에 전송하는 데이터와 웹 서비스에서 가져오는 데이터로 해야 할 일을 Retrofit에 알립니다. 예를 들어, ScalarsConverter는 웹 서비스 데이터를 String 또는 기타 프리미티브로 취급합니다.
  • 앱이 인터넷에 연결할 수 있게 하려면 Android 매니페스트에 "android.permission.INTERNET" 권한을 추가합니다.
  • 지연 초기화는 객체가 처음 사용되는 시점으로 객체 생성을 위임합니다. 객체가 아닌 참조를 생성합니다. 처음 객체를 액세스할 때 참조가 생성되고 그 후 액세스할 때마다 사용됩니다.

JSON 파싱

  • 웹 서비스 응답의 형식은 주로 구조화된 데이터를 나타내는 일반적인 형식인 JSON으로 지정됩니다.
  • JSON 객체는 키-값 쌍 모음입니다.
  • JSON 객체 모음은 JSON 배열입니다. 웹 서비스의 응답으로 JSON 배열을 받게 됩니다.
  • 키-값 쌍의 키는 따옴표로 묶입니다. 이 값은 숫자이거나 문자열일 수 있습니다.
  • Kotlin에서는 데이터 직렬화 도구를 별도의 구성요소인 kotlinx.serialization에서 사용할 수 있습니다. kotlinx.serialization은 JSON 문자열을 Kotlin 객체로 변환하는 일련의 라이브러리를 제공합니다.
  • Retrofit용으로 커뮤니티에서 개발한 Kotlin 직렬화 변환기 라이브러리(retrofit2-kotlinx-serialization-converter)가 있습니다.
  • kotlinx.serialization은 JSON 응답의 키를 이름이 같은 데이터 객체의 속성과 일치시킵니다.
  • 키에 다른 속성 이름을 사용하려면 속성에 @SerialName 주석과 JSON 키 value로 주석을 지정합니다.

11. 자세히 알아보기

Android 개발자 문서:

Kotlin 문서:

기타: