본문 바로가기

안드로이드

안드로이드 비동기처리 어떻게하세요?(enqueue vs future)

비동기처리 뭐 쓰세요?(Thread pool은 어떻게 관리할까?)

내가 제일 좋아하는 주제가 나왔다.

내가 제일 좋아하는 "비동기처리, 오류처리, 그리고 레트로핏 라이브러리 까보기"

뭔가 이름이 ‘이브, 프시케 그리고 푸른 수염의 아내’ 같다 ㅋㅋㅋㅋㅋㅋ

어쩃든 주제가 무엇인가? 비동기처리 Thread관리에 따른 성능차이이다.

 

// 이 글을 쓰기 시작한 시작한 날로부터 한달이 넘게 지나도록 학습하고있는 2023/07/10 기록

이 글을 쓰다가 이렇게 오래걸리고 돌아올줄 몰랐는데 이글은 결론적으로 Thread pool의 관리개념과 최적화 관련글이 되어버렸다.(얼탱이가 없네)

 

이 글 또한 오류처리처럼 방대한 개념을 모았고 학습기간도 오래되었기 때문에 글이 두서없을 가능성이 크다 그러므로 키워드 위주로 뽑아보면 좋을 수 있다.

 

결론부터 정리하고 시작하면. 뭐가 되었든 쓰레드를 관리하지 않는 방식으로 사용하는것은 무책임한 방법이다.

 

우선 글에 들어가기에 앞서 우리가 Thread 와 Thread pool에 대해 학습해야하는 이유를 살펴보자

Thread pool 꼭 공부해야해?

비동기 처리는 개발분야에서 빼놓고 갈수없는 분야이지만 러닝커브가 높고 신입개발자에게는 공포적인 존재이다.

( 특히나 Thread pool은 공부를 꽤 많이 했는데도 대략적인 개념의 이해일뿐 여전히 도메인 상황에 맞는 커스텀이 필요함으로 많은 실험과 도메인에 대한 이해도 또한 필요하다.)

제이슨에게 비동기를 잘 공부하려면 어느책을 봐야할까요 물어보면

이따구로 생긴 기차책을 추천하는데 정말 무섭다 -> 제이슨은 심지어 가볍게 접근할수있는 책부터 추천해주는데 비동기자체가 얼마나 빡세면 이렇게 생긴 책을 추천하는지 두렵다.(참고로 흑백에 두껍다 학교 전공서 느낌 거의 화공양론급 불친절함이다...)

 

이렇게 러닝 커브가 높다보니 기초적인 사용법만 익히고 최적화 혹은 쓰레드 관리측면은 시도 조차 안하고 돌아만 가게 하는경우가 많다.

(초심자 대학생이라면 상관없겠지만 주니어 딱지를 달려면 쓰레드 관리를 해야하지 않을까?)

하지만 이런부분을 가볍게 넘어갈만한 주제인가? 하면 또 아니다.

물론 성능측면에서 무시하겠다면 뭐 할말은 없지만 이런 티끌들이 모이면 앱을 사용할수있는 사용자가 그만큼 줄어든다는 뜻이기도 하다.

또한 아예 Thread에 대한 이해나 Thread pool에 대한 이해없이 코드를 짜는것은 너무 우가우가 구석기 코드라 생각한다.

 

사실상 Thread를 무식하게 계속 생성해서 사용한다면 누가봐도 문제가 있는 코드이고 자바에서 제공하는 Executor관련 클래스들(ex.ThreadPoolExecutor, ScheduledThreadPoolExecutor, ForkJoinPool, 가장 많이 사용하는 유틸클래스 Executors)를 사용하더라도 이는 가장 기저에 깔린 언어에서 지원하는 기술이니 좀 투박한 면이있다.

 

이를 세련되게 쓰거나(가독성이 좋고 사용하기 편리하게 변경), 및 기타 필요한 개념들을 얹어서 만들어진 비동기(동시성)처리 혹은 병렬성관련 처리를 지원하는 api들이 많은데 대표적으로 자바 코틀린 진영에서 많이 쓰이는것으로 코루틴,RX,Future등등이 있다.

 

