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

시중에 나와 있는 대부분의 Android 앱은 인터넷에 연결하여 일부 네트워크 작업을 실행합니다. 예를 들어 이메일이나 메시지 또는 이와 유사한 정보는 백엔드 서버에서 검색하고 Gmail, YouTube, Google 포토와 같은 앱은 인터넷에 연결하여 사용자 데이터를 표시합니다.

이 Codelab에서는 오픈소스로 개발된 라이브러리를 사용하여 네트워크 계층을 빌드하고 백엔드 서버에서 데이터를 가져옵니다. 이렇게 하면 데이터 가져오기가 크게 단순해지고 백그라운드 스레드에서 작업을 실행하는 등의 Android 권장사항을 준수하는 앱을 빌드할 수 있습니다. 또한 인터넷 속도가 느리거나 인터넷을 사용할 수 없는 경우 앱의 사용자 인터페이스를 업데이트합니다. 그러면 사용자가 네트워크 연결 문제에 관해 알 수 있습니다.

기본 요건

  • 프래그먼트를 만들고 사용하는 방법
  • Android 아키텍처 구성요소 ViewModelLiveData를 사용하는 방법
  • Gradle 파일에 종속 항목을 추가하는 방법

학습할 내용

  • REST 웹 서비스의 정의
  • Retrofit 라이브러리를 사용하여 인터넷에서 REST 웹 서비스에 연결하고 응답 받기
  • Moshi 라이브러리를 사용하여 JSON 응답을 데이터 객체로 파싱하기

실행할 작업

  • 웹 서비스 API 요청을 실행하고 응답을 처리하도록 스타터 앱을 수정합니다.
  • Retrofit 라이브러리를 사용하여 앱의 네트워크 계층을 구현합니다.
  • Moshi 라이브러리를 사용하여 웹 서비스의 JSON 응답을 앱의 LiveData 객체로 파싱합니다.
  • Retrofit의 코루틴 지원을 사용하여 코드를 단순화합니다.

필요한 항목

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

이 과정에서는 화성 표면의 이미지를 보여 주는 MarsPhotos라는 스타터 앱을 사용합니다. 이 앱은 웹 서비스에 연결하여 화성 사진을 검색하고 표시합니다. 이미지는 NASA의 화성 탐사 로봇이 화성에서 촬영한 실제 사진입니다. 다음은 RecyclerView로 빌드된 썸네일 속성 이미지 그리드가 있는 최종 앱의 스크린샷입니다.

6c26142b52c51285.png

이 Codelab에서 빌드하는 버전의 앱에는 시각적 플래시가 많지 않습니다. 인터넷에 연결하고 웹 서비스를 사용해 원시 속성 데이터를 다운로드하기 위해 앱의 네트워킹 계층 부분에 중점을 둡니다. 데이터가 올바르게 검색되고 파싱되도록 하기 위해 백엔드 서버에서 받은 수의 사진만 텍스트 뷰에 출력합니다.

e98a0641540fcb73.png

시작 코드 다운로드하기

이 Codelab은 시작 코드를 제공합니다. 이 Codelab에서 학습한 기능을 사용하여 시작 코드를 확장할 수 있습니다. 시작 코드에는 이전의 Codelab을 통해 익숙한 코드와 그렇지 않은 코드가 모두 있을 수 있습니다. 익숙하지 않은 코드에 관해서는 이후 Codelab에서 자세히 알아봅니다.

GitHub의 시작 코드를 사용하는 경우 폴더 이름은 android-basics-kotlin-mars-photos-app입니다. Android 스튜디오에서 프로젝트를 열 때 이 폴더를 선택하세요.

이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.

코드 가져오기

  1. 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
  2. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
  5. Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.

시작 코드 실행하기

  1. Android 스튜디오에서 다운로드한 프로젝트를 엽니다. 프로젝트의 폴더 이름은 android-basics-kotlin-mars-photos-app입니다. 시작 코드의 폴더 구조는 다음과 같습니다.
  2. Android 창에서 app > java를 펼칩니다. 앱에 overview라는 패키지 폴더가 있습니다. 앱의 UI 레이어입니다.

