๐Ÿ’์„œ๋ฒ„ ์‘๋‹ต Cherry Pick!๐Ÿ’ (OkHttp Interceptor)

MJ Studio
MJ Studio
Published in
14 min readJan 13, 2020

๋Œ€๋‹ค์ˆ˜์˜ ๊ทœ๋ชจ๊ฐ€ ์žˆ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜๋“ค์€ ์ž์ฒด ์„œ๋ฒ„๋ฅผ ์šด์˜ํ•˜๋ฉฐ REST API๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‘” ์„œ๋ฒ„ ํ†ต์‹ ๋“ค์€ ๋ฐ์ดํ„ฐ๋ฅผ JSON์˜ ํ˜•ํƒœ๋กœ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ์ด๋Ÿฐ ์„œ๋ฒ„๋“ค๊ณผ REST API๋ฅผ ์ด์šฉํ•œ ํ†ต์‹ ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ Retrofit, OkHttp, Gson๊ณผ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ์ฏค ๋“ค์–ด๋ณด์•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉํ•ด๋ณด์•˜์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด ํฌ์ŠคํŒ…์€ ์ด๋Ÿฌํ•œ ํ†ต์‹  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์„œ๋ฒ„์˜ JSON ์‘๋‹ต ์ค‘, OkHttp์˜ Interceptor๋ฅผ ์ด์šฉํ•˜์—ฌ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„๋งŒ ๋ฝ‘์•„์„œ ๋ณ„๋„์˜ ํŒŒ์‹ฑ ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉด Retrofit ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•˜๋Š” ๊ณผ์ •๊ณผ ์‚ฌ์šฉํ•˜๋Š” ๊ณผ์ •์ด ๋งค์šฐ ํŽธ๋ฆฌํ•ด์ง‘๋‹ˆ๋‹ค!๐Ÿ˜€

Problem : Boilerplate code๐Ÿ˜ซ

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„œ๋ฒ„์˜ ์‘๋‹ต์„ ์ƒ๊ฐํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ์ •์ƒ์ ์ธ ์‘๋‹ต์ด ์„ฑ๊ณตํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

{
"status": 200,
"success": true,
"message": "ํŽ€๋”ฉ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต",
"data": [
{
"funding_idx": 95,
"user_idx": 12,
"store_idx": 1,
"funding_money": 2400,
"reward_percent": 0,
"funding_time": "2019-12-30T04:44:55.000Z",
"reward_money": 0
},
{
"funding_idx": 87,
"user_idx": 12,
"store_idx": 6,
"funding_money": 2400,
"reward_percent": 0,
"funding_time": "2019-12-29T11:57:39.000Z",
"reward_money": 0
},
....
]
}

๋‹ค์Œ์€ ์„œ๋ฒ„์—์„œ ์ •์ƒ์ ์ธ ์‘๋‹ต์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

{
"status": 400,
"success": false,
"message": "ํ•ด๋‹นํ•˜์ง€ ์•Š๋Š” userIdx๊ฐ’์ž…๋‹ˆ๋‹ค."
}

์šฐ๋ฆฌ๊ฐ€ ์ด๋Ÿฌํ•œ ์„œ๋ฒ„์˜ ์‘๋‹ต์„ Retrofit์˜ GsonConverterFactory๋ฅผ ์ด์šฉํ•ด์„œ ๋ฐ›์œผ๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”? ์ด์ƒ์ ์ธ ์ƒํ™ฉ์ด๋ผ๋ฉด ์šฐ๋ฆฌ๋Š” ๋ณดํ†ต data์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ์ฒด๋“ค์˜ ๋ฆฌ์ŠคํŠธ๋งŒ ๋ฐ›์•„์™€์„œ UI์— ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”ํ‹€๋ฆฐ์˜ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋ฅผ ๋จผ์ € ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ Funding์ด๋ผ๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋กœ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

