Cronet 기본사항

1. 소개

1ee223bf9e1b75fb.png

최종 업데이트: 2022년 5월 6일

Cronet은 Android 앱에서 라이브러리로 사용하도록 제공되는 Chromium 네트워크 스택입니다. Cronet은 지연 시간을 줄이고 앱이 작동해야 하는 네트워크 요청의 처리량을 늘리는 여러 기술을 활용합니다.

Cronet 라이브러리는 YouTube, Google 앱, Google 포토, 지도 - 탐색 및 대중교통 등 매일 수백만 명이 사용하는 앱의 요청을 처리합니다. Cronet은 가장 많이 사용되는 Android 네트워킹 라이브러리로, HTTP3를 지원합니다.

자세한 내용은 Cronet 기능 페이지를 참고하세요.

빌드할 항목

이 Codelab에서는 이미지 디스플레이 애플리케이션에 Cronet 지원을 추가합니다. 그러면 앱에서 다음과 같이 작동하게 됩니다.

  • Google Play 서비스에서 Cronet을 로드하거나 Cronet을 사용할 수 없는 경우 안전하게 대체합니다.
  • Cronet을 사용하여 요청을 전송하고 응답을 수신하고 처리합니다.
  • 간단한 UI에 결과를 표시합니다.

28b0fcb0fed5d3e0.png

학습할 내용

  • Cronet을 앱에 종속 항목으로 포함하는 방법
  • Cronet 엔진을 구성하는 방법
  • Cronet을 사용하여 요청을 전송하는 방법
  • Cronet 콜백을 작성하여 응답을 처리하는 방법

이 Codelab에서는 Cronet을 사용하는 데 중점을 둡니다. 애플리케이션은 대부분 사전 구현되어 있으므로 Android 개발 경험이 거의 없어도 이 Codelab을 완료할 수 있습니다. 하지만 이 Codelab을 최대한 활용하려면 Android 개발 기본사항과 Jetpack Compose 라이브러리에 관해 알고 있어야 합니다.

필요한 항목

2. 코드 가져오기

이 프로젝트에 필요한 모든 것을 Git 저장소에 넣었습니다. 시작하려면 저장소를 클론하고 Android 스튜디오에서 코드를 엽니다.

git clone https://github.com/android/codelab-cronet-basics

3. 기준 설정

시작점

이 Codelab을 위해 설계된 기본 이미지 디스플레이 앱에서 시작해 보겠습니다. 이미지 추가 버튼을 클릭하면 인터넷에서 이미지를 가져오는 데 걸린 시간에 관한 세부정보와 함께 새 이미지가 목록에 추가됩니다. 애플리케이션은 Kotlin에서 제공하는 내장된 HTTP 라이브러리를 사용하며 이 라이브러리는 고급 기능을 지원하지 않습니다.

이 Codelab 과정을 진행하며 Cronet 및 일부 기능을 사용하도록 애플리케이션을 확장해 보겠습니다.

4. Gradle 스크립트에 종속 항목 추가

Cronet을 애플리케이션과 함께 제공되는 독립형 라이브러리로 통합하거나, 또는 플랫폼에서 제공하는 대로 사용할 수 있습니다. Cronet팀에서는 Google Play 서비스 제공업체를 사용하는 것을 권장합니다. Google Play 서비스 제공업체를 사용하면 애플리케이션이 Cronet 제공의 바이너리 크기 비용(약 5MB)을 지불할 필요가 없습니다. 플랫폼에서는 최신 업데이트 및 보안 수정사항의 제공을 보장합니다.

구현을 가져오려는 방식과 관계없이 Cronet API를 포함하려면 cronet-api 종속 항목도 추가해야 합니다.

build.gradle 파일을 열고 dependencies 섹션에 다음 두 줄을 추가합니다.

implementation 'com.google.android.gms:play-services-cronet:18.0.1'
implementation 'org.chromium.net:cronet-api:101.4951.41'

5. Google Play 서비스 Cronet 제공업체 설치