39027f91862361f1.png

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

4886b6b36023a53f.png

  1. 파일을 둘러보면서 시작 코드를 이해합니다. 레이아웃 파일의 경우 오른쪽 상단에 있는 Split 옵션을 사용하여 레이아웃과 XML의 미리보기를 동시에 확인할 수 있습니다.

시작 코드 둘러보기

이 작업에서는 프로젝트 구조를 숙지합니다. 다음은 프로젝트의 중요한 파일과 폴더를 설명합니다.

OverviewFragment:

  • MainActivity 내에 표시되는 프래그먼트입니다. 이전 단계에서 확인한 자리표시자 텍스트가 이 프래그먼트에 표시됩니다.
  • 다음 Codelab에서 이 프래그먼트는 화성 사진 백엔드 서버에서 받은 데이터를 표시합니다.
  • 이 클래스는 OverviewViewModel 객체 참조를 보유합니다.
  • OverviewFragment에는 데이터 결합을 사용하여 fragment_overview 레이아웃을 확장하고 결합 수명 주기 소유자를 자체로 설정하고 결합 객체의 viewModel 변수를 자체로 설정하는 onCreateView() 함수가 있습니다.
  • 수명 주기 소유자가 할당되므로 데이터 결합에 사용된 모든 LiveData의 변경이 자동으로 관찰되며, 변경사항에 따라 UI가 업데이트됩니다.

OverviewViewModel:

  • OverviewFragment에 상응하는 뷰 모델입니다.
  • 이 클래스에는 지원 속성과 함께 _status라는 MutableLiveData 속성이 있습니다. 이 속성의 값을 업데이트하면 화면에 표시되는 자리표시자 텍스트가 업데이트됩니다.
  • getMarsPhotos() 메서드는 자리표시자 응답을 업데이트합니다. Codelab 후반에 이 메서드를 사용하여 서버에서 가져온 데이터를 표시합니다. 이 Codelab의 목표는 인터넷에서 가져오는 실제 데이터를 사용하여 ViewModel 내에서 status LiveData를 업데이트하는 것입니다.

res/layout/fragment_overview.xml:

  • 이 레이아웃은 데이터 결합을 사용하도록 설정되며 단일 TextView로 구성됩니다.
  • OverviewViewModel 변수를 선언한 다음 ViewModelstatusTextView를 결합합니다.

MainActivity.kt: 이 활동의 작업은 활동의 레이아웃인 activity_main을 로드하는 것뿐입니다.

layout/activity_main.xml: fragment_overview를 가리키는 단일 FragmentContainerView가 포함된 기본 활동 레이아웃입니다. 이 개요 프래그먼트는 앱이 실행될 때 인스턴스화됩니다.

이 Codelab에서는 백엔드 서버와 통신하는 네트워크 서비스의 레이어를 만들고 필요한 데이터를 가져옵니다. 이러한 구현을 위해 Retrofit이라는 타사 라이브러리를 사용합니다. 이 내용은 나중에 자세히 알아봅니다. ViewModel은 이 네트워크 계층과 직접 통신하며, 앱의 나머지 부분은 이 구현에 표시되지 않습니다.

d5a05ab8fd5ff011.png

OverviewViewModel은 네트워크를 호출하여 화성 사진 데이터를 가져옵니다. ViewModel에서 수명 주기 인식 데이터 결합과 함께 LiveData를 사용하여 데이터 변경 시 앱 UI를 업데이트합니다.

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

b3ab0ee52bfd791e.png

4a23a1ba3307b2a5.png

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

표준화된 방법으로 URI를 통해 RESTful 웹 서비스에 요청이 전송됩니다. URI(Uniform Resource Identifier)는 리소스 위치나 리소스에 액세스하는 방법을 암시하지 않고 서버의 리소스를 이름으로 식별합니다. 예를 들어 이 과정의 앱에서는 다음 서버 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)를 통해 네트워크에서 가져올 수 있으며 /realestate 또는 /photos 등으로 식별되는 리소스를 참조합니다. 이 Codelab에서는 /photos 엔드포인트를 사용합니다.

