Android

Retrofit2

탱구리몬 2024. 4. 15. 04:30

오늘은 API Call에 쓰이는 Retrofit2를 학습해보고 정리를 하려고 한다.

사용중인 서버가 없기 때문에 jsonplaceholder를 사용해서 Test할 예정이다.

 

1. 기본적인 사용법

(0) build.gradle에 dependencies 추가

dependencies {
    ...
    
    implementation('com.squareup.retrofit2:retrofit:2.9.0')
    implementation('com.squareup.retrofit2:converter-gson:2.9.0')
}

 

(1) Retrofit 객체를 생성한다.

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    val BASE_URL = "https://jsonplaceholder.typicode.com"

    val client = Retrofit
        .Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun getInstance() = client
}

 

(2) 반환 데이터를 보고, DTO를 셋업한다.

// https://jsonplaceholder.typicode.com/posts/1 호출 시 얻을 수 있는 값
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

 

위는 api 호출 시 반환 예시인데 이걸 보고, key의 Name과 value의 Type으로 DTO를 만들어주면 된다.

- key의 Name은 필요하다면 다르게해도 무관하지만, 

data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String,
)

 

(3) Api interface 정의

import com.example.retrofittest.dto.Post
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path

interface TestApi {
    // [정적] post/1 호출
    @GET("posts/1")
    fun getPost1(): Call<Post>

    // [동적] post/number 호출
    @GET("posts/{number}")
    fun getPostNumber(
        @Path("number") number: Int
    ): Call<Post>
}

@GET 안에 {변수}를 통해서 동적으로 원하는 url을 호출할 수 있다.

이때는 @Path를 통해 number가 입력 받는 파라미터라고? 명시해야 하면 된다.

 

이제 준비는 모두 끝났다. 실제로 Activity에서 API를 호출해보자.

 

(4) Activity Api Call 로직 구현

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    // (1)번에서 정의한 Retrofit 객체를 가져온 뒤, TestApi를 연동한다?
    val api = RetrofitInstance.getInstance()
        .create(TestApi::class.java)

    // (3)번의 getPost1() 메소드 호출
    api.getPost1().enqueue(object: Callback<Post> {
        // 호출에 성공한 경우(단순히 호출의 성공 유무이므로, 404등 error가 발생한 경우도 포함된다)
        override fun onResponse(call: Call<Post>, response: Response<Post>) {
            // Response{protocol=h2, code=200, message=, url=https://jsonplaceholder.typicode.com/posts/1}
            Log.d("API1", response.toString())            // 통신 결과
            
            // Post(userId=1, id=1, title=..., body=...)
            Log.d("API1", response.body().toString())     // data
        }

        // 호출에 실패한 경우
        override fun onFailure(call: Call<Post>, t: Throwable) {
            Log.d("API1", "fail")
        }
    })

    // (3)번의 getPostNumber(number: Int) 메서드 호출
    api.getPostNumber(10).enqueue(object: Callback<Post> {
        // 호출에 성공한 경우(단순히 호출의 성공 유무이므로, 404등 error가 발생한 경우도 포함된다)
        override fun onResponse(call: Call<Post>, response: Response<Post>) {
            // number = 10 -> Response{protocol=h2, code=200, message=, url=https://jsonplaceholder.typicode.com/posts/10}
            // number = 1000 ->Response{protocol=h2, code=404, message=, url=https://jsonplaceholder.typicode.com/posts/1000}
            Log.d("API2", response.toString())            // 통신 결과

            // number = 10 -> Post(userId=1, id=10, title=..., body=...)
            // number = 1000 -> null
            Log.d("API2", response.body().toString())     // data
        }

        // 호출에 실패한 경우
        override fun onFailure(call: Call<Post>, t: Throwable) {
            Log.d("API2", "fail")
        }
    })
}

 

위와 같이 작성하면 간단하게 API를 호출할 수 있었다.

 

- 아래의 결과는 어떻게 나올까?

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    val api = RetrofitInstance.getInstance()
        .create(TestApi::class.java)

    (1..4).forEach {
        api.getPostNumber(it).enqueue(object: Callback<Post> {
            override fun onResponse(call: Call<Post>, response: Response<Post>) {
                Log.d("API$it", response.toString())
                Log.d("API$it", response.body().toString())
            }

            override fun onFailure(call: Call<Post>, t: Throwable) {
                Log.d("API$it", "fail")
            }
        })
    }
}

- API Call은 각 Task에 시간이 얼마나 걸릴지 모르기 때문에 당연히 비동기적으로 수행된다.

  따라서 API1 -> API2 -> API3 -> API4 이 순서로 결과값이 나오지 않는다.

  순차적으로 Api Call은 하지만, 서버로 부터 데이터를 먼저 받는 로직의 onResponse가 먼저 실행되기 때문이다.

 

- 이게 왜 문제가 될까? 만약 아래와 같이 A라는 Api Call을 하고, 반환되는 response값에 따라 B라는 Api Call을 해야한다면 어떻게 해야한다면 지금같이 그냥 나열해서는 안될 것이다.

 

- 단순하게 생각하면 A라는 Api Call의 결과인 onResponse 내부에서 B를 다시 Api Call 하도록 작성하면 될 것이다.

api.getPostNumber(1).enqueue(object: Callback<Post> {
    override fun onResponse(call: Call<Post>, response: Response<Post>) {
        Log.d("API1", response.body().toString())

        api.getPostNumber(2).enqueue(object: Callback<Post> {
            override fun onResponse(call: Call<Post>, response: Response<Post>) {
                Log.d("API2", response.body().toString())

                api.getPostNumber(3).enqueue(object: Callback<Post> {
                    override fun onResponse(call: Call<Post>, response: Response<Post>) {
                        Log.d("API3", response.body().toString())
                    }

                    override fun onFailure(call: Call<Post>, t: Throwable) {
                        Log.d("API3", "fail")
                    }
                })
            }

            override fun onFailure(call: Call<Post>, t: Throwable) {
                Log.d("API2", "fail")
            }
        })
    }

    override fun onFailure(call: Call<Post>, t: Throwable) {
        Log.d("API1", "fail")
    }
})

  하지만, 이런 로직이 점차 복잡해진다면 A -> B -> C -> D 코드를 계속해서 depth를 추가하며 넣어야 하고, 가독성도 매우 좋지않아 유지보수가 어려운 코드를 작성하게 된다.

 

그렇다면 어떻게 관리해야 효율적이고, 원하는 순서대로 api call을 수행할 수 있을까?

 

2. 간단한 Coroutine 예제 + ViewModelScope (CoroutineScope와 viewModelScope의 차이)

class SecondViewModel: ViewModel() {
    fun a() {
        CoroutineScope(Dispatchers.IO).launch {
            getData("Test - CoroutineScope")
        }
    }

    fun b() {
        viewModelScope.launch {
            getData("Test - viewModelScope")
        }
    }
    
    suspend fun getData(tag: String) {
        (1..15).forEach {
            delay(1000)
            Log.d(tag, it.toString())
        }
    }
}

CoroutineScope(a)와 viewModelScope(b) 는 어떤 차이가 있을까?

 

가정 1. getData 함수는 SecondView의 UI를 구성하기 위한 데이터를 가져오는 API Call 로직이다.

가정 2. a(), b()는 UI 구성을 위한 데이터 이므로 액티비티 진입 시 바로 호출된다.

 

사용자가 실수로 잘못 눌러서 secondActivity에 진입한다.

-> 이런 경우, 사용자는 바로 Back 버튼을 눌러 다시 이전화면으로 돌아가지만 CoroutineScope는 계속해서 돌아간다.

-> 즉, 불필요한 Task를 수행하고 있는 것이다. 이런 경우에는 별도로 코루틴을 중지해줘야 하는 코드를 추가해야한다.

 

코드 실행 결과

더보기

2024-04-14 17:01:28.516 10222-10222 Test - viewModelScope   com.example.retrofittest             D  1
2024-04-14 17:01:28.517 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  1
2024-04-14 17:01:29.520 10222-10222 Test - viewModelScope   com.example.retrofittest             D  2
2024-04-14 17:01:29.520 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  2
2024-04-14 17:01:30.521 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  3
2024-04-14 17:01:30.523 10222-10222 Test - viewModelScope   com.example.retrofittest             D  3
2024-04-14 17:01:31.523 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  4
2024-04-14 17:01:31.524 10222-10222 Test - viewModelScope   com.example.retrofittest             D  4

/ / 사용자가 Back Button을 눌러 이전 화면으로 돌아감.