Funding ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์‹ค์ƒ ํ•„์š” ์—†์ง€๋งŒ status, success, message๊ณผ ์‘๋‹ต์ด ์‹คํŒจํ•  ์‹œ์— data๊ฐ€ ์˜ค์ง€ ์•Š์œผ๋ฏ€๋กœ data๋ฅผ nullable๋กœ ์ •์˜ํ•œ ResponseWrapper ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋ฅผ ํ•˜๋‚˜ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„์˜ ์‘๋‹ต์ด ๋ชจ๋‘ ์œ„์™€ ๊ฐ™์€ ํ˜•์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์š”์ฒญ์˜ ์‘๋‹ต์— ๊ณ„์† ์“ฐ์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

ResponseWrapper ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค

๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด Retrofit ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@GET("mypage/fundlist")
fun listFundingHistories() : Call<ResponseWrapper<List<Funding>>>

๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ์ƒ์—์„œ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Dirty Response Wrap!

์„œ๋ฒ„๋ž‘ ํ†ต์‹  ํ•œ๋ฒˆ ํ•˜๊ฒ ๋‹ค๋Š”๋ฐ ๊ณจ ๋•Œ๋ฆฌ๋Š” ์ฝ”๋“œ๊ฐ€ ์•„๋‹ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค๐Ÿ˜ซ. ์‹ค์ œ๋กœ ์กฐ๊ธˆ ๊ณผ์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์œ„์™€ ๊ฐ™์ด ๋งŒ๋“ค์—ˆ์ง€๋งŒ, ์™„์ „ํžˆ ๋น„ํ•ฉ๋ฆฌ์ ์ธ ์ฝ”๋“œ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ฃผ์„์œผ๋กœ ์ˆซ์ž๋ฅผ ๋‹ฌ์•„๋‘” ๋„คํŠธ์›Œํฌ ์š”์ฒญ์˜ ์‹คํŒจ ์ผ€์ด์Šค์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ์„œ๋ฒ„์™€ ํ†ต์‹ ์ด ์•„์˜ˆ ์‹คํŒจํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ๋‹ซํ˜€์žˆ์„ ์ˆ˜๋„ ์žˆ๊ณ , 404 Not Found ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„ ์ˆ˜๋„ ์žˆ๊ณ  TimeOut ์ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. Response์—์„œ Code๊ฐ€ 200 ์ด์ƒ 300๋ฏธ๋งŒ์ด ์•„๋‹Œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. REST API์—์„œ ๋…ผ๋ฆฌ์ ์œผ๋กœ ์„ฑ๊ณต ์‘๋‹ต์œผ๋กœ ๊ฐ„์ฃผ๋˜๋Š” Code์˜ ๋ฒ”์ฃผ ์ธ์ง€๋กœ ์‘๋‹ต์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
  3. ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฐœ๋ฐœํ•œ ๋กœ์ง์ƒ์—์„œ ์‹คํŒจํ–ˆ์„ ๋•Œ์ž…๋‹ˆ๋‹ค. ๋ณดํ†ต 2๋ฒˆ์ด ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ 3๋ฒˆ์ด ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž์˜ ์‹ค์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ๋“œ๋ญ…๋‹ˆ๋‹ค.
  4. ์„œ๋ฒ„์—์„œ๋„ ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๋ณดํ†ต ์‘๋‹ต์—์„œ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ ์‘๋‹ต ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š” ์—†์ด ์ฝ”๋“œ๋งŒ 200~300์œผ๋กœ ์˜ค๋Š” ์‘๋‹ต๋„ ์ข…์ข… ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ ํƒ์ ์ธ ๊ฒ€์‚ฌ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. ๋ณดํ†ต 2๋ฒˆ์ด ์„ฑ๊ณตํ–ˆ์„ ๋•Œ 4๋ฒˆ์ด ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ๋“œ๋ญ…๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ 1,2๋ฒˆ์€ Retrofit ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฒฐ์ •ํ•ด์ฃผ๋Š” ์‹คํŒจ ์ƒํ™ฉ์ด๊ณ , 3,4๋ฒˆ์€ ์„œ๋ฒ„ ํ”„๋กœํ† ์ฝœ์ด status, success, message ๊ทธ๋ฆฌ๊ณ  data๋กœ ํ•œ๋ฒˆ ์‹ธ์—ฌ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์–ด์ฉ” ์ˆ˜ ์—†์ด ํ•œ ๋ฒˆ ๋” ๊ฒ€์‚ฌํ•ด์•ผ ํ•˜๋Š” ์‹คํŒจ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์ด๋ฅผ RxJava๋‚˜ Kotlin Coroutine ๊ทธ๋ฆฌ๊ณ  ResponseWrapper์— ๋Œ€ํ•œ ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜๋ฉด ์ข€ ๋” ๊ฐ„ํŽธํ•˜๊ฒŒ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, ์–ด์ฐŒ ๋˜์—ˆ๋“  ์šฐ๋ฆฌ๊ฐ€ ์„œ๋ฒ„์˜ ๊ทœ๊ฒฉ์— ๋งž์ถฐ์ง„ ์‘๋‹ต ๋•Œ๋ฌธ์— ResponseWrapper๋ž€ ๋…€์„์„ ํ•œ ๊บผํ’€ ๋ฒ—๊ฒจ๋‚ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์€ ๋‹ค๋ฆ„์—†์Šต๋‹ˆ๋‹ค.