웹 서비스 요청

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

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

  • 서버 데이터를 검색하는 GET
  • 서버에 새로운 데이터를 추가/생성/업데이트하는 POST 또는 PUT
  • 서버에서 데이터를 삭제하는 DELETE

앱이 화성 사진 정보를 가져오려는 HTTP GET 요청을 서버에 보내고, 그런 다음 서버는 이미지 URL을 포함한 응답을 앱에 반환합니다.

9fb57e255df97a4d.png

6da405d572445df9.png

웹 서비스의 응답은 일반적으로 키-값 쌍으로 구조화된 데이터를 나타내는 XML 또는 JSON 등의 일반적인 웹 형식 중 하나로 지정됩니다. JSON에 관해서는 이후 작업에서 자세히 알아봅니다.

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

외부 라이브러리

외부 라이브러리 또는 타사 라이브러리는 핵심 Android API의 확장 프로그램과 같습니다. 대체로 커뮤니티에 의해 개발된 오픈소스로, 전 세계적으로 대규모 Android 커뮤니티의 집단적 참여에 의해 유지관리됩니다. 이를 토대로 Android 개발자는 더 나은 앱을 빌드할 수 있습니다.

Retrofit 라이브러리

이 Codelab에서 RESTful Mars 웹 서비스와 통신하기 위해 사용할 Retrofit 라이브러리는 잘 지원되고 유지관리되는 라이브러리의 좋은 예입니다. GitHub 페이지에서 미해결된 문제(일부는 기능 요청임)와 종료된 문제를 확인하여 판단할 수 있습니다. 개발자가 정기적으로 문제를 해결하고 기능 요청에 응답한다면 이 라이브러리는 잘 유지관리되고 있으며 앱에서 사용하기 적합한 후보라고 생각할 수 있습니다. Retrofit 문서 페이지도 참고할 수 있습니다.

Retrofit 라이브러리는 백엔드와 통신합니다. 이 라이브러리는 웹 서비스에 전달하는 매개변수를 기반으로 웹 서비스의 URI를 만듭니다. 이 내용은 이후 섹션에서 자세히 알아봅니다.

c9e1034e86327abd.png

Retrofit 종속 항목 추가하기

Android Gradle을 사용하면 외부 라이브러리를 프로젝트에 추가할 수 있습니다. 라이브러리 종속 항목 외에도 라이브러리가 호스팅되는 저장소도 포함해야 합니다. Jetpack 라이브러리의 ViewModelLiveData와 같은 Google 라이브러리는 Google 저장소에 호스팅됩니다. Retrofit과 같은 대부분의 커뮤니티 라이브러리는 JCenter에서 호스팅됩니다.

  1. 프로젝트의 최상위 수준 build.gradle(Project: MarsPhotos) 파일을 엽니다. repositories 블록 아래에 저장소가 나열됩니다. 두 저장소 google(), jcenter()가 표시됩니다.
repositories {
   google()
   jcenter()
}
  1. 모듈 수준 Gradle 파일 build.gradle (Module: MarsPhots.app)을 엽니다.
  2. dependencies 섹션에서 다음과 같은 Retrofit 라이브러리 줄을 추가합니다.
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

첫 번째 종속 항목은 Retrofit2 라이브러리 자체와 관련되며, 두 번째 종속 항목은 Retrofit 스칼라 변환기와 관련됩니다. 이 변환기를 사용하면 Retrofit에서 JSON 결과를 String으로 반환할 수 있습니다. 두 라이브러리는 함께 작동합니다.

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

자바 8 언어 기능 지원 추가하기