하지만 이런 새로운 api들 또한 어짜피 Thread Pool 을 생성하고 관리하는 측면에서 디폴트 Thread pool이 관리되고있고(Thread pool이 강제된것이나 다름없음) 이런것들을 세련되게 기타 역경이 오기전까지 알아서 api측면에서 처리해주기 때문에 Thread와 Thread pool 관련 학습이 필요없어 보인다.

 

이런 이유 때문에 Thread 와 Thread pool의 학습을 미루는 경향이 있는데 언젠가는 마주 해야하는 기술 부채라고 생각한다.(신기술들이 그냥 마법을 부려서 성능을 좋게하는게 아니라 이런 Thread pool을 최대한 범용적으로 사용할 수 있도록 처리하여 강제하기 때문에 성능이 좋아지는것이기 때문이다.)

 

이런 api들을 하나하나 뜯어보면 다 직접 Executor를 직접 커스텀하여 성능을 높일수있도록 Executor을 갈아끼울수 있도록 열어놓았다.

 

이 소리는 Thread pool과 Thread의 이해도를 가지고 커스텀한 Executor를 적용하여 현재 도메인상황에 맞는 적절한 처리(Thread수 관리, 유휴시간 관리 등등)를 통해 성능의 최적화가 가능하다는 이야기이다.

 

우리가 직접 커스텀 하지않더라도 어느정도 이해도가 있어야 문제가 터졌을때 적절한 대처를 할수있지 않을까? 라는 부분에서 충분히 학습해야할 가치가 있다고 생각한다.


멧돼지의 실험 CompletableFuture 와 Retrofit enqueue

우테코 레벨2 마지막 미션은 비동기 처리를 하되 코루틴,Rx 등 최근 대세인 비동기 처리방식을 사용하지말고 전통적인 enqueue에 콜백전달, Thread, 안드로이드의 Handler,Looper를 이용하여 처리를 하라는 요구사항이 있었다.(배우지 않은거 쓰지마라 + 가독성이 산으로 가는것을 보면서 앞으로 배울것들의 소중함을 느껴라 정도의 의미?)

 

어쨋든 이러다보니 각각의 선택지는 갈렸다. 대충 3가지 방법으로 갈렸다.

  1. Retrofit의 enqueue활용
  2. 자바의 Thread 클래스 혹은 Executors를 통한 쓰레드 풀 처리
  3. CompletableFuture를 이용한 처리

 

학습을 진행하며 기술을 사용하는데 근거를 확보하기위해

Retrofit의 enqueue 와 CompletableFuture 에 대한 비교와 성능 차이에 대해서 실험을 진행하였다.

이에 대해서 이야기 하기전에 안드로이드 개발자라면 생소할수있는 Completable Future에 대해서 살펴보고 가자


CompletableFuture

java5에 비동기 처리를 간편하게 할수있도록 제공하는 Future라는 api가 있었는데 몇가지 한계점(여러 작업을 조합하거나 예외처리 불가 등등)

을 보완하기 위해 java8에서 future의 문제를 해결한 CompletableFuture가 나온것이다.

 

CompletableFuture를 각잡고 공부할게 아니라 chatgpt에게 간단히 요약해달라고 한결과

CompletableFuture는 Future 인터페이스를 확장하며, 비동기적으로 실행되는 작업의 결과를 나타냅니다. 기존의 Future와는 달리 CompletableFuture는 작업이 완료되었을 때 결과를 직접 설정하거나, 다른 CompletableFuture와 조합하여 병렬 작업을 처리하고 결과를 처리할 수 있습니다.

CompletableFuture는 Promise와 유사한 개념으로, 비동기 작업의 결과를 나중에 받을 수 있는 객체입니다. 이 객체는 작업의 성공, 실패 또는 취소 여부를 나타내는 상태를 가지며, 작업이 완료되면 그 결과를 얻을 수 있습니다.

제공하는 api들을 대략적으로 살펴본결과 결과 값을 기다리고, 작업을 체이닝 할수있고, 가독성이 좋게 만들어줄수 있는 기능들을 제공하고있다.

언어차원에서 비동기처리 지원기능 혹은 coroutine 개념을 도입하여 처리할 수 있도록 만들어준 언어의 api들이 죄다 비스무리 한것처럼 CompletableFuture도 순수하게 쓰레드 혹은 Executor를 통해서 쓰레드 풀링해서 사용하는것 보다 가독성이 좋고 개발자 입장에서 편리하게 비동기처리 를 할수있도록 도와주는 기능이다.

 

 

또한 자세히는 다루지 않을것이지만 Future또한 내부적으로 알아서 Thread pool을 유지하며 관리하고있다. -> 언어차원의 api라 그런지 효율적인 편이다.

Java 8부터 기본적으로 CompletableFutureForkJoinPool.commonPool()을 사용하여 스레드를 관리합니다. commonPool()은 기본적으로 현재 환경에 적합한 개수의 스레드를 생성하는 공유 스레드 풀입니다. 일반적으로 CPU 코어의 수에 따라 동적으로 스레드를 조정하므로 작업 부하에 따라 스레드 개수가 달라질 수 있습니다.

일단 java8 버전에서 어떤 Thread pool 을 사용하는가를 간단하게 살펴봤는데 자바의 업데이트가 빨라서 그런지 java9에서도 세부적으로 다른 Threadpool을 사용하지만 Thread pool을 관리하여 성능을 최적화 한다는 것이 주요 내용이다.

 

Future 또한 직접 커스텀한 Executor를 사용하도록 커스텀할수 있으므로 Thread pool에 대한 이해도가 있다면 성능을 끌어올릴수 있을것이다.(하지만 일반적으로 깊은 이해도가 없다면 디폴트로 언어차원에서 제공하는 방법이 훨씬 효율적일 것이다.)

 

 

CompletableFuture를 사용법 정도라도 좀 더 공부해보고싶다면 이 두글을 참고하자

https://mangkyu.tistory.com/263

 

[Java] CompletableFuture에 대한 이해 및 사용법

이번에는 자바8에 추가된 CompletableFuture에 대해 알아보도록 하겠습니다. 1. CompletableFuture에 대한 이해 [ Future의 단점 및 한계 ] Java5에 Future가 추가되면서 비동기 작업에 대한 결과값을 반환 받을

mangkyu.tistory.com

https://velog.io/@suyeon-jin/JAVA-CompletableFuture

 

JAVA 비동기 프로그래밍: CompletableFuture

CompletableFuture를 이해하기 위해서 자바의 Concurrent 프로그래밍부터 짚어볼 필요가 있다. 1. Concurrent Programming Concurrent 소프트웨어는 동시에 여러 작업을 할 수 있는 소프트웨어를 의미한다. 예를 들

velog.io


멧돼지의 선택 Retrofit Enqueue

이 세가지 선택지중 나는 callback을 사용했다.

callback을 사용한 이유는 크게 3가지이다.

  1. 오류처리상 장점
  2. square사에서 만들어놓은 엄청 강력한 쓰레드 관리 및 쓰레드 풀링( 쓰레드 풀에대한 이해도가 있다면 조금의 조정이 가능하다.)
  3. 코루틴으로의 연결

오류처리는 이번글에서 논점이 살짝 벗어나고 굉장히 깊은이야기라 글이 또 산으로 갈수도 있으니 생략하고 코루틴으로의 연결 또한 마찬가지 이유로 이번글에서는 다루지 않는다.

 

나머지 쓰레드 관리 측면에서 callback 을 사용한 이유를 설명해보겠다.

 

square사에서 만들어놓은 엄청 강력한 쓰레드 관리 및 쓰레드 풀링

위에서 Thread pool을 학습해야하는 이유를 설명하였다.

이런부분에서 Thread pool을 학습하지않고 주니어에게 적당히 타협해서 성능은 챙기는 방법은 없을까? -> 이미 사용하고 있는 라이브러리를 활용하자

 

아마 안드로이드 개발자라면 글을 쓰는 현시점 2023.06에는 HTTP 통신라이브러리로 Retrofit2를 많이 사용할것이다.

Retrofit의 경우 이글에서 설명하겠지만 Thread pool을 Okhttp3에서 효율적으로 처리하도록 되어있다.

 

이 부분 또한 이해도를 가지고 적절한 세부 항목들을 조정하여 최적화를 하여 사용한다면 전체적인 Thread Pool관리 로직을 짜지않고도 최적화가 가능한 방향으로 나아갈 수 있을것이다(물론 언젠간 마스터가 되어야겠지).

 

 

square사는 뭐하는 곳일까?

Retrofit은 미친 킹갓제너럴제이슨같은 회사인 square에서 만들었다. 나는 square가 그냥 이런 라이브러리 만드는 회사인줄알았는데 그것이 아니라

Square는 기업과 소상공인을 위한 금융 기술 솔루션을 개발하는 회사입니다.

이렇게 일반 서비스를 만드는 회사인데 자기들이 쓰는것들을 라이브러리화 해서 뿌린것을 이렇게 안드로이드 전역에서 사용하는것이였다.

진짜 개쩐다....

 

어쨋든 retrofit 에서 비동기 처리를 위해 enqueue를 제공하는데 callback을 지정해서 비동기 처리를 해줄수있다.

물론 callback형태로 코드를 작성해야하는 단점이있지만 (콜백지옥, 극악의 가독성)

기본적으로 추가 학습없이 자동으로 Thread관리를 해준다는 CompletableFuture 과 비슷한 성능을 내거나 세부값들을 조정해준다면 더 좋은 성능을 낼수도 있다.

 

레트로핏은 어떤식으로 비동기처리를 할까?

https://mccoy-devloper.tistory.com/126

 

레트로핏은 어떤식으로 비동기처리를 할까?

부제: 레트로핏의 Thread pool 관리 및 어떤 Thread가 작업에 관여하는가? 서론 Retrofit은 아주 강력한 라이브러리라고 하는데 내부적으로 어떻게 쓰레드 관리,비동기처리를 하는지 궁금한적이 있지

mccoy-devloper.tistory.com

매번 글이 너무 길어지는것이 불편해서 글을 나눠보았다.

 

위에 첨부한 글에서의 내용과 같이 retrofit 을 사용한다면 내부적으로 촘촘하게 Thread Pool을 관리하고있고 또한 Dispatcher의 maxRequest,maxRequestsPerHost 조절을 통해 현재 프로젝트의 상황에 맞도록 조절도 가능하다.

 

그래서 이를 통해 CompletableFuture와의 성능비교 테스트를 해보았고 enqueue의 성능을 높이는 방향을 찾아보았다.

 

테스트 내용을 보기에 앞서 내가 겪은 바보같은 이야기를 해보자면

최초에 실험을 설계하는데 실수가있어 enqueue의 성능이 Future에 비해 3배정도 빠르게 나왔다.

 

이로인해 OkHttp의 Thread Pool 관리가 엄청 효율적일것이라 판단하여 OkHttp의 Thread Pool 관리를 학습한다면 효율적인 Thread Pool 관리에 대해서 익힐수 있다고 생각하여 OkHttp를 정말 샅샅히 뒤졌다.

하지만 CompletableFuture와 Thread Pool 관리에 있어서 뚜렷히 차이나는 부분이 없어 도데체 어디서 차이가 나는건지 square에서 마법을 부린건지 찾아다니다 제이슨에게 조언을 구했는데 실험이 잘못됐음을 짚어주셨다.

그래서 실험조건을 맞추고 나니 아주크게 성능이 차이나지는 않았다.

이런부분을 봤을때 Thread pool 관리를 하는 api 혹은 개발자가 직접 thread pool을 관리한다면 굳이 성능에 있어서 구애받을 필요없이 다른측면(가독성,사용성)을 선택에있어서 더 고려하는것이 좋을것이라는 생각이 들었다.

 

어쨋든 이제 실험결과를 쭉 봐보고 분석해보도록 하겠다.


1차실험(maxRequest,maxRequestsPerHost default 사용)

Enqueue(okHttp) 1000 번 5회

1회차: enqueue with CountDownLatch : 13443

2회차: enqueue with CountDownLatch : 12335

3회차: enqueue with CountDownLatch : 14532

4회차: enqueue with CountDownLatch : 16644

5회차: enqueue with CountDownLatch : 14901

 

Future 1000 번 5회

1회차: execute with CountDownLatch : 10866

2회차: execute with CountDownLatch : 10794

3회차: execute with CountDownLatch : 10289

4회차: execute with CountDownLatch : 10735

5회차: execute with CountDownLatch : 9747

 

테스트 코드

class FutureVsEnqueue {
    @Test
    fun okhttpEnqueueTest() {
        val enqueue = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val okHttpClient = OkHttpClient()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                okHttpClient.newCall(request).enqueue(object : Callback {
                    override fun onFailure(call: Call, e: IOException) {
                    }
​
                    override fun onResponse(call: Call, response: Response) {
                        latch.countDown()
                        response.close()
                    }
                })
            }
            latch.await()
        }
​
        println("enqueue with CountDownLatch : $enqueue")
    }
​
    @Test
    fun futureTest() {
        val execute = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val okHttpClient = OkHttpClient()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                CompletableFuture.supplyAsync {
                    val response = okHttpClient.newCall(request).execute()
                    response.close()
                }.thenAccept {
                    latch.countDown()
                }
            }
            latch.await()
        }
​
        println("execute with CountDownLatch : $execute")
    }
}

OkHttp의 maxRequest, maxRequestsPerHost 값을 조절하지 않고 기본값으로 놓고 테스트하였다.

 

많은 차이가 나지는 않지만 근소하게 CompletableFuture의 속도가 높은것을 볼수있다.

 

CompletableFuture의 경우 자바에서 가용할수있는 Thread수를 판단하여 Thread Pool의 크기를 결정하는것이기 때문에

현재 OKHttp가 Thread를 온전하게 사용하지 못한다고 판단 maxRequest, maxRequestsPerHost 를 늘려서 속도가 올라가는지 추가실험을 해보았다.


2차실험(maxRequest = 100, maxRequestsPerHost default = 100)

Enqueue(okHttp) 1000 번 5회

1회차: enqueue with CountDownLatch : 10791

2회차: enqueue with CountDownLatch : 10112

3회차: enqueue with CountDownLatch : 10844

4회차: enqueue with CountDownLatch : 12104

5회차: enqueue with CountDownLatch : 11233

 

Future 1000 번 5회

1회차: enqueue with CountDownLatch : 11467

2회차: enqueue with CountDownLatch : 11514

3회차: enqueue with CountDownLatch : 11756

4회차: enqueue with CountDownLatch : 11341

5회차: enqueue with CountDownLatch : 11675

 

테스트 코드

class FutureVsEnqueue {
    @Test
    fun okhttpEnqueueTest() {
        val enqueue = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val dispatcher = Dispatcher().apply {
                maxRequests = 100
                maxRequestsPerHost = 100
            }
            val okHttpClient = OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .build()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                okHttpClient.newCall(request).enqueue(object : Callback {
                    override fun onFailure(call: Call, e: IOException) {
                    }
​
                    override fun onResponse(call: Call, response: Response) {
                        latch.countDown()
                        response.close()
                    }
                })
            }
            latch.await()
        }
​
        println("enqueue with CountDownLatch : $enqueue")
    }
​
    @Test
    fun futureTest() {
        val execute = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val dispatcher = Dispatcher().apply {
                maxRequests = 100
                maxRequestsPerHost = 100
            }
            val okHttpClient = OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .build()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                CompletableFuture.supplyAsync {
                    val response = okHttpClient.newCall(request).execute()
                    response.close()
                }.thenAccept {
                    latch.countDown()
                }
            }
            latch.await()
        }
​
        println("execute with CountDownLatch : $execute")
    }
}

maxRequest, maxRequestsPerHost 값을 화끈하게 100으로 늘려버렸다.

확실히 OKHttp의 속도가 향상되었고 CompletableFuture 와 비슷한 성능을 내거나 좀 더 빨라졌다.

이런측면에서 maxRequest, maxRequestsPerHost을 늘리는 방향이 성능에 긍정적 영향을 끼친다고 생각되어 점진적으로 늘려 결과를 보는 실행을 진행하였다.


3차실험(maxRequest = 120, maxRequestsPerHost default = 120)

Enqueue(okHttp) 1000 번 5회

1회차: enqueue with CountDownLatch : 9458

2회차: enqueue with CountDownLatch : 8801

3회차: enqueue with CountDownLatch : 7439

4회차: enqueue with CountDownLatch : 7978

5회차: enqueue with CountDownLatch : 7601

 

Future 1000 번 5회

1회차: execute with CountDownLatch : 13388

2회차: execute with CountDownLatch : 15932

3회차: execute with CountDownLatch : 15801

4회차: execute with CountDownLatch : 14476

5회차: execute with CountDownLatch : 13823

 

테스트 코드

class FutureVsEnqueue {
    @Test
    fun okhttpEnqueueTest() {
        val enqueue = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val dispatcher = Dispatcher().apply {
                maxRequests = 120
                maxRequestsPerHost = 120
            }
            val okHttpClient = OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .build()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                okHttpClient.newCall(request).enqueue(object : Callback {
                    override fun onFailure(call: Call, e: IOException) {
                    }
​
                    override fun onResponse(call: Call, response: Response) {
                        latch.countDown()
                        response.close()
                    }
                })
            }
            latch.await()
        }
​
        println("enqueue with CountDownLatch : $enqueue")
    }
​
    @Test
    fun futureTest() {
        val execute = measureTimeMillis {
            val latch = CountDownLatch(1000)
            val url = "https://pbs.twimg.com/media/FpFzjV-aAAAIE-v?format=jpg&name=large"
            val dispatcher = Dispatcher().apply {
                maxRequests = 120
                maxRequestsPerHost = 120
            }
            val okHttpClient = OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .build()
            val request = Request.Builder().url(url).build()
​
            repeat(1000) {
                CompletableFuture.supplyAsync {
                    val response = okHttpClient.newCall(request).execute()
                    response.close()
                }.thenAccept {
                    latch.countDown()
                }
            }
            latch.await()
        }
​
        println("execute with CountDownLatch : $execute")
    }
}

maxRequest, maxRequestsPerHost 을 120으로 잡자 확연히 enqueue의 속도가 빨라졌다.


4차실험(maxRequest > 150, maxRequestsPerHost default > 150)

호출제한양을 150 이상 늘린결과 서버에서 거부하였다.(너무많은 요청이 동시에 들어와 튕겨버린것이라 예상) -> 애초에 너무 가혹한 환경이고 이 이상으로 호출제한을 늘리면 안될거라 예상된다. 서버, 인터넷, 현재 사용기기등 다양한 상황에 영향을 받으니 다양한 실험을해보고 maxRequests와 maxRequestsPerHost를 조절해야한다고 판단된다.

 

maxRequests와 maxRequestsPerHost를 150 이상으로 설정하였을시 나타난 Exception:

okhttp3.internal.http2.StreamResetException: stream was reset: REFUSED_STREAM

 

StreamResetException에 대한내용:

StreamResetException은 OkHttp 라이브러리의 내부 HTTP/2 모듈에 특정한 예외이다.

이 예외는 서버가 요청된 스트림을 처리하거나 처리하지 않기로 거부했음을 나타낸다.

 

서버가 요청을 받고 처리하지 않기로 결정한 경우, "REFUSED_STREAM" 오류 코드를 클라이언트에게 전송하여 해당 스트림이 재설정되었으며 요청이 수행되지 않을 것임을 알릴 수 있다. StreamResetException은 이러한 상황을 클라이언트 애플리케이션에게 알리기 위해 OkHttp에서 발생시키는 예외이다.

 

