Android - Retrofit 없이 Http 통신하기

MoonHwi Han·2024년 12월 27일

Retrofit?

Retrofit이란 Android에서 Http통신을 쉽게 할 수 있도록 만들어진 Library이다.
어노테이션과 함께 Interface를 작성하면, 그 내용을 토대로 객체를 만들어 반환해 준다.
만약, GitHub에서 이슈를 가져오는 코드를 짠 다면, 아래와 같이 짤 수 있다.

internal interface IssueApiRetrofit {
    @GET("issues")
    suspend fun getIssue(
        @Query("page") page: Int,
        @Query("per_page") perPage: Int = 100,
    ): Response<List<IssueDto>>
}

GET, POST, UPDATE, DELETE 어노테이션으로 API 통신 함수를 만들어 주고
error code, body, errorBody를 손 쉽게 가져올 수 있는 Response와 Call 객체를 제공해 준다.


이렇게 손쉽게 만들 수 있는 Retrofit.
하지만 "Retrofit을 왜 사용하는가?" 에 대한 대답을 온전히 할 수 없을 것 같다.
대답을 하려면 Retrofit이 없는 환경을 먼저 알아야 하는게 좋다고 생각했다.

따라서 나는 Retrofit 없이 HttpConnection과 JsonParser를 이용하여 Http통신을 해보려 한다.

HttpConnection 만들기

먼저 HttpConnection을 만드려면 URL을 만들어야 한다.
그럼 URL의 구조에 대해 알아야 하는데, URL의 구조를 위키에서는 아래와 같이 표현하고 있다.

이 구조를 보고 자주 사용하는 패턴으로 바꾼다면

https(또는 http)://authority/path?query

이 정도로 볼 수 있다.

그럼 Retrofit과 비교했을 때, 위의 URL은 어떻게 대응될까?



https와 authority

먼저 https(또는 http)://authority는 Retrofit 객체를 만들 때 들어가는 baseUrl로 볼 수 있다.

이 때 BaseUrl에는 통신 규약(https, http)와 authority가 들어간다.



Path

그 다음 path는 GET, POST, UPDATE, DELETE에 들어가는 값이나

@GET("issues") //Path 값이 들어간다.
suspend fun getIssue(
    @Query("page") page: Int,
    @Query("per_page") perPage: Int = 100,
): Response<List<IssueDto>>

@Path 어노테이션으로 대응될 수 있다.

@PATCH("issues/{issue_number}")
suspend fun updateIssue(
    @Path("issue_number") issueNumber: Long,
    @Body issueBody: IssueBody
): Response<IssueDto>

Query

마지막으로 쿼리는 @Query라는 어노테이션으로 대체될 수 있다.

@GET("issues")
suspend fun getIssue(
    @Query("page") page: Int, //query 대체
    @Query("per_page") perPage: Int = 100,
): Response<List<IssueDto>>

즉, GitHub야, Page가 1인, issue를 줘. 라고 해야 한다면

https://github.com/issue?page=1

이렇게 URL을 보내면 된다.

이제 GET, POST와 Header를 붙여 요청하는 함수를 만들어 보자.

HttpConnection함수 작성

위의 내용을 토대로 BaseUrl, Path, Qeury를 받는 함수를 만들자.


const val GITHUB_ACCEPT_KEY = "Accept"
const val GITHUB_ACCEPT_VALUE = "application/vnd.github+json"

const val GITHUB_AUTH_KEY = "Authorization"
const val GITHUB_AUTH_VALUE_BEARER = "Bearer ${BuildConfig.TOKEN}"

const val GITHUB_API_VERSION_KEY = "X-GitHub-Api-Version"
const val GITHUB_API_VERSION_VALUE = "2022-11-28"

private suspend fun getConnection(
    method: String,
    baseUrl: String,
    endPoint: String,
    vararg queries: String
): HttpURLConnection {

	//url 생성
    val urlString = baseUrl + endPoint
    
    //query 생성
    val queryString = if (queries.isEmpty()) "" else "?${queries.joinToString("&")}"
    
    //URL을 만들고 Connection 생성
    val url = URL(urlString + queryString)
    val conn = withContext(Dispatchers.IO) {
        url.openConnection()
    } as HttpURLConnection
	
    //GET, POST, UPDATE, DELETE
    conn.requestMethod = method
    
    //TIME_OUT 제한 설정
    conn.connectTimeout = TIME_OUT_MILLIS
    conn.readTimeout = TIME_OUT_MILLIS
        
    //Header 붙이기
    conn.setRequestProperty(Header.GITHUB_AUTH_KEY, Header.GITHUB_AUTH_VALUE_BEARER)
    conn.setRequestProperty(Header.GITHUB_ACCEPT_KEY, Header.GITHUB_ACCEPT_VALUE)
    conn.setRequestProperty(Header.GITHUB_API_VERSION_KEY, Header.GITHUB_API_VERSION_VALUE)
    return conn
}