Retrofit2를 비롯한 많은 타사 라이브러리는 자바 8 언어 기능을 사용합니다. Android Gradle 플러그인에는 특정 자바 8 언어 기능을 위한 지원이 내장되어 있습니다.

  1. 이 내장 기능을 사용하려면 모듈의 build.gradle 파일에 다음 코드가 필요합니다. 이 단계는 이미 완료되어 있습니다. build.gradle(Module: MarsPhotos.app).에 다음 코드가 있는지 확인하세요.
android {
  ...

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = '1.8'
  }
}

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

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

64fe0b5717a31f71.png

이 작업에서는 ViewModel이 웹 서비스와 통신하는 데 사용할 네트워크 계층을 MarsPhotos 프로젝트에 추가합니다. 다음 단계에 따라 Retrofit 서비스 API를 구현합니다.

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

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

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

메시지가 표시되면 retrofit2.Retrofit을 가져옵니다.

  1. Retrofit에서는 웹 서비스의 기본 URI 및 변환기 팩토리가 있어야 웹 서비스 API를 빌드할 수 있습니다. 변환기는 웹 서비스에서 얻은 데이터로 해야 할 일을 Retrofit에 알립니다. 이 경우에는 Retrofit에서 웹 서비스의 JSON 응답을 가져와 String으로 반환하려고 합니다. Retrofit에는 문자열 및 기타 프리미티브 유형을 지원하는 ScalarsConverter가 있으므로 ScalarsConverterFactory의 인스턴스를 사용하여 빌더에서 addConverterFactory()를 호출합니다.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())

메시지가 표시되면 retrofit2.converter.scalars.ScalarsConverterFactory를 가져옵니다.

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

요청이 있는 경우 retrofit2.http.GET을 가져옵니다.

  1. getPhotos() 메서드가 호출되면 Retrofit은 요청을 시작하는 데 사용된 기본 URL(Retrofit 빌더에서 정의함)에 엔드포인트 photos를 추가합니다. 함수의 반환 유형을 String에 추가합니다.
interface MarsApiService {
    @GET("photos")
    fun getPhotos(): String
}

객체 선언

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

Kotlin을 사용하면 싱글톤을 쉽게 선언할 수 있습니다. 다음은 객체 선언 및 액세스의 예입니다. 객체 선언에는 항상 object 키워드 뒤에 이름이 있습니다.

예:

// Object declaration
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }
​
    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)

Retrofit 객체에서 create() 함수를 호출하는 데는 리소스가 많이 들고, 앱에는 Retrofit API 서비스의 인스턴스가 하나만 필요합니다. 따라서 객체 선언을 사용하여 나머지 앱의 나머지 부분에 서비스를 노출합니다.

  1. MarsApiService 인터페이스 선언 외부에서 MarsApi라는 공개 객체를 정의하여 Retrofit 서비스를 초기화합니다. 이 객체는 앱의 나머지 부분에서 액세스할 수 있는 공개 싱글톤 객체입니다.
object MarsApi {

}
  1. MarsApi 객체 선언 내부에서 MarsApiService 유형의 지연 초기화 Retrofit 객체 속성 retrofitService를 추가합니다. 최초 사용 시 초기화되도록 하기 위해 이러한 지연 초기화를 사용합니다. 다음 단계에서 오류를 수정합니다.
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 객체를 사용합니다.

OverviewViewModel에서 웹 서비스 호출하기

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

ViewModelScope

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

ViewModelScope를 사용하여 코루틴을 실행하고 백그라운드에서 Retrofit 네트워크 호출을 실행합니다.

  1. MarsApiService에서 getPhotos()를 정지 함수로 만듭니다. 그러면 코루틴 내에서 이 메서드를 호출할 수 있습니다.
@GET("photos")
suspend fun getPhotos(): String
  1. overview/OverviewViewModel을 엽니다. 아래로 스크롤하여 getMarsPhotos() 메서드를 찾습니다. 상태 응답을 "Set the Mars API Response here!".로 설정하는 줄을 삭제합니다. 이제 getMarsPhotos() 메서드가 비어 있어야 합니다.
private fun getMarsPhotos() {

}
  1. getMarsPhotos() 내부에서 viewModelScope.launch를 사용하여 코루틴을 실행합니다.