이전 섹션에서 설명한 것처럼 Cronet은 여러 가지 방법으로 애플리케이션에 추가할 수 있습니다. 이러한 각 방법은 라이브러리와 애플리케이션 간의 필수 링크가 포함되도록 하는 Provider로 추상화됩니다. 새로운 Cronet 엔진을 만들 때마다 Cronet은 모든 활성 제공업체를 확인하고 엔진을 인스턴스화하는 데 가장 적합한 제공업체를 선택합니다.

Google Play 서비스 제공업체는 일반적으로 즉시 사용할 수 없으므로 먼저 설치해야 합니다. MainActivity에서 TODO를 찾아 다음 스니펫을 붙여넣습니다.

val ctx = LocalContext.current
CronetProviderInstaller.installProvider(ctx)

이렇게 하면 제공업체를 비동기식으로 설치하는 Play 서비스 Task가 실행됩니다.

6. 제공업체 설치 결과 처리

제공업체를 설치했습니다. 잠깐, 정말 설치되었을까요? Task는 비동기식이며 결과는 어떤 방식으로도 처리되지 않았습니다. 이 문제를 해결해보겠습니다. installProvider 호출을 다음 스니펫으로 바꿉니다.

CronetProviderInstaller.installProvider(ctx).addOnCompleteListener {
   if (it.isSuccessful) {
       Log.i(LOGGER_TAG, "Successfully installed Play Services provider: $it")
       // TODO(you): Initialize Cronet engine
   } else {
       Log.w(LOGGER_TAG, "Unable to load Cronet from Play Services", it.exception)
   }
}

이 Codelab의 목적상 Cronet 로드에 실패하는 경우 네이티브 이미지 다운로더를 계속 사용합니다. 네트워킹 성능이 애플리케이션에 중요한 경우 Play 서비스를 설치하거나 업데이트하는 것이 좋습니다. 자세한 내용은 CronetProviderInstaller 문서를 참고하세요.

이제 애플리케이션을 실행합니다. 모든 것이 제대로 작동하면 제공업체가 성공적으로 설치되었다는 로그 구문이 표시됩니다.

7. Cronet 엔진 만들기

Cronet 엔진은 Cronet으로 요청을 전송하는 데 사용할 핵심 객체입니다. 엔진은 다양한 Cronet 옵션을 구성할 수 있는 빌더 패턴을 사용하여 구성됩니다. 지금은 기본 옵션을 계속 사용합니다. TODO를 다음 스니펫으로 바꿔 새 Cronet 엔진을 인스턴스화합니다.

val cronetEngine = CronetEngine.Builder(ctx).build()
// TODO(you): Initialize the Cronet image downloader

8. Cronet 콜백 구현

Cronet의 비동기적인 특성은 콜백, 즉 UrlRequest.Callback의 인스턴스를 사용하여 응답 처리가 제어된다는 의미입니다. 이 섹션에서는 메모리에 대한 전체 응답을 읽는 도우미 콜백을 구현합니다.

ReadToMemoryCronetCallback이라는 새 추상 클래스를 만들고 UrlRequest.Callback을 확장하도록 한 후 Android 스튜디오에서 메서드 스텁을 자동으로 생성하도록 합니다. 새 클래스는 다음 스니펫과 비슷하게 표시됩니다.

abstract class ReadToMemoryCronetCallback : UrlRequest.Callback() {
   override fun onRedirectReceived(
       request: UrlRequest,
       info: UrlResponseInfo,
       newLocationUrl: String?
   ) {
       TODO("Not yet implemented")
   }

   override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
       TODO("Not yet implemented")
   }

   override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onReadCompleted(
       request: UrlRequest,
       info: UrlResponseInfo,
       byteBuffer: ByteBuffer
   ) {
       TODO("Not yet implemented")
   }
}

onRedirectReceived, onSucceeded, onFailed 메서드는 설명이 따로 필요하지 않으므로 지금은 세부정보를 다루지 않고 onResponseStartedonReadCompleted에 중점을 두겠습니다.