2024-04-14 17:01:32.527 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  5
2024-04-14 17:01:33.529 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  6
2024-04-14 17:01:34.532 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  7
2024-04-14 17:01:35.534 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  8
2024-04-14 17:01:36.538 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  9
2024-04-14 17:01:37.543 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  10
2024-04-14 17:01:38.547 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  11
2024-04-14 17:01:39.553 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  12
2024-04-14 17:01:40.555 10222-10274 Test - CoroutineScope   com.example.retrofittest             D  13
2024-04-14 17:01:41.560 10222-10276 Test - CoroutineScope   com.example.retrofittest             D  14
2024-04-14 17:01:42.564 10222-10276 Test - CoroutineScope   com.example.retrofittest             D  15

 

SecondActivity가 destroy 되어 더이상 api로 부터 데이터를 받아올 필요가 없지만, 계속해서 가져오고 있는 CoroutineScope를 볼 수 있다.

 

-> viewModelScope는 SecondActivity가 destroy이 되고, viewModel이 필요 없어지면 자동으로 실행중인 코루틴을 취소한다.

 

최종적으로는 다른 api를 호출한 뒤 glide( string -> url -> ImageView에 적용)를 사용하여 ReclcyerView에 띄워봤다.

https://github.com/rlaxodud214/NBC-Challenge-Week4/tree/feat/Api-Call

 

 

 

interface SearchApi {
    @GET("v2/search/image")
    // @Headers("Authorization: ${BuildConfig.KAKAO_API_KEY}")
    suspend fun getSearchImage(
        @Header("Authorization") accessToken: String,
        @Query("query") query: String,
    ): KaKaoSearchResponse

    @GET("v2/search/image")
    // @Headers("Authorization: ${BuildConfig.KAKAO_API_KEY}")
    fun getSearchImageCall(
        @Header("Authorization") accessToken: String,
        @Query("query") query: String,
    ): Call<KaKaoSearchResponse>
}

위 로직을 이용하여 kakao API를 호출했지만, Head의 인증키 값이 누락되었다는 메세지가 출력된다.

fun setSearchImageDataCall() {
    viewModelScope.launch {
        val keyWord = _searchWord.value.toString()

         repository.getSearchImageDataCall(keyWord).enqueue(object: Callback<KaKaoSearchResponse> {
             override fun onResponse(
                 call: Call<KaKaoSearchResponse>,
                 response: Response<KaKaoSearchResponse>,
             ) {
                     Log.d("API Call", "response : ${response}")
                     Log.d("API Call", "headers() : ${call.request().headers()}")
                     Log.d("API Call", "header[Authorization] : ${call.request().headers("Authorization")}")
                     Log.d("API Call", "ContentType : ${call.request().headers("Content-Type")}")
             }

             override fun onFailure(call: Call<KaKaoSearchResponse>, t: Throwable) {
                 TODO("Not yet implemented")
             }
         })
    }
}

 

 