private fun getMarsPhotos() {
    viewModelScope.launch {
    }
}

메시지가 표시되면 androidx.lifecycle.viewModelScopekotlinx.coroutines.launch를 가져옵니다.

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

메시지가 표시되면 com.example.android.marsphotos.network.MarsApi를 가져옵니다.

  1. 백엔드 서버에서 받은 결과를 _status.value.에 할당합니다.
 val listResult = MarsApi.retrofitService.getProperties()
 _status.value = 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 권한이 없을 수도 있음을 나타냅니다. 이 문제는 다음 작업에서 앱에 인터넷 권한을 추가하여 해결하겠습니다.

Android 권한

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

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

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

  1. manifests/AndroidManifest.xml을 엽니다. <application> 태그 바로 앞에 다음 줄을 추가합니다.
<uses-permission android:name="android.permission.INTERNET" />
  1. 앱을 컴파일하고 다시 실행합니다. 인터넷이 연결되어 있으면 화성 사진과 관련된 데이터가 포함된 JSON 텍스트가 표시됩니다. JSON 형식에 관해서는 Codelab 후반에서 자세히 알아봅니다.

f7ba3feaf864d4cf.png

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

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

예외 처리

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

예외가 발생하는 원인은 0으로 나누기 또는 네트워크 오류와 같이 단순한 것일 수 있습니다. 이러한 예외는 이전 Codelab에서 배운 NumberFormatException과 유사합니다.

서버에 연결할 때 발생할 수 있는 문제는 예를 들어 다음과 같습니다.

  • 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. overview/OverviewViewModel.kt를 엽니다. 아래로 스크롤하여 getMarsPhotos() 메서드를 찾습니다. launch 블록 내부에서 MarsApi 호출 주위에 예외 처리용 try 블록을 추가합니다. try 블록 뒤에 catch 블록을 추가합니다.
viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       _status.value = listResult
   } catch (e: Exception) {

   }
}
  1. catch {} 블록 내부에서 실패 응답을 처리합니다. e.message_status.value로 설정하여 사용자에게 오류 메시지를 표시합니다.
catch (e: Exception) {
   _status.value = "Failure: ${e.message}"
}
  1. 비행기 모드를 사용 설정한 채로 앱을 다시 실행합니다. 이번에는 앱이 갑자기 닫히지는 않지만 대신 오류 메시지가 표시됩니다.

2fbc318b4fff2f34.png

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

JSON

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

예를 들어 이 앱에서는 https:// android-kotlin-fun-mars-server.appspot.com/photos 서버에서 데이터를 검색합니다. 브라우저에 이 URL을 입력하면

화상 표면의 이미지 URL과 ID의 목록이 JSON 형식으로 표시됩니다.

샘플 JSON 응답의 구조:

68fdfa54410ee03e.png

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

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

17116bdeb21fec0d.png

이제 Mars 웹 서비스에서 JSON 응답을 받게 되며, 이것은 훌륭한 출발입니다. 그러나 정말 필요한 것은 큰 JSON 문자열이 아닌 Kotlin 객체입니다. Moshi라는 외부 라이브러리가 있습니다. 이 라이브러리는 JSON 문자열을 Kotlin 객체로 변환하는 Android JSON 파서입니다. Retrofit은 Moshi와 연동되는 변환기가 있어서 여기서의 목적에 적합한 라이브러리입니다.

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

Moshi 라이브러리 종속 항목 추가하기

  1. build.gradle (Module: app)을 엽니다.
  2. 종속 항목 섹션에서 아래와 같은 코드를 추가하여 Moshi 종속 항목을 포함합니다. 이 종속 항목은 Kotlin 지원과 함께 Moshi JSON 라이브러리 지원을 추가합니다.
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
  1. dependencies 블록에서 Retrofit 스칼라 변환기 줄을 찾아 converter-moshi를 사용하도록 다음 종속 항목을 변경합니다.

다음 내용을

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

다음으로 바꿉니다.

// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.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로, 문자열입니다.

Moshi는 이 JSON 데이터를 파싱하여 Kotlin 객체로 변환합니다. 이를 위해 Moshi는 Kotlin 데이터 클래스가 있어야 파싱된 결과를 저장할 수 있으므로, 이 단계에서는 데이터 클래스 MarsPhoto를 만듭니다.

  1. network 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin File/Class를 선택합니다.
  2. 팝업에서 Class를 선택하고 클래스 이름으로 MarsPhoto를 입력합니다. 그러면 network 패키지에 MarsPhoto.kt라는 새 파일이 생성됩니다.
  3. 클래스 정의 앞에 data 키워드를 추가하여 MarsPhoto를 데이터 클래스로 만듭니다. {} 중괄호를 () 괄호로 변경합니다. 그러면 오류가 발생합니다. 왜냐하면 데이터 클래스에 속성이 하나 이상 정의되어 있어야 하기 때문입니다.
data class MarsPhoto(
)
  1. 다음 속성을 MarsPhoto 클래스 정의에 추가합니다.
data class MarsPhoto(
   val id: String, val img_src: String
)

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

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

@Json 주석

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

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

  1. img_src 키의 줄을 아래에 나온 줄로 바꿉니다. 요청이 있는 경우 com.squareup.moshi.Json을 가져옵니다.
@Json(name = "img_src") val imgSrcUrl: String

MarsApiService 및 OverviewViewModel 업데이트하기

이 작업에서는 Retrofit 빌더와 마찬가지로 Moshi 빌더를 사용하여 Moshi 객체를 만듭니다.

ScalarsConverterFactoryKotlinJsonAdapterFactory로 바꿔 Moshi를 사용해 JSON 응답을 Kotlin 객체로 변환하도록 Retrofit에 알립니다. 그런 다음 Moshi 객체를 사용하도록 네트워크 API와 ViewModel을 업데이트합니다.

  1. network/MarsApiService.kt를 엽니다. ScalarsConverterFactory의 미해결 참조 오류를 확인할 수 있습니다. 이전 단계에서 Retrofit 종속 항목을 변경했기 때문에 발생한 것입니다. ScalarConverterFactory의 가져오기를 삭제하세요. 다른 오류는 곧 수정할 예정입니다.

다음을 삭제합니다.

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 파일 상단에서 Retrofit 빌더 바로 앞에 다음 코드를 추가하여 Retrofit 객체와 비슷한 Moshi 객체를 만듭니다.
private val moshi = Moshi.Builder()

요청이 있는 경우 com.squareup.moshi.Moshicom.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory를 가져옵니다.

  1. Moshi의 주석이 Kotlin과 원활하게 작동하려면 Moshi 빌더에서 KotlinJsonAdapterFactory를 추가한 다음 build()를 호출합니다.
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()
  1. retrofit 객체 선언에서 Retrofit 빌더에 ScalarConverterFactory 대신 MoshiConverterFactory가 사용되도록 변경하고 방금 만든 moshi 인스턴스를 전달합니다.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

요청이 있는 경우 retrofit2.converter.moshi.MoshiConverterFactory를 가져옵니다.

  1. 이제 MoshiConverterFactory를 가져왔으므로 JSON 문자열을 반환하는 대신 JSON 배열에서 MarsPhoto 객체 목록을 반환하도록 Retrofit에 요청할 수 있습니다. Retrofit이 String이 아닌 MarsPhoto 객체 목록을 반환하도록 MarsApiService 인터페이스를 업데이트합니다.
interface MarsApiService {
   @GET("photo")
   fun getPhotos(): List<MarsPhoto>
}
  1. viewModel을 비슷하게 변경하고 OverviewViewModel.kt를 엽니다. 아래로 스크롤하여 getMarsPhotos() 메서드를 찾습니다.
  2. getMarsPhotos() 메서드에서 listResult는 더 이상 String이 아닌 List<MarsPhoto>입니다. 이 목록의 크기는 수신 및 파싱된 사진의 수입니다. 검색된 사진 수를 출력하려면 _status.value를 다음과 같이 업데이트합니다.