onResponseStarted는 Cronet이 요청을 전송하고 모든 응답 헤더를 수신한 후 본문 읽기를 시작하기 전에 호출됩니다. Cronet은 다른 일부 라이브러리(예: Volley)처럼 전체 본문을 자동으로 읽지 않습니다. 대신 UrlRequest.read()를 사용하여 본문의 다음 청크를 개발자가 제공하는 버퍼로 읽습니다. Cronet은 응답 본문 청크 읽기를 완료하면 onReadCompleted 메서드를 호출합니다. 이 프로세스는 더 이상 읽을 데이터가 없을 때까지 반복됩니다.

39d71a5e85f151d8.png

읽기 주기 구현을 시작하겠습니다. 먼저 새 바이트 배열 출력 스트림과 이를 사용하는 채널을 인스턴스화합니다. 채널을 응답 본문의 싱크로 사용합니다.

private val bytesReceived = ByteArrayOutputStream()
private val receiveChannel = Channels.newChannel(bytesReceived)

다음으로, onReadCompleted 메서드를 구현하여 바이트 버퍼의 데이터를 싱크에 복사하고 다음 읽기를 호출합니다.

// The byte buffer we're getting in the callback hasn't been flipped for reading,
// so flip it so we can read the content.
byteBuffer.flip()
receiveChannel.write(byteBuffer)

// Reset the buffer to prepare it for the next read
byteBuffer.clear()

// Continue reading the request
request.read(byteBuffer)

본문 읽기 루프를 완료하려면 onResponseStarted 콜백 메서드에서 초기 읽기를 호출합니다. Cronet에서는 직접 바이트 버퍼를 사용해야 합니다. 이 Codelab의 목적상 버퍼의 용량은 중요하지 않지만 16KiB가 대부분의 프로덕션 용도에서 적절한 기본값입니다.

request.read(ByteBuffer.allocateDirect(BYTE_BUFFER_CAPACITY_BYTES))

이제 클래스의 나머지 부분을 마치겠습니다. 리디렉션에는 별 관심이 없으므로 웹브라우저와 마찬가지로 리디렉션을 따르면 됩니다.

override fun onRedirectReceived(
   request: UrlRequest, info: UrlResponseInfo?, newLocationUrl: String?
) {
   request.followRedirect()
}

마지막으로 onSucceededonFailed 메서드를 처리해야 합니다. onFailed는 도우미 콜백 사용자에게 제공하려는 서명과 일치하므로 정의를 삭제하고 확장 클래스가 메서드를 재정의하도록 할 수 있습니다. onSucceeded는 본문 다운스트림을 바이트 배열로 전달해야 합니다. 서명에 본문과 함께 새로운 추상 메서드를 추가합니다.

abstract fun onSucceeded(
   request: UrlRequest, info: UrlResponseInfo, bodyBytes: ByteArray)

그런 다음 요청이 성공적으로 완료되면 새 onSucceeded 메서드가 올바르게 호출되었는지 확인합니다.

final override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
   val bodyBytes = bytesReceived.toByteArray()
   onSucceeded(request, info, bodyBytes)
}

잘하셨습니다. Cronet 콜백을 구현하는 방법을 배웠습니다.

9. 이미지 다운로더 구현

이전 섹션에서 만든 콜백을 사용하여 Cronet 기반 이미지 다운로더를 구현해 보겠습니다.

ImageDownloader 인터페이스를 구현하고 CronetEngine을 생성자 매개변수로 허용하는 CronetImageDownloader라는 새 클래스를 만듭니다.

class CronetImageDownloader(val engine: CronetEngine) : ImageDownloader {
   override suspend fun downloadImage(url: String): ImageDownloaderResult {
       TODO("Not yet implemented")
   }
}

downloadImage 메서드를 구현하려면 Cronet 요청을 만드는 방법을 알아야 합니다. 간단합니다. CronetEnginenewUrlRequestBuilder() 메서드를 호출하면 됩니다. 이 메서드는 url, 콜백 클래스의 인스턴스, 콜백 메서드를 실행하는 실행자를 사용합니다.