์ €๋Š” ๋ณดํ†ต ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์œ„์™€ ๊ฐ™์ด ์‘๋‹ต์„ ๋ณด๋‚ด์ฃผ๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. status์™€ success๋ผ๋Š” ์‘๋‹ต ๊ฐ’์€ ์‘๋‹ต์˜ ์ฝ”๋“œ๋กœ ํŒ๋‹จ์„ํ•˜๋ฉด ๋˜๋Š” ์ผ์ด๊ณ  message๋Š” ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋„์šฐ๋Š” ๋ฐ ๋‹น์žฅ์€ ์œ ์šฉํ•˜๊ฒ ์ง€๋งŒ, ์•ฑ์ด ํ™•์žฅ๋˜๊ณ  ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ์ง€์›ํ•˜๊ฒŒ ๋˜๋Š” ์ˆœ๊ฐ„ ์“ธ๋ชจ์—†๋Š” ํ˜น๋ฉ์–ด๋ฆฌ๊ฐ€ ๋  ๋ฟ์ž…๋‹ˆ๋‹ค. ์กฐ๊ธˆ์ด์ง€๋งŒ ํŠธ๋ž˜ํ”ฝ๋„ ๋” ๋ฐœ์ƒํ•  ๊ฒƒ์ด๊ณ  ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž๋ณด๋‹จ ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ž๊ฐ€ ํž˜๋“ค์–ด์ง€๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋‹จํŽธ์ ์ธ ์˜ˆ๋กœ Github์˜ API๋„ ์‘๋‹ต์˜ Body์—” ๋ฐ์ดํ„ฐ๋งŒ ๋‹ด์•„์„œ ๋ณด๋‚ด์ฃผ๋Š” ํ˜•์‹์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.

Solution : OkHttp Custom Interceptor๐Ÿ˜€

ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” ํ•ญ์ƒ ๊ฐœ๋ฐœ์˜ ์ดˆ๊ธฐ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๊ด€์—ฌํ•  ์ˆ˜ ์žˆ์ง€ ์•Š๊ณ , ์ด๋ฏธ ๊ฐœ๋ฐœ๋œ ์„œ๋ฒ„์— ๋งž์ถฐ์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿด ๋•Œ ์šฐ๋ฆฌ๋Š” OkHttp์˜ Interceptor๋ฅผ ์ด์šฉํ•ด์„œ Retrofit์„ ์ด์šฉํ•œ ์„œ๋ฒ„ ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ค‘๊ฐ„์—์„œ ๊ฐ€๋กœ์ฑ„ ์›ํ•˜๋Š” ๊ฐ’์„ ๋ผ์›Œ์„œ ์š”์ฒญํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต์„ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ์กฐ๊ธˆ ๋ณ€ํ˜•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Retrofit์€ ๋‚ด๋ถ€์ ์œผ๋กœ OkHttp๋ผ๋Š” Http ํ†ต์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ทธ๋ฆผ์€ OkHttp์—์„œ Interceptor์˜ ๊ฐœ๋…์ด ๋ฌด์—‡์ธ์ง€ ๋‹จํŽธ์ ์œผ๋กœ ์„ค๋ช…ํ•ด์ค๋‹ˆ๋‹ค.

์œ—๋ถ€๋ถ„์ด ์šฐ๋ฆฌ์˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜, ์•„๋žซ๋ถ€๋ถ„์ด ์„œ๋ฒ„๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ƒฅ Interceptor๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ OkHttp ์ฝ”์–ด ์‚ฌ์ด์—์„œ ์š”์ฒญ/์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„๋Š” ์—ญํ• ์„ ํ•˜๊ณ , NetworkInterceptor๋Š” ์‹ค์ œ ํ†ต์‹ ์—์„œ ์„œ๋ฒ„์™€ OkHttp ์ฝ”์–ด ์‚ฌ์ด์—์„œ ์š”์ฒญ/์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ Interceptor๋ฅผ ์žฅ์ฐฉํ•ด์„œ ์–ด๋–ป๊ฒŒ Retrofit ๊ฐ์ฒด๋ฅผ ์–ป์–ด๋ƒˆ๋Š”์ง€ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

private val client = OkHttpClient.Builder()
.addNetworkInterceptor(commonNetworkInterceptor)
.build()

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

Retrofit.Builder๋ฅผ ์ด์šฉํ•˜์—ฌ Retrofit ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค ๋•Œ, ๊ทธ๋ƒฅ ๋งŒ๋“  ๊ฒƒ์ด ์•„๋‹ˆ๋ผ OkHttpClient ๊ฐ์ฒด๋ฅผ .clinet() ๋ฉ”์„œ๋“œ๋กœ ํƒ‘์žฌํ•ด์„œ ์ƒ์„ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  OkHttpClient ๊ฐ์ฒด๋Š” addNetworkInterceptor ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด ์–ด๋–ค Interceptor ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•ด์„œ ์ƒ์„ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

commonNetworkInterceptor ๊ฐ์ฒด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

private val commonNetworkInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
.......
}
}

Interceptor๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์„œ intercept๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด์ œ ์ด ์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์ฐจ๊ทผ์ฐจ๊ทผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

/**
* 1) Common Header with API Access Token
*/
val newRequest = chain.request().newBuilder()
.addHeader("token", SPUtil.accessToken).build()
  1. ๊ณตํ†ต ํ—ค๋” ์žฅ์ฐฉ(์„ ํƒ) : ์šฐ์„ , ์šฐ๋ฆฌ๋Š” ์‘๋‹ต์œผ๋กœ ์˜จ chain ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด์„œ ๊ณตํ†ต์ ์ธ ํ—ค๋”๋ฅผ ๋‹ฌ์•„์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‘๋‹ต์„ ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ๊ณผ๋Š” ๊ด€๋ จ์—†์ง€๋งŒ, ์„œ๋ฒ„์— ํ•ญ์ƒ ๊ณตํ†ต์ ์ธ Access Token์„ Header์— ๋‹ฌ์•„์ค„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, Retrofit API ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•  ๋•Œ, @Header์–ด๋…ธํ…Œ์ด์…˜์„ ๋‹ฌ์•„์ค„ ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค.
/**
* 2) General Response from Server (Unwrapping data)
*/
val response = chain.proceed(newRequest)

2. ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์‘๋‹ต๋ฐ›๊ธฐ : chain์˜ proceed ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ์„œ๋ฒ„๋กœ ํ†ต์‹ ์„ ํ•˜๊ณ  ์‘๋‹ต์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. return ๊ฐ’์œผ๋กœ ๋ฐ›์€ response ๊ฐ์ฒด๊ฐ€ ๋ฐ”๋กœ ๊ทธ ์‘๋‹ต์„ ์˜๋ฏธํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

/**
* 3) Parse body to json
*/
val rawJson = response.body?.string() ?: "{}"

3. ์‘๋‹ต์˜ body๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋งŒ๋“ค๊ธฐ: body๋ฅผ toString() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์ผ๋ฐ˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

/**
* 4) Wrap body with gson
*/
val type = object : TypeToken<ResponseWrapper<*>>() {}.type
val res = try {
val r = gson.fromJson<ResponseWrapper<*>>(rawJson, type) ?: throw JsonSyntaxException("Parse Fail")

if(!r.success)
ResponseWrapper<Any>(-999, false, "Server Logic Fail : ${r.message}", null) //1
else
r

} catch (e: JsonSyntaxException) {
ResponseWrapper<Any>(-999, false, "json parsing fail : $e", null) //2
} catch (t: Throwable) {
ResponseWrapper<Any>(-999, false, "unknown error : $t", null) //3
}

4. JSON ๋ฌธ์ž์—ด์„ ResponseWrapper ํ˜•์‹์˜ ๊ฐ์ฒด๋กœ Gson์„ ์ด์šฉํ•ด์„œ ๋ณ€ํ™˜ํ•˜๊ธฐ : Gson์€ JSON ๋ฌธ์ž์—ด์„ POJO(Plain-Old-Java-Object)๋‚˜ ์ฝ”ํ‹€๋ฆฐ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋กœ ์‰ฝ๊ฒŒ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. fromJson<T>(String, Type) ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ๊ฐ–๊ณ  ์žˆ๋Š” JSON ๋ฌธ์ž์—ด ๊ฐ์ฒด๋ฅผ ResponseWrapper ๊ฐ์ฒด๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋˜ 3 ๊ฐ€์ง€์˜ ๋ฌธ์ œ ์ƒํ™ฉ์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฃผ์„์œผ๋กœ ์ฒ˜๋ฆฌํ•ด๋‘” ์ˆซ์ž๋ฅผ ๋ณด๋ฉด,

1- JSON ๋ฌธ์ž์—ด์ด ์šฐ๋ฆฌ์˜ ResponseWrapper ๊ฐ์ฒด๋กœ ํƒˆ๋ฐ”๊ฟˆ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ, success ํ•„๋“œ๊ฐ€ false๋ผ์„œ ์„œ๋ฒ„๋‹จ์—์„œ ์ •์ƒ์ ์ธ ์š”์ฒญ์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

2- ์•„์˜ˆ ResponseWrapper๋กœ ํƒˆ๋ฐ”๊ฟˆ๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

3- ์•Œ์ˆ˜์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

๊ณตํ†ต์ ์œผ๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋“  ๋ง๋“  ResponseWrapper๋ผ๋Š” ๊ฐ์ฒด๋กœ ์ฝ”ํ‹€๋ฆฐ ๋ฌธ๋ฒ•์„ ์ด์šฉํ•ด์„œ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋งŒ๋“  ์ด์œ ๋Š” ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. res๋ผ๋Š” ๊ฐ์ฒด์— ResponseWrapper ๊ฐ์ฒด๋ฅผ ๊ฐ–๊ณ  ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

/**
* 5) get data json from data
*/
val dataJson = gson.toJson(res.data)

5. ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ : ์ด์ œ res ๊ฐ์ฒด์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋‹ค์‹œ JSON ๋ฌธ์ž์—ด๋กœ Gson์˜ toJson ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

/**
* 6) return unwrapped response with body
*/
return response.newBuilder()
.message(res.message)
.body(dataJson.toResponseBody())
.build()

6. ๊ธฐ์กด ์‘๋‹ต์— Message์™€ Body๋งŒ ๊ต์ฒดํ•ด์„œ ์‘๋‹ต ์ „๋‹ฌ : ๊ธฐ์กด ๊ฐ–๊ณ  ์žˆ๋˜ ์‘๋‹ต์ธ response ๊ฐ์ฒด์— newBuilder() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์‘๋‹ต์„ ๋ณต์‚ฌํ•˜๊ณ , message์—” ์ค€๋น„ํ•ด๋‘” message(์„ฑ๊ณต์ผ ๋• ์„œ๋ฒ„์—์„œ์˜ message ๊ทธ๋Œ€๋กœ, ์‹คํŒจ์ผ ๋• ๊ณผ์ • 4์˜ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€)๋ฅผ, data์—” ์šฐ๋ฆฌ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•˜๋Š” JSON ๋ฌธ์ž์—ด์„ RequestBody ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