이제 에러를 핸들링 할 함수를 만들고

private suspend fun <T : Any> handleResponse(
    connSupplier: suspend () -> HttpURLConnection,
    supplier: (String) -> T
): ApiResult<T> {
    return try {
        val conn = connSupplier()
        if (conn.responseCode == HttpURLConnection.HTTP_OK) {
            val jsonString = getResponseString(conn)
            conn.disconnect()
            val info = supplier(jsonString)
            ApiResult.Succeed(conn.responseCode, info)
        } else {
            ApiResult.Error(conn.responseCode, conn.responseMessage)
        }
    } catch (e: Exception) {
        e.printStackTrace()
        ApiResult.Error(InternalErrorCode.PARSING_FAIL.code, e.message)
    }
}

아래와 같이 호출해 주면 된다.

override suspend fun getIssue(page: Int): ApiResult<List<IssueDto>> {
    return handleResponse({
        getConnection(
            "GET",
            END_POINT_ISSUES,
            "page=$page"
        )
    }) { jsonString ->
        //json parsing하기
    }
}

Json 직접 파싱하기.

Json을 파싱하려면 string을 JsonObject로 만든 후, 필드의 이름을 string으로 넘겨주면 가져올 수 있다.

만약 array형태로 되어있다면, JsonArray를 사용하면 된다.

fun parseIssueDto(jsonObject: JSONObject): IssueDto {

    //label
    val labels = parseLabelDtoList(jsonObject.getJSONArray("labels"))
    val assignees = parseUserDtoList(jsonObject.getJSONArray("assignees"))
    val url = jsonObject.getString("url")
    val repository_url = jsonObject.getString("repository_url")
    val comments_url = jsonObject.getString("comments_url")
    val events_url = jsonObject.getString("events_url")
    val html_url = jsonObject.getString("html_url")
    val id = jsonObject.getLong("id")
    val node_id = jsonObject.getString("node_id")
    val number = jsonObject.getLong("number")
    val title = jsonObject.getString("title")
    val user = parseUserDto(jsonObject.getJSONObject("user"))
    val state = jsonObject.getString("state")
    val locked = jsonObject.getBoolean("locked")
    val body = jsonObject.getString("body")
    val mileStoneDto = jsonObject.optJSONObject("milestone")?.let { parseMileStoneDto(it) }
    val created_at = jsonObject.getString("created_at")
    val closed_at = jsonObject.getString("closed_at")
    val updated_at = jsonObject.getString("updated_at")

    return IssueDto(
        url = url,
        repositoryUrl = repository_url,
        commentsUrl = comments_url,
        eventsUrl = events_url,
        htmlUrl = html_url,
        id = id,
        nodeId = node_id,
        number = number,
        title = title,
        user = user,
        labels = labels,
        state = state,
        locked = locked,
        assignees = assignees,
        body = body,
        milestone = mileStoneDto,
        createdAt = created_at,
        closedAt = closed_at,
        updatedAt = updated_at,
    )

}

이렇게 하면 Retrofit 없이 Http통신을 할 수 있다.

느낀점

이런 노가다를 하며... 느낀점이 있다.

1. 실수할 여지가 많다.

GET, POST, UPDATE, DELETE 메소드를 String으로 넘겨야 하고,
또한 Query도 String으로 넘겨 주어야 한다.

String으로 넘겼을 때 단점은 다들 알다시피 오타가 날 수 있다는 것이다.

따라서 오타가 났는지, 아닌지 조심하며 작성해야 한다.

또한 Query를 넘길 때, Query가 없다면 물음표(?)를 빼줘야 하고, Query가 있을 때는 넣어야 한다는 귀찮음도 있었다.

2. Json Parsing이 어렵다.

이건 HttpConnection으로 데이터를 가져온 후, Kotlin Serialization이나 Gson으로 대체하여 Parsing할 수 있지만, 이번에 Retrofit 사용 안하는 김에 똑같이 사용하지 않고 JsonObject로 해 보았다.

이 때의 단점은 앞선 오타의 단점과, 하나하나 JsonObject로 접근해 String, Int등으로 가져와야 하는 번거로움이 있었다. 실수할 여지도 많았으며, 필드가 많으면 잘못 가져올 수도 있다는 단점도 있었다.

profile
기초 튼튼 개발자

0개의 댓글