val request = engine.newUrlRequestBuilder(url, callback, executor)

URL은 downloadImage 매개변수를 통해 알 수 있습니다. 실행자의 경우 인스턴스 전체 필드를 만듭니다.

private val executor = Executors.newSingleThreadExecutor()

마지막으로 이전 섹션의 도우미 콜백 구현을 사용하여 callback을 구현합니다. 이 구현은 Kotlin 코루틴 주제와 더 관련이 있으므로 자세히 다루지 않습니다. cont.resumedownloadImage 메서드의 return으로 생각하면 됩니다.

종합하면 downloadImage 구현은 다음 스니펫과 같습니다.

override suspend fun downloadImage(url: String): ImageDownloaderResult {
   val startNanoTime = System.nanoTime()
   return suspendCoroutine {
       cont ->
       val request = engine.newUrlRequestBuilder(url, object: ReadToMemoryCronetCallback() {
       override fun onSucceeded(
           request: UrlRequest,
           info: UrlResponseInfo,
           bodyBytes: ByteArray) {
           cont.resume(ImageDownloaderResult(
               successful = true,
               blob = bodyBytes,
               latency = Duration.ofNanos(System.nanoTime() - startNanoTime),
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }

       override fun onFailed(
           request: UrlRequest,
           info: UrlResponseInfo,
           error: CronetException
       ) {
           Log.w(LOGGER_TAG, "Cronet download failed!", error)
           cont.resume(ImageDownloaderResult(
               successful = false,
               blob = ByteArray(0),
               latency = Duration.ZERO,
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }
   }, executor)
       request.build().start()
   }
}

10. 최종 배선

MainDisplay 컴포저블로 돌아가 방금 만든 이미지 다운로더를 사용하여 마지막 TODO를 처리해 보겠습니다.

imageDownloader = CronetImageDownloader(cronetEngine)

잘하셨습니다. 애플리케이션을 실행해 보세요. Cronet 이미지 다운로더를 통해 요청이 라우팅되는 것을 확인할 수 있습니다.

11. 맞춤설정

요청 수준과 엔진 수준에서 모두 요청 동작을 맞춤설정할 수 있습니다. 캐싱을 사용하여 이를 보여주지만 더 많은 옵션이 있습니다. 자세한 내용은 UrlRequest.BuilderCronetEngine.Builder 문서를 참고하세요.

엔진 수준에서 캐싱을 사용 설정하려면 빌더의 enableHttpCache 메서드를 사용합니다. 아래 예에서는 메모리 내 캐시를 사용합니다. 사용할 수 있는 다른 옵션을 보려면 문서를 참고하세요. Cronet 엔진을 만들면 다음과 같이 됩니다.

val cronetEngine = CronetEngine.Builder(ctx)
   .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 10 * 1024 * 1024)
   .build()

애플리케이션을 실행하고 이미지를 몇 개 추가합니다. 반복적으로 추가된 이미지는 지연 시간이 훨씬 짧고 UI에서는 이미지가 캐시된 것으로 표시합니다.

이 기능은 요청별로 재정의할 수 있습니다. Cronet 다운로더에 약간의 해킹을 적용하여 URL 목록 중 첫 번째인 태양 이미지의 캐싱을 사용 중지해 보겠습니다.

if (url == CronetCodelabConstants.URLS[0]) {
   request.disableCache()
}

request.build().start()

이제 애플리케이션을 다시 실행합니다. 태양 이미지가 캐시되지 않습니다.

d9d0163c96049081.png

12. 결론

축하합니다. Codelab을 완료했습니다. 이 과정에서 Cronet 사용 방법에 관한 기본사항을 알아봤습니다.

Cronet에 관한 자세한 내용은 개발자 가이드소스 코드를 확인하세요. 또한 Android 개발자 블로그를 구독하여 Cronet 및 일반 Android 뉴스를 가장 먼저 확인하세요.