๋‹ค์Œ์€ Interceptor ์ „์ฒด ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

Result๐Ÿ’

์šฐ๋ฆฌ์˜ ์ฝ”๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ณ€ํ™”๋˜์—ˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์šฐ์„  Retrofit์˜ API Interface์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. ResponseWrapper๊ฐ€ ์ค‘๊ฐ„์—์„œ ๋ชจ์Šต์„ ๊ฐ์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.

without ResponseWrapper!

๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜์™€ ๊ฐ™์ด Network ์š”์ฒญ ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

without ResponseWrapper logic check!

์—ฌ์ „ํžˆ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•˜์ง„ ์•Š์ง€๋งŒ ๊ทธ๋Ÿญ์ €๋Ÿญ ์‚ฌ์šฉํ•ด์งˆ๋งŒํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ : [Funding(nickname=null, fundingIdx=167, userIdx=113, storeIdx=30, fundingMoney=4500, rewardPercent=0, fundingTime=2020-01-03T15:22:02.000Z, rewardMoney=9000)]

๋„คํŠธ์›Œํฌ ์š”์ฒญ๋„ ๊น”๋”ํ•˜๊ฒŒ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์œ„์™€ ๊ฐ™์ด Interceptor๋ฅผ ์žฅ์ฐฉํ•˜๋Š” ๊ฒฝ์šฐ,

{
"status": 200,
"success": true,
"message": "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."
}

์œ„์™€ ๊ฐ™์ด data๊ฐ€ ์—†๋Š” ์‘๋‹ต๋„ ์ž๋™์œผ๋กœ Code๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.

Bonus : Coroutineโค๏ธ

์ตœ๊ทผ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•œ Coroutine์€ ์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ•„์š” ์—†๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. Retrofit 2.6.0๋ถ€ํ„ฐ suspend ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•œ ์ฝ”๋ฃจํ‹ด ์‘๋‹ต ์ง€์›์ด ์ƒ๊ฒผ๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

@GET("mypage/fundlist")
suspend fun getMyFundingHistories() : List<Funding>

์œ„์™€ ๊ฐ™์ด Call๋„ ์—†์ด suspend ํ•จ์ˆ˜๋กœ API Interface ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๊ณ ,

lifecycleScope.launchWhenCreated {
kotlin.runCatching {
NetworkClient.fundingService.listFundingHistories()
}.onSuccess {
Timber.e("๋ฐ์ดํ„ฐ : $it")
}.onFailure {
Timber.e(it)
}
}

Structured Concurrency์˜ CoroutineScope๋ฅผ ์ด์šฉํ•ด ์›ํ•˜๋Š” ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ , ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ํŒŒ๊ดด๋  ๋•Œ ์ž๋™์œผ๋กœ ํ†ต์‹ ์ด ์ทจ์†Œ๋˜๋„๋ก ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

Conclusionโœ”๏ธ

์—ฌ๊ธฐ๊นŒ์ง€ ํ•ด์„œ OkHttp์˜ Interceptor๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„์˜ JSON ์‘๋‹ต ์ค‘, ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„๋งŒ ๋ฝ‘์•„์„œ ๋ณ„๋„์˜ ํŒŒ์‹ฑ ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์•„์Šต๋‹ˆ๋‹ค.

Retrofit๊ณผ Gson์„ ๊ณต๋ถ€ํ•˜๋ฉด ๊ณต๋ถ€ํ•˜๋Š” ๋งŒํผ Enum์„ ์š”์ฒญ/์‘๋‹ต์— ์‚ฌ์šฉํ•˜๊ณ  ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์˜ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์— ๋Œ€ํ•ด ๊นŠ์ด ์ดํ•ดํ•˜๊ณ  ๋‹ค๋ฃจ๋Š” ๋ฒ•์— ๋Šฅ์ˆ™ํ•ด์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ”ผ๋“œ๋ฐฑ์€ ์–ธ์ œ๋‚˜ ํ™˜์˜์ž…๋‹ˆ๋‹ค.

--

--