_status.value = "Success: ${listResult.size} Mars photos retrieved"

메시지가 표시되면 com.example.android.marsphotos.network.MarsPhoto를 가져옵니다.

  1. 기기 또는 에뮬레이터에서 비행기 모드가 사용 중지되어 있는지 확인합니다. 앱을 컴파일하고 실행합니다. 이번에는 다음과 같이 큰 JSON 문자열이 아닌 웹 서비스에서 반환된 속성의 수가 표시됩니다.

7da53c64bd36fe74.png

build.gradle(Module : MarsPhotos.app)

다음은 포함될 새 종속 항목입니다.

dependencies {
    ...
    // Moshi
    implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'

    // Retrofit with Moshi Converter
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    ...
}

Manifests/AndroidManifest.xml

아래 코드 스니펫에서 인터넷 권한인 <uses-permission..> 코드를 추가합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.marsphotos">

    <!-- In order for our app to access the Internet, we need to define this permission. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
    </application>

</manifest>

network/MarsPhoto.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Json

/**
* This data class defines a Mars photo which includes an ID, and the image URL.
* The property names of this data class are used by Moshi to match the names of values in JSON.
*/
data class MarsPhoto(
   val id: String,
   @Json(name = "img_src") val imgSrcUrl: String
)

network/MarsApiService.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET

private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"

/**
* Build the Moshi object with Kotlin adapter factory that Retrofit will be using.
*/
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

/**
* The Retrofit object with the Moshi converter.
*/
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

/**
* A public interface that exposes the [getPhotos] method
*/
interface MarsApiService {
   /**
    * Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
    * The @GET annotation indicates that the "photos" endpoint will be requested with the GET
    * HTTP method
    */
   @GET("photos")
   suspend fun getPhotos() : List<MarsPhoto>
}

/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object MarsApi {
   val retrofitService: MarsApiService by lazy { retrofit.create(MarsApiService::class.java) }
}

Overview/OverviewViewModel.kt

package com.example.android.marsphotos.overview

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch

/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {

   // The internal MutableLiveData that stores the status of the most recent request
   private val _status = MutableLiveData<String>()

   // The external immutable LiveData for the request status
   val status: LiveData<String> = _status
   /**
    * Call getMarsPhotos() on init so we can display status immediately.
    */
   init {
       getMarsPhotos()
   }

   /**
    * Gets Mars photos information from the Mars API Retrofit service and updates the
    * [MarsPhoto] [List] [LiveData].
    */
   private fun getMarsPhotos() {
       viewModelScope.launch {
           try {
               val listResult = MarsApi.retrofitService.getPhotos()
               _status.value = "Success: ${listResult.size} Mars photos retrieved"
           } catch (e: Exception) {
               _status.value = "Failure: ${e.message}"
           }
       }
   }
}

REST 웹 서비스

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

JSON 파싱

  • 웹 서비스의 응답은 구조화된 데이터를 나타내는 일반적인 형식인 JSON 형식으로 지정되는 경우가 많습니다.
  • JSON 객체는 키-값 쌍 모음입니다.
  • JSON 객체 모음은 JSON 배열입니다. 웹 서비스의 응답으로 JSON 배열을 받게 됩니다.
  • 키-값 쌍의 키는 따옴표로 묶입니다. 값은 숫자이거나 문자열일 수 있습니다.
  • Moshi 라이브러리는 JSON 문자열을 Kotlin 객체로 변환하는 Android JSON 파서입니다. Retrofit에는 Moshi와 호환되는 변환기가 있습니다.
  • Moshi는 JSON 응답의 키와 이름이 같은 데이터 객체의 속성을 일치시킵니다.
  • 키에 다른 속성 이름을 사용하려면 해당 속성에 @Json 주석과 JSON 키 이름으로 주석을 추가합니다.

Android 개발자 문서:

Kotlin 문서:

기타: