나미래 Android 개발자

OkHttp는 왜 사용했을까? 본문

안드로이드/Why

OkHttp는 왜 사용했을까?

Moimeme Futur 2026. 1. 6. 07:00

Android 공식 배발 가이드에서도 네트워크 통신 라이브러리로 Retrofit 사용을 권장하고 있다.
나 또한 Retrofit을 당연하게 사용해왔지만 왜 Retrofit 사용이 표준이 되었는지 궁금해졌고 글로 설명된 Retrofit 장점을 직접 느껴보기 위해서 Square에서 제공하는 OkHttp가 어떻게 사용되는지 학습하게 됐다.

 

이 글에서는 OkHttp를 직접 사용해 네트워크 통신을 구현해보며, OkHttp가 어떤 방식으로 동작하는지와 Java의 HttpURLConnection 대비 어떤 점이 개선되었는지를 정리해본다.

 

OkHttp를 이용한 네트워크 GET 요청 처리

OkHttp에서는 HttpURLConnection과 다르게 사용자가 동기적으로 통신할 수 있고 비동기적으로 통신할 수 있는 선택지를 제공한다.
먼저 동기적으로 유저 정보를 가져오는 GET 방식을 학습했다.

유저 정보를 읽어오는 GET 요청 구현 소스 코드 1 - 동기 방식(execute)

val client = OkHttpClient()

fun fetchUserWithOkHttpClient(userId: Int): User? {
    val request = Request.Builder()
        .url("https://jsonplaceholder.typicode.com/users/$userId")
        .build()
    return try {
        val response = client.newCall(request).execute()
        if (response.isSuccessful) {
            val jsonBody = response.body()?.string()
            Gson().fromJson(jsonBody, User::class.java)
        } else {
            null
        }
    } catch(e: Exception) {
        null
    }
}

네트워크 통신 이해 및 소스 코드 분석

1. 네트워크 통신을 수행할 OkHttpClient()를 생성한다.

  • val client = OkHttpClient()

2. 네트워크 통신에 대한 정보를 갖는 Request 객체를 만든다.

  • Request 객체는 Builder 패턴으로 만든다.
  • Builder#url() 메서드를 이용해서 서버의 사용자 조회 API URL를 설정한다.
    • .url("https://jsonplaceholder.typicode.com/users/$userId")
  • build()를 통해서 통신 관련된 설정값을 포함한 Request를 만든다.

네트워크 통신 method를 정의하지 않은 이유는 Builder를 통해 생성 시, 기본값이 GET이기 때문에 설정하지 않았다.

3. OkHttpClient를 이용해서 네트워크 통신을 동기적으로 처리하도록 요청한다.

  • client.newCall(request).execute()
  • execute(): 동기적으로 처리하는 함수이다.

4. 네트워크 통신이 성공한 경우에 대해 처리한다.

  • response.isSuccessful: 네트워크 통신 결과값(OkHttp에서 제공하는 Response)을 통해서 성공 여부를 확인한다.
  • response.body()?.string(): 통신 결과값 중, Body 영역에 해당하는 값을 읽어온다.
  • Gson().fromJson(jsonBody, User::class.java): Json 형식의 문자열로 반환된 Body 값을 미리 정의해둔 User 데이터 클래스 형태로 변환한다.

 

유저 정보를 읽어오는 GET 요청 구현 소스 코드 2 - 비동기 방식(enqueue)

val client = OkHttpClient()

private fun fetchUserWithOkHttpClient(
    userId: Int,
    onSuccess: (User) -> Unit,
    onError: (String) -> Unit
) {
    val request = Request.Builder()
        .url("https://jsonplaceholder.typicode.com/users/$userId")
        .build()

    client.newCall(request).enqueue(object : Callback {
        override fun onResponse(response: Response) {
            if (response.isSuccessful) {
                val jsonBody = response.body().string()
                val user = Gson().fromJson(jsonBody, User::class.java)
                onSuccess(user)
            }
        }

        override fun onFailure(request: Request, e: IOException) {
            onError(e.toString())
        }
    })
}

네트워크 통신 이해 및 소스 코드 분석

1. 네트워크 통신을 수행할 OkHttpClient()를 생성한다.

  • val client = OkHttpClient()

2. 네트워크 통신에 대한 정보를 갖는 Request 객체를 만든다.

  • Request 객체는 Builder 패턴으로 만든다.
  • Builder#url() 메서드를 이용해서 서버의 사용자 조회 API URL를 설정한다.
    • .url("https://jsonplaceholder.typicode.com/users/$userId")
  • build()를 통해서 통신 관련된 설정값을 포함한 Request를 만든다.

네트워크 통신 method를 정의하지 않은 이유는 Builder를 통해 생성 시, 기본값이 GET이기 때문에 설정하지 않았다.

3. OkHttpClient 이용해서 네트워크 통신을 비동기적으로 처리하도록 요청한다.

  • client.newCall(request).enqueue(object: Callback{ ... })
  • enqueue(): 비동기적으로 처리하는 함수이다.
  • object: Callback{ ... }: 비동기적으로 처리하기 위해서 네트워크 통신 성공과 실패를 처리할 콜백을 선언해준다.
    • onResponse(response: Response): 네트워크 통신이 성공한 경우, 해달 콜백이 호출된다.
      • onFailure(request: Request, e: IOException): 네트워크 통신이 실패한 경우, 해당 콜백이 호출된다.

Callback#onResponse 콜백이 호출된다는 것은 클라이언트에서 원하는 성공의 의미와는 다르다.
네트워크 통신이 실패하지 않고 성공했다는 의미로 서버에서 에러를 정상적으로 내려주는 경우도 네트워크 통신 성공으로 취급된다.
그러기 때문에 내부에서 response 값을 이용해서 클라이언트 기준에서 성공/실패 여부를 관리해서 처리해야한다.

 

HttpURLConnection보다 OkHttp가 나은 점

이전에 HttpURLConnection을 직접 사용해 네트워크 통신을 구현해보며 Low-level API가 어떤 식으로 동작하는지 확인했다.
이를 통해서 HTTP 통신의 기본 구조를 이해할 수 있었지만, 동시에 왜 이 방식이 실무에서 잘 사용되지 않는지도 자연스럽게 체감하게 됐다.

 

같은 GET 요청을 OkHttp로 구현해보며 HttpURLConnection과 비교했을 때, 개발자 입장에서 어떤 점이 개선되었는지를 정리해보면 다음과 같다.

1. 요청과 연결 개념의 분리

HttpURLConnection에서는 연결 객체 자체가 요청의 역할을 한다.

val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty(...)
  • 요청 정보가 mutable한 connection 객체에 흩어져서 설정된다.
  • "이것이 하나의 요청이다"라는 개념적 경계가 불명확하다.
  • 설정 순서에 따라 실수가 발생하기 쉽다.

반면 OkHttp는 Request라는 명확한 요청 모델을 제공한다.

val request = Request.Builder()
    .url("https://example.com")
    .get()
    .build()
  • URL, Method, Header, Body가 하나의 불변 객체(Request)로 묶인다.
  • 요청 자체가 값처럼 취급된다.
  • 요청 정의와 실행(Call)이 명확히 분리된다.

Request 모델 덕분에 요청을 '연결 설정 과정'이 아니라 '명세'로 다룰 수 있게 됐다.

2. 동기/비동기 처리 모델의 명확한 분리

HttpURLConnection은 비동기 처리를 직접 제공하지 않는다. 개발자가 직접 Thread/Executor를 만들어야 하고 동기/비동기 처리 경계가 코등상에 명확하지 않다.

반면 OkHttp는 명확한 선택지를 제공한다.

// 동기
client.newCall(request).execute()

// 비동기
client.newCall(request).enqueue(callback)
  • execute(): 호출한 스레드를 block
  • enqueue(): OkHttp 내부 스레드에서 비동기 실행 후 콜백 전달

3. Response 구조의 명확화

HttpUrlConnection에서는 응답을 다루기 위해서 여러 API를 조합해야 한다.

  • responseCode
  • getHeaderField()
  • inputStream/errorStream

OkHttp는 HTTP 응답을 하나의 Response 객체로 구조화된다.

response.code
response.headers
response.body
  • Status/Header/Body의 역할이 명확하다.
  • onResponse가 호출되더라도 HTTP 에러일 수 있음을 명시적으로 인지할 수 있다.
  • 응답 처리 흐름이 코드로 자연스럽게 드러난다.

HTTP 프로토콜 구조가 API 설계에 그대로 반영되어 있는 것 같다.

4. 확장성과 공통 관심사 분리

HttpURLConnection은 공통 로직을 넣기 어렵다.

  • 로깅, 인증 헤더 추가, 재시도, 캐싱 등 모든 요청마다 중복 코드가 생기기 쉽다.

OkHttp는 Interceptor를 통해 이를 구조적으로 해결한다.

// 인증 헤더 추가 관련 Interceptor 구현
class AuthInterceptor(
    private val tokenProvider: TokenProvider
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()

        val newRequest = original.newBuilder()
            .addHeader("Authorization", "Bearer ${tokenProvider.token}")
            .build()

        return chain.proceed(newRequest)
    }
}

// OkHttpClient에 추가
val client = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor(tokenProvider))
    .build()
  • 네트워크 요청 흐름에 공통 로직을 레이어로 삽입 가능하다.
  • 요청/응답을 가로채되, 구조를 깨지 않는다.

OkHttp는 네트워크 계층을 확장 가능한 파이프라인으로 다룰 수 있는 큰 장점이 있는 것 같다.

Comments