스트림이 거부되는 이유는 다음과 같을 수 있습니다:

  1. 서버가 과부하 상태이거나 바쁘기 때문에 들어오는 요청을 처리할 수 없습니다.
  2. 서버가 인증 또는 권한 문제로 인해 요청을 거부했습니다.
  3. 서버가 요청한 기능이나 리소스를 지원하지 않습니다.
  4. 서버가 요청을 처리하는 동안 내부 오류가 발생했습니다.

거부당한 예상시나리오: 한번에 너무많은 요청을 넣어버려서 그냥 거부당했다.

실험 결과 분석

결국 실험을 분석해보면 Thread Pool을 관리하고있는한 엄청나게 뚜렷한 속도차이는 만들어내기 힘들다.

하지만 OkHttp의 maxRequest, maxRequestsPerHost의 조절을 통해서 어느정도 성능의 최적화를 얻어낼 수 있다는 결과를 도출했다.

maxRequest, maxRequestsPerHost가 임계점을 넘기전까지 올릴때 성능은 항상되는 경향이있다.

또한 너무 많은 요청을 한다면 서버측에서 요청을 거부해 버릴수도 있기 때문에 한꺼번에 너무 많은 요청을 하는것은 막아야한다.

 

하지만 이 실험에는 맹점이 다수 존재한다.

 

    1 . 이렇게 가혹한 환경이 안드로이드에서 나오기 힘들다.

애초에 안드로이드 환경에서 이런식으로 서버통신을 repeat로 최대한 짧은 시간안에 1000번 찌를일이 별로 없거니와 이런경우 서버가 너무 많은 요청이라 판단하여 요청을 튕겨버릴 가능성이 있다.

 

    2 . 현재 실험하고있는 상황이 핸드폰 실기기가 아니라 나의 강력한 맥북프로에서 에뮬도 아니고 그냥 Junit으로 돌린것이다.

현재 사용중인 맥북이 cpu만 14코어이다. 그런데 이런 무지막지한 환경에서 실험하니 가용가능한 코어가 핸드폰에 비해 넘칠테고 이로 인해 Thread의 갯수 혹은 요청의 갯수를 늘리면 늘릴수록 좋은 방향으로 실험 결과가 나왔을것이다.

만약 실제적으로 성능 최적화가 매우 중요하다면 주요 타겟층이 많이 사용하는 기기사양의 실기기를 가지고 실험해서 평균치를 가지고 값을 정해야할 것 같다.

 

이런것들을 고려하여 최종적으로 내린결론은 maxRequest, maxRequestsPerHost의 값을 크게 조절하지 않는 방향으로 하고

maxRequest의 default값이 64인데 maxRequestsPerHost의 디폴트값이 5이므로 요청할 수 있는 서버의 갯수가 2개라 가정했을 때

64개의 동시요청 가능한 크기에 비해 10개의 요청만 동시에 할 수 있게 됨으로 온전히 리소스를 사용하지 못한다 판단하였다.

 

이에 대한 조정으로 최초에는 maxRequest의 경우 default를 사용하고 maxRequestsPerHost = maxRequest/서버수 로 설정하려했지만,

예상치못한 다양한 상황이 발생할 수 있으며 동시요청을 많이 늘려봐야 성능이 좋지 못한 편인 핸드폰 기기에서 오히려 많은 쓰레드의 생성으로 인하여 컨텍스트 스위칭에 의한 성능저하를 일으킬수도 있다고 생각하여 보수적으로 maxRequestPerHost를 10으로 늘리는 방향으로 커스텀해서 사용할것이다.

 

어쨋든 이 실험을 통해서 좀 더 유연하게 maxRequest, maxRequestsPerHost를 조정해서 사용해야겠다는 판단을 하였다.

도메인의 환경에 따라 서버통신을 연속적으로 많이해야하는 가혹한 환경이라면 maxRequest, maxRequestsPerHost를 늘리는 방향으로 고려하고

 

서버의 환경, 현재 타겟층이 많이 사용하는 실기기의 성능 등등을 고려하여 안드로이드 실기기 위에서 돌아가는 테스트를 통해서 값을 도출해내면 될것이라고 생각한다.