2024-04-15 03:20:00.315 15469-15469 API Call                com.example.retrofittest             D  KaKaoSearchResponse(meta=Meta(totalCount=-1, pageableCount=-1, isEnd=false), documents=[ImageDocument(title=, url=, datetime=2011-04-09T09:37:54.000+09:00), ImageDocument(title=, url=, datetime=2023-01-18T11:24:58.000+09:00), ImageDocument(title=, url=, datetime=2023-11-10T11:06:42.000+09:00), ImageDocument(title=, url=, datetime=2024-02-16T13:18:18.000+09:00), ImageDocument(title=, url=, datetime=2011-05-17T12:05:32.000+09:00), ImageDocument(title=, url=, datetime=2022-11-21T21:54:39.000+09:00), ImageDocument(title=, url=, datetime=2020-03-23T17:23:46.000+09:00), ImageDocument(title=, url=, datetime=2023-11-02T02:56:23.000+09:00), ImageDocument(title=, url=, datetime=2023-10-09T12:55:25.000+09:00), ImageDocument(title=, url=, datetime=2020-02-08T19:58:45.000+09:00), ImageDocument(title=, url=, datetime=2017-04-22T20:23:00.000+09:00), ImageDocument(title=, url=, datetime=2023-06-19T14:26:23.000+09:00), ImageDocument(title=, url=, datetime=2024-04-04T13:56:28.000+09:00), ImageDocument(title=, url=, datetime=2023-02-27T23:21:37.000+09:00), ImageDocument(title=, url=, datetime=2021-08-14T20:59:12.000+09:00), ImageDocument(title=, url=, datetime=2023-06-20T07:30:39.000+09:00), ImageDocument(title=, url=, datetime=2016-12-13T09:00:06.000+09:00), ImageDocument(title=, url=, datetime=2023-09-08T07:57:29.000+09:00), ImageDocument(title=, url=, datetime=2023-04-23T19:02:34.000+09:00), ImageDocument(title=, url=, datetime=2024-03-29T15:25:59.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T13:24:31.000+09:00), ImageDocument(title=, url=, datetime=2023-09-15T16:12:09.000+09:00), ImageDocument(title=, url=, datetime=2020-12-14T04:32:24.000+09:00), ImageDocument(title=, url=, datetime=2024-03-07T11:06:33.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T09:00:14.000+09:00), ImageDocument(title=, url=, datetime=2023-07-17T23:53:00.000+09:00), ImageDocument(title=, url=, datetime=2023-10-14T12:25:44.000+09:00), ImageDocument(title=, url=, datetime=2023-03-05T10:49:17.000+09:00), ImageDocument(title=, url=, datetime=2017-05-02T18:37:23.000+09:00), ImageDocument(title=, url=, datetime=2023-09-13T15:04:21.000+09:00), ImageDocument(title=, url=, datetime=2023-06-17T10:58:03.000+09:00), ImageDocument(title=, url=, datetime=2023-09-02T01:32:08.000+09:00), ImageDocument(title=, url=, datetime=2021-07-02T13:46:03.000+09:00), ImageDocument(title=, url=, datetime=2023-03-18T09:00:30.000+09:00), ImageDocument(title=, url=, datetime=2021-12-01T14:36:01.000+09:00), ImageDocument(title=, url=, datetime=2023-03-19T10:09:55.000+09:00), ImageDocument(title=, url=, datetime=2017-11-08T20:52:10.000+09:00), ImageDocument(title=, url=, datetime=2023-07-07T07:25:35.000+09:00), ImageDocument(title=, url=, datetime=2022-04-10T22:15:43.000+09:00), ImageDocument(title=, url=, datetime=2023-06-04T08:00:37.000+09:00), ImageDocument(title=, url=, datetime=2021-09-11T18:38:52.000+09:00), ImageDocument(title=, url=, datetime=2024-03-07T11:06:33.000+09:00), ImageDocument(title=, url=, datetime=2019-07-22T11:34:24.000+09:00), ImageDocument(title=, url=, datetime=2024-03-02T18:35:25.000+09:00), ImageDocument(title=, url=, datetime=2023-11-10T23:02:11.000+09:00), ImageDocument(title=, url=, datetime=2014-08-20T10:50:18.000+09:00), ImageDocument(title=, url=, datetime=2024-03-09T15:52:41.000+09:00), ImageDocument(title=, url=, datetime=2023-05-15T11:44:29.000+09:00), ImageDocument(title=, url=, datetime=2023-11-14T17:36:30.000+09:00), ImageDocument(title=, url=, datetime=2023-02-26T05:55:02.000+09:00), ImageDocument(title=, url=, datetime=2019-07-10T21:42:53.000+09:00), ImageDocument(title=, url=, datetime=2023-08-17T08:00:59.000+09:00), ImageDocument(title=, url=, datetime=2023-03-15T23:23:04.000+09:00), ImageDocument(title=, url=, datetime=2023-03-11T05:55:45.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T13:24:31.000+09:00), ImageDocument(title=, url=, datetime=2021-05-14T20:52:03.000+09:00), ImageDocument(title=, url=, datetime=2023-07-26T20:45:51.000+09:00), ImageDocument(title=, url=, datetime
2024-04-15 03:20:00.329 15469-15469 API Call                com.example.retrofittest             D  response : Response{protocol=h2, code=200, message=, url=https://dapi.kakao.com/v2/search/image?query=%EA%B3%A0%EC%96%91%EC%9D%B4}
2024-04-15 03:20:00.330 15469-15469 API Call                com.example.retrofittest             D  headers() : Authorization: KakaoAK {인증키}
2024-04-15 03:20:00.330 15469-15469 API Call                com.example.retrofittest             D  header[Authorization] : [KakaoAK {인증키}]
2024-04-15 03:20:00.331 15469-15469 API Call                com.example.retrofittest             D  ContentType : []

 

headers 출력 시 "Authorization: KakaoAK {인증키}"로 제대로 출력되지만, 제대로 인증되지 않았다는 아래와 같은 오류가 발생한다.

 

Response의 url 접속 시 출력되는 오류 문구. 하지만, status Code는 200이다.

 

이유가 뭘까,,, 한 줄만 수정하면 될 것 같은데 해결이 되지 않고있다,,,

 

- 혹시 몰라 postman 에서 key 검증을 해봤다.

그 결과 key는 이상이 없었고, 아마 내가 코드에서 header를 제대로 전달하지 못한 것으로 확인된다.

{
    "documents": [
        {
            "collection": "cafe",
            "datetime": "2011-04-09T09:37:54.000+09:00",
            "display_sitename": "Daum카페",
            "doc_url": "https://cafe.daum.net/withemilerin/j4oo/156",
            "height": 117,
            "width": 150
        },
        ...
     ],
    "meta": {
        "is_end": false,
        "pageable_count": 3978,
        "total_count": 29885745
    }
}
 

 

 

 

object RetrofitInstance {
    val BASE_URL: String
        get() = BuildConfig.KAKAO_API_URL

    val client = initInstance(BASE_URL)

    fun getInstance() = client

    fun initInstance(url: String) = Retrofit
        .Builder()
        .baseUrl(url)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
}

 

OkHttpClient를 사용해서 Header를 담는 방법이 있어 위의 코드를 아래로 수정해보았다.

 

object RetrofitInstance {
    private val INSTANCE = initInstance()
    fun getInstance() = INSTANCE

    private fun initInstance(): Retrofit {
        val client = OkHttpClient.Builder()
            .addInterceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                    .header("Authorization", BuildConfig.KAKAO_API_KEY)
                    .build()
                chain.proceed(request)
            }
            .build()

        return Retrofit.Builder()
            .baseUrl(BuildConfig.KAKAO_API_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

 

이렇게 수정하니 디버그 로그가 아래와 같이 바꼈다. Header는 비어있는 것 같아 오히려 전의 코드가 좀 더 근접하다고 생각된다.

2024-04-15 03:59:56.044 17245-17245 API Call                com.example.retrofittest             D  response : Response{protocol=h2, code=200, message=, url=https://dapi.kakao.com/v2/search/image?query=%EA%B3%A0%EC%96%91%EC%9D%B4}
2024-04-15 03:59:56.053 17245-17245 API Call                com.example.retrofittest             D  headers() : 
2024-04-15 03:59:56.053 17245-17245 API Call                com.example.retrofittest             D  header[Authorization] : 
2024-04-15 03:59:56.053 17245-17245 API Call                com.example.retrofittest             D  ContentType : []
2024-04-15 03:59:56.055 17245-17245 API Call                com.example.retrofittest             D  KaKaoSearchResponse(meta=Meta(totalCount=-1, pageableCount=-1, isEnd=false), documents=[ImageDocument(title=, url=, datetime=2011-04-09T09:37:54.000+09:00), ImageDocument(title=, url=, datetime=2023-01-18T11:24:58.000+09:00), ImageDocument(title=, url=, datetime=2023-11-10T11:06:42.000+09:00), ImageDocument(title=, url=, datetime=2024-02-16T13:18:18.000+09:00), ImageDocument(title=, url=, datetime=2011-05-17T12:05:32.000+09:00), ImageDocument(title=, url=, datetime=2022-11-21T21:54:39.000+09:00), ImageDocument(title=, url=, datetime=2020-03-23T17:23:46.000+09:00), ImageDocument(title=, url=, datetime=2023-11-02T02:56:23.000+09:00), ImageDocument(title=, url=, datetime=2023-10-09T12:55:25.000+09:00), ImageDocument(title=, url=, datetime=2020-02-08T19:58:45.000+09:00), ImageDocument(title=, url=, datetime=2017-04-22T20:23:00.000+09:00), ImageDocument(title=, url=, datetime=2023-06-19T14:26:23.000+09:00), ImageDocument(title=, url=, datetime=2024-04-04T13:56:28.000+09:00), ImageDocument(title=, url=, datetime=2023-02-27T23:21:37.000+09:00), ImageDocument(title=, url=, datetime=2021-08-14T20:59:12.000+09:00), ImageDocument(title=, url=, datetime=2023-06-20T07:30:39.000+09:00), ImageDocument(title=, url=, datetime=2016-12-13T09:00:06.000+09:00), ImageDocument(title=, url=, datetime=2023-09-08T07:57:29.000+09:00), ImageDocument(title=, url=, datetime=2023-04-23T19:02:34.000+09:00), ImageDocument(title=, url=, datetime=2024-03-29T15:25:59.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T13:24:31.000+09:00), ImageDocument(title=, url=, datetime=2023-09-15T16:12:09.000+09:00), ImageDocument(title=, url=, datetime=2020-12-14T04:32:24.000+09:00), ImageDocument(title=, url=, datetime=2024-03-07T11:06:33.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T09:00:14.000+09:00), ImageDocument(title=, url=, datetime=2023-07-17T23:53:00.000+09:00), ImageDocument(title=, url=, datetime=2023-10-14T12:25:44.000+09:00), ImageDocument(title=, url=, datetime=2023-03-05T10:49:17.000+09:00), ImageDocument(title=, url=, datetime=2017-05-02T18:37:23.000+09:00), ImageDocument(title=, url=, datetime=2023-09-13T15:04:21.000+09:00), ImageDocument(title=, url=, datetime=2023-06-17T10:58:03.000+09:00), ImageDocument(title=, url=, datetime=2023-09-02T01:32:08.000+09:00), ImageDocument(title=, url=, datetime=2021-07-02T13:46:03.000+09:00), ImageDocument(title=, url=, datetime=2023-03-18T09:00:30.000+09:00), ImageDocument(title=, url=, datetime=2021-12-01T14:36:01.000+09:00), ImageDocument(title=, url=, datetime=2023-03-19T10:09:55.000+09:00), ImageDocument(title=, url=, datetime=2017-11-08T20:52:10.000+09:00), ImageDocument(title=, url=, datetime=2023-07-07T07:25:35.000+09:00), ImageDocument(title=, url=, datetime=2022-04-10T22:15:43.000+09:00), ImageDocument(title=, url=, datetime=2023-06-04T08:00:37.000+09:00), ImageDocument(title=, url=, datetime=2021-09-11T18:38:52.000+09:00), ImageDocument(title=, url=, datetime=2024-03-07T11:06:33.000+09:00), ImageDocument(title=, url=, datetime=2019-07-22T11:34:24.000+09:00), ImageDocument(title=, url=, datetime=2024-03-02T18:35:25.000+09:00), ImageDocument(title=, url=, datetime=2023-11-10T23:02:11.000+09:00), ImageDocument(title=, url=, datetime=2014-08-20T10:50:18.000+09:00), ImageDocument(title=, url=, datetime=2024-03-09T15:52:41.000+09:00), ImageDocument(title=, url=, datetime=2023-05-15T11:44:29.000+09:00), ImageDocument(title=, url=, datetime=2023-11-14T17:36:30.000+09:00), ImageDocument(title=, url=, datetime=2023-02-26T05:55:02.000+09:00), ImageDocument(title=, url=, datetime=2019-07-10T21:42:53.000+09:00), ImageDocument(title=, url=, datetime=2023-08-17T08:00:59.000+09:00), ImageDocument(title=, url=, datetime=2023-03-15T23:23:04.000+09:00), ImageDocument(title=, url=, datetime=2023-03-11T05:55:45.000+09:00), ImageDocument(title=, url=, datetime=2022-11-28T13:24:31.000+09:00), ImageDocument(title=, url=, datetime=2021-05-14T20:52:03.000+09:00), ImageDocument(title=, url=, datetime=2023-07-26T20:45:51.000+09:00), ImageDocument(title=, url=, datetime

 

그래도 오류는 해결되지 않았다 ㅎㅎㅎㅎㅎㅎㅎ,,,,,,,,,, 내일 좀 더 보자..

 

다시 생각해보니, 그냥 url로 접속하면 당연히 header가 없기 때문에 위 처럼 오류가 발생하는 게 당연하다,,,,

 

data class ImageDocument(
    val collection: String,
    val url: String,
    val datetime: String,
)

데이터를 출력할 때, datetime이 제대로 나오는 걸 뒤는 게 발견하고, 다시 Model을 보니 제대로 반환 파라미터를 받고 있지 않아 model을 아래와 같이 수정하니 데이터가 제대로 출력되는 걸 볼 수 있었다,,,,,,

-> Header는 제대로 전달이 되고 있었다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ,,,,,, 

 

data class ImageDocument(
    // 만약 변수명을 다르게 쓰고 싶다면 SerializedName을 사용해서 명시해주면 된다.
    @SerializedName("collection")
    val collection: String,
    @SerializedName("datetime")
    val datetime: String,
    val display_sitename: String,
    val doc_url: String,
    val height: Int,
    val width: Int,
    val image_url: String,
    val thumbnail_url: String,
)

 

api call 결과 code가 200인 걸 좀 더 생각해봤으면 삽질을 덜했을텐데,,, 아쉽다 ㅎㅎㅎㅎ

 

-> 내일은 간단하게 RecyclerView를 적용하고 프로젝트를 마치자

-> 구현완료

 

시연 영상

 

gitHub : https://github.com/rlaxodud214/NBC-Challenge-Week4