Retrofit์ ์ดํดํ๊ธฐ ์ํด ์์์ผ ํ๋ ๊ฐ๋
- ์๋ฒ/ํด๋ผ์ด์ธํธ
- HTTP
- API
- REST
- JSON
- GSON
1. ์๋ฒ? ํด๋ผ์ด์ธํธ?
- ์๋ฒ (Server)
- ๋ฐ์ดํฐ๋ ๋ฆฌ์์ค๋ฅผ ์ ๊ณตํ๋ ์์คํ
- ์ฌ์ฉ์์ ์์ฒญ์ ๊ธฐ๋ค๋ฆฌ๊ณ , ์์ฒญ์ด ๋ค์ด์ค๋ฉด ๊ทธ์ ๋ง๋ ์๋ต์ ์ ์ก - ํด๋ผ์ด์ธํธ (Client)
- ์ฌ์ฉ์๋ฅผ ๋ํํ์ฌ ์๋ฒ์ ์ ๋ณด๋ ์๋น์ค๋ฅผ ์์ฒญํ๋ ์์คํ
- ์น ๋ธ๋ผ์ฐ์ , ๋ชจ๋ฐ์ผ ์ฑ, ๋ฐ์คํฌํฑ ์ฑ ๋ฑ ๋ค์ํ ํํ๋ก ์กด์ฌ
์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ํต์ ํ๋ ๋ฐฉ์์ ๋ค์ํ๋ฉฐ, ์ฌ์ฉํ๋ ํ๋กํ ์ฝ / ์ฉ๋ / ์ฑ๋ฅ ์๊ตฌ์ฌํญ์ ๋ฐ๋ผ ์ ์ ํ ๊ฒ์ ์ ํ
ํต์ ๋ฐฉ์์ผ๋ก๋ HTTP/HTTPS, WebSockets, Socket(TCP/UDP), FTP, RPC, SOAP, GraphQL, MQTT ๋ฑ์ด ์์
ํ๋กํ ์ฝ : ํต์ ๊ท์ฝ (= ์ฝ์, ์ปดํจํฐ ๋๋ ์ ์๊ธฐ๊ธฐ ๊ฐ์ ์ํํ ํต์ ์ ์ํด ์งํค๊ธฐ๋ก ์ฝ์ํ ๊ท์ฝ)
๐ก 3-Tier ์ํคํ ์ณ
- ํ๋ ์ ํ ์ด์ ๊ณ์ธต (Web Server) : ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ๋ ์ฑ(ํด๋ผ์ด์ธํธ)
- ์ ํ๋ฆฌ์ผ์ด์ ๊ณ์ธต (WAS) : ๋ฆฌ์์ค๋ฅผ ์ ๋ฌํด์ฃผ๋ ์ฑ(์๋ฒ)
- ๋ฐ์ดํฐ ๊ณ์ธต (DB) : ๋ฆฌ์์ค ์ ์ฅ ๊ณต๊ฐ(๋ฐ์ดํฐ๋ฒ ์ด์ค)
2. HTTP? ์น ์ ํ๋ฆฌ์ผ์ด์ ํ๋กํ ์ฝ
- ์น ๊ธฐ๋ฐ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฃผ๋ก ์ฌ์ฉ
- REST API๋ SOAP์ ๊ฐ์ ์น ์๋น์ค ํต์ ๋ฐฉ์์์ ๊ธฐ๋ฐ
- ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์๋ก HTTP ๋ผ๋ ํ๋กํ ์ฝ์ ์ด์ฉํด์ ์๋ก ๋ํ๋ฅผ ๋๋
- HTTP๋ฅผ ์ด์ฉํด ์ฃผ๊ณ ๋ฐ๋ ๋ฉ์์ง๋ "HTTP ๋ฉ์์ง" ๋ผ๊ณ ๋ถ๋ฆ
3. API? Application Programming Interface
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์๊ฒ ์์ฒญํ๋ ค๋ฉด, ์ ํํ ์ฃผ๋ฌธ ๋ฐฉ๋ฒ์ ๋ฐ๋ผ ์์ฒญํด์ผ ํจ
- ์ด์ฒ๋ผ ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฆฌ์์ค๋ฅผ ์ ํ์ฉํ ์ ์๋๋ก ์ ๊ณตํ๋ ์ธํฐํ์ด์ค๋ฅผ API๋ผ๊ณ ์นญํจ
์ฌ๊ธฐ์ ์ธํฐํ์ด์ค์ ๋ป์ '์์ฌ์ํต์ด ๊ฐ๋ฅํ๋๋ก ๋ง๋ค์ด์ง ์ ์ ', ์ฆ ๊ท๊ฒฉ/๋งค๋ด์ผ ๋๋
4. REST API? Representational State Transfer
- API์๋ ์ฒด๊ณ๊ฐ ํ์ํ๋ค๋ ๊ด์ ์์ ๋์จ ๋ฐฉ๋ฒ์ด์, API ๊ท์น๊ณผ ๋ด์ฉ์ ๋ ์ง๊ด์ ์ด๊ณ ๊ฐ๋จํ๊ณ ์ฝ๊ฒ ๋ง๋ ๊ฒ
- HTTP ์์ฒญ์ ๋ณด๋ผ ๋ ์ด๋ค URI์ ์ด๋ค Method๋ฅผ ์ฌ์ฉํ ์ง ๊ฐ๋ฐ์ ์ฌ์ด์์ ๋๋ฆฌ์ง์ผ์ง๋ ์ฝ์
์ผ์ข ์ ๊ดํ์ ์ธ ํ์๊ณผ ์ฝ์์ด๋ฏ๋ก, ์ฑ์ ๋ง๋ค๋ ์น์ ๋ง๋ค๋ ์ด๋ค ์ธ์ด๋ฅผ ์จ์ ๊ตฌํํ๋ , ๊ทธ ์์ ์ํํธ์จ์ด ๊ฐ HTTP๋ก ์ ๋ณด๋ฅผ ์ฃผ๊ณ ๋ฐ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ๊ท์น์ ์ค์ํ์ฌ RESTfulํ ์๋น์ค๋ฅผ ๋ง๋ค ์ ์์ - ์น์ ์ฅ์ ์ ์ต๋ํ ํ์ฉ
์๋ ์์ด๋ ์น(WWW) : REST ์ํคํ ์ฒ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑ - REST API Method
๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ๋ HTTP URI๋ก ์ด๋ค ์์์ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ป์ ๊ฒ์ธ์ง ํ์
- POST: Create(์์ฑํ๊ธฐ) ex. ์น ํ์ด์ง์ ์ฌ์ง์ ์ฌ๋ฆฌ๋ ์์ฒญ.
- GET: Read(์ฝ์ด์ค๊ธฐ) ex. ์น ํ์ด์ง์์ ์ฌ์ง์ ๋ถ๋ฌ์ค๋ ์์ฒญ.
- PUT(์ ์ฒด) / PATCH(์ผ๋ถ): Update(๋ณ๊ฒฝํ๊ธฐ) ex. ์น ํ์ด์ง์ ์ฌ์ง์ ๋ฐ๊พธ๋ ์์ฒญ.
- DELETE: Delete(์ญ์ ํ๊ธฐ) ex. ์น ํ์ด์ง์ ์ฌ์ง์ ์ง์ฐ๋ ์์ฒญ. - RESTful API ์๋ฒ ์๋ต: 2XX ์ฝ๋๋ ์ฑ๊ณต์ ๋ํ๋ด๊ณ 4XX ๋ฐ 5XX ์ฝ๋๋ ์ค๋ฅ
5. JSON? : JavaScript Object Notation
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๊ฑฐ๋ ์ ์ฅํ ๋ ์ฌ์ฉํ๋ ๊ฒฝ๋์ ๋ฐ์ดํฐ ๊ตํ ํ์
- ๊ณผ๊ฑฐ ์น ์ด๊ธฐ ์์ ๋ถํฐ ์ฌ์ฉ๋ XML์ ํค๋์ ํ๊ทธ ๋ฑ์ ์ฌ๋ฌ ์์๋ก ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๊ณ , ์ธ๋ฐ์์ด ์ฉ๋์ ์ก์๋จน๋๋ค๋ ๋จ์ ์ ํญ์ ์ง์ ๋ฐ์๋ค. ์ด์ ๋์ํด ๊ฐ๊ฒฐํ๊ณ ํต์ผ๋ ์์์ผ๋ก ๊ฐ๊ด์ ๋ฐ๊ณ ์๋ ๊ฒ์ด JSON์ด๋ค.
- ์๋ฒ๋ก๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด์ ํ๋์ ์ฝ์์ผ๋ก JSON์ด๋ ๋ฐ์ดํฐํ์์ ์ฌ์ฉํ์ฌ ํต์ ํ๋ค๊ณ ์๊ฐํ๋ฉด๋๋ค.
{
"์ด๋ฆ๊ณต๊ฐ(ํค)": "๊ฐ",
"๊ฐ ๊ตฌ๋ถ์": "๊ฐ๊ฐ์ ๊ฐ๋ค์ ',' (์ฝค๋ง)๋ก ๊ตฌ๋ถ๋์ด์ผ ํฉ๋๋ค.",
"์ด์ค์ผ์ดํ": "ํค๋ ๊ฐ์์ ํฐ๋ฐ์ดํ๋ฅผ ์ฐ๊ณ ์ถ์ผ๋ฉด-ํน์ ๋ฌธ์๋ฅผ ์ด์ค์ผ์ดํ ํ๋ ค๋ฉด- \" ์ฒ๋ผ ๋ฌธ์ ์์ ์ญ์ฌ๋์๋ฅผ ๋ถ์
๋๋ค.",
"์๋ฃํ": "ํํ ๊ฐ๋ฅํ ์๋ฃํ์ ๋ฌธ์์ด, ์ซ์, ๋ถ๋ฆฌ์ธ, ๋, ๊ฐ์ฒด, ๋ฐฐ์ด 6๊ฐ์
๋๋ค.",
"๋ฌธ์์ด ๊ฐ": "๋๋ฌด์ํค, ์ฌ๋ฌ๋ถ์ด ๊ฐ๊พธ์ด ๋๊ฐ๋ ์ง์์ ๋๋ฌด",
"์ซ์ ๊ฐ": 19721121,
"๋ถ๋ฆฌ์ธ ๊ฐ": true,
"๋ ๊ฐ": null,
"๊ฐ์ฒด ๊ฐ": {
"๊ฐ1": 3.14159265358979323846264338,
"๊ฐ2": false,
"๊ฐ3": {
"๊ฐ์ฒด ์์": "๊ฐ์ฒด๋ฅผ ๋ฃ๋๊ฒ๋ ๊ฐ๋ฅํ์ง์",
"๊ตฌ๋ถ์": "๋ํ ํค์ ๊ฐ์ ':' ๋ก ๊ตฌ๋ถ๋ฉ๋๋ค"
}
},
"๋ฐฐ์ด ๊ฐ": [
"์ด๊ฒ์ ๋ฐฐ์ด์
๋๋ค.",
{
"ํ์ฌ ๊ฐ์ ์ธ๋ฑ์ค": 1,
"์ด๋ฐ ์์ผ๋ก": "๋ฐฐ์ด ์์ ์ฌ๋ฌ ๊ฐ์ ๋ฃ์ ์ ์์ต๋๋ค."
},
[ "๋ฐฐ์ด", "์์", "๋ฐฐ์ด์", "๋ฃ๋๊ฒ๋", "๊ฐ๋ฅํ์ง์" ]
],
"๊ฐ์ ๊ฐ์๊ฐ ์ ์๋๋": "๋ค์๊ณผ ๊ฐ์ด ํ ์ค๋ก๋ ๊ฐ์ฒด์ ๋ฐฐ์ด ํํ์ด ๊ฐ๋ฅํฉ๋๋ค.",
"ํ ์ค ๊ฐ์ฒด": { "๊น๋ํ": "๋ ๊น๋ํ์ด๋ค", "์ฌ์": "๋ด๊ฐ ๊ณ ์๋ผ๋", "์์ฌ์๋ฐ": "๋ณ์ ์ ๋ง๋ค์ด์ฃผ๋ง" , "์ด๊ทผ": "4๋ฒ์ ๊ฐ์ธ์ฃผ์์ผ" },
"ํ ์ค ๋ฐฐ์ด": [ "๋๋ฌด์ํค๋", "๋๊ตฌ๋", "๊ธฐ์ฌํ ", "์", "์๋", "์ํค์
๋๋ค." ]
}
- JSON๋ฐ์ดํฐ๋ ํ๋์ NAME๊ณผ VALUE๋ก ์ด๋ฃจ์ด์ง ("๋ฐ์ดํฐ์ด๋ฆ" : ๊ฐ)
๋ฐ์ดํฐ์ด๋ฆ(NAME) : Stringํ์
๊ฐ(value) : ์ซ์, ๋ฌธ์์ด, ๋ถ๋ฆฌ์ธ(boolean), ๊ฐ์ฒด, ๋ฐฐ์ด(array), NULL๊ฐ ๋ฑ - JSON๋ฐฐ์ด์ ์ค๊ดํธ{}๊ฐ ์๋ ๋๊ดํธ[]๋ก ๋๋ฌ์์ ํํ
6. GSON?
- Google์์ ์ ๊ณตํ๋ ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, Java์ Kotlin์์ ์ฃผ๋ก ์ฌ์ฉ
- ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด, ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋ ์์ฃผ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ ํฌ๋งท์ธ JSON ๋ฐ์ดํฐ๋ฅผ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๊ฐ์ฒด๋ก ๋ณํํด์ผ ํ ๋๊ฐ ์์ (๋ฐ๋๋ก ๊ฐ์ฒด๋ฅผ JSON ํํ๋ก ๋ณํํด์ผ ํ ๋๋ ์กด์ฌ)
→ ์ด๋ฌํ ์์ ์ '์ง๋ ฌํ(Serialization)'์ '์ญ์ง๋ ฌํ(Deserialization)'๋ผ๊ณ ํจ - Gson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ฌํ ์ง๋ ฌํ / ์ญ์ง๋ ฌํ ์์ ์ ๋งค์ฐ ๊ฐ๋จํ๊ฒ ํด์ค
val gson = Gson()
val jsonString = gson.toJson(someObject)
- Kotlin ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํ
val myClassInstance: MyClass = gson.fromJson(jsonString, MyClass::class.java)
- JSON์ Kotlin ๊ฐ์ฒด๋ก ๋ณํ
data class Person(
@SerializedName("person_name")
val name: String
)
- @SerializedName("์๋ JSONํค ์ด๋ฆ") ์ด๋ ธํ ์ด์
- Kotlin ํ๋์ JSON ํค ์ด๋ฆ์ด ๋ค๋ฅผ ๊ฒฝ์ฐ ๋งคํ
์ด์ธ์๋ Custom Serializer/Deserializer (ํน์ ํ์
์ ๋ํด ์ฌ์ฉ์ ์ง์ ์ง๋ ฌํ ๋ฐ ์ญ์ง๋ ฌํ ๋ก์ง์ ์ ์)
Exclusion Strategies (ํน์ ํ๋์ ์ง๋ ฌํ ๋๋ ์ญ์ง๋ ฌํ๋ฅผ ์ ์ธํ๊ธฐ ์ํ ์ ๋ต ์ ์)๊ฐ ์์
๋ค๋ง 2024๋
ํ์ฌ ์์ ์์ Gson๋ณด๋ค Moshi ์ฌ์ฉ์ ์งํฅํ๋ ํธ
(Gson์ ์ฝํ๋ฆฐ์ ๋์ธ์ดํํฐ๋ฅผ ์ค์ํ์ง ์์ผ๋ฉฐ, ์ฝํ๋ฆฐ์ ๋ํดํธ ๋ฌธ๋ฒ์ ๋ฌด์ํด๋ฒ๋ฆผ
๋ฐ๋ฉด Moshi๋ Kotlin๋ ํฌํจ๋ ์ปจ๋ฒํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๋ ์ธ์ดํํฐ ๊ฐ์ ์ฅ์ ์ ์ด๋ฆด ์ ์๊ณ ์
๋ฐ์ดํธ ํ๋ฐ)
7. Retrofit?
- ์๋๋ก์ด๋ ๋ฐ ์๋ฐ๋ฅผ ์ํ ํ์ ์ธ์ดํํ HTTP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์๋๋ก์ด๋ ๊ฐ๋ฐ ์ ์๋ฒํต์ ์ ์ฌ์ฉ๋๋ HTTP API๋ฅผ ์๋ฐ, ์ฝํ๋ฆฐ์ ์ธํฐํ์ด์ค ํํ๋ก ๋ณํํด API๋ฅผ ์ฝ๊ฒ ํธ์ถํ ์ ์๋๋ก ์ง์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์ฅ์
1. ์ฝ๋์ ๊ฐ๊ฒฐ์ฑ
- ๋ณต์กํ HTTP API ์์ฒญ์ ์ฝ๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ฆ
- ๊ฐ๋จํ ์ด๋ ธํ ์ด์ ์ ํตํด ์์ฒญ ๋ฉ์๋์ URL์ ์ ์ ๊ฐ๋ฅ
2. ์์ ์ฑ๊ณผ ํ์ฅ์ฑ
- ๋ด๋ถ์ ์ผ๋ก OkHttp ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํต์ , ์ด๋ฅผ ํตํด ์์ ์ ์ธ ํต์ ์ด ๊ฐ๋ฅ
- ์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ/์๋ต ํ๋ก์ธ์ค๋ฅผ ํ์ฅํ๊ฑฐ๋ ์์ ํ ์ ์์
3. ๋ค์ํ ํ๋ฌ๊ทธ์ธ๊ณผ ์ปจ๋ฒํฐ ์ง์
- ๋ค์ํ ๋ฐ์ดํฐ ํ์(JSON, XML ๋ฑ)์ ๋ํด ๋ฐ์ดํฐ ๋ณํ ์ปจ๋ฒํฐ๋ฅผ ์ ๊ณต
- RxJava, Coroutines์ ๊ฐ์ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฐ๋ ๊ฐ๋ฅ
์ฅ์
1. ์ฝ๋์ ๊ฐ๊ฒฐ์ฑ
- ๋ณต์กํ HTTP API ์์ฒญ์ ์ฝ๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ค ์ ์์
- ๊ฐ๋จํ ์ด๋
ธํ
์ด์
์ ํตํด ์์ฒญ ๋ฉ์๋์ URL์ ์ ์ํ ์ ์์
2. ์์ ์ฑ๊ณผ ํ์ฅ์ฑ
- ๋ด๋ถ์ ์ผ๋ก OkHttp ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํต์ , ์ด๋ฅผ ํตํด ์์ ์ ์ธ ํต์ ์ด ๊ฐ๋ฅ
- ์ธํฐ์
ํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ/์๋ต ํ๋ก์ธ์ค๋ฅผ ํ์ฅํ๊ฑฐ๋ ์์ ํ ์ ์์
3. ๋ค์ํ ํ๋ฌ๊ทธ์ธ๊ณผ ์ปจ๋ฒํฐ ์ง์
- ๋ค์ํ ๋ฐ์ดํฐ ํ์(JSON, XML ๋ฑ)์ ๋ํด ๋ฐ์ดํฐ ๋ณํ ์ปจ๋ฒํฐ๋ฅผ ์ ๊ณต
- RxJava, Coroutines์ ๊ฐ์ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฐ๋ ๊ฐ๋ฅ
1. ์ฑ ์์ค build.gradle์ Retrofit ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐํ๊ธฐ
// build.gradle (Module: app)
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
2. API ์ธํฐํ์ด์ค ์ ์ํ๊ธฐ
interface ApiService {
@GET("users/{id}")
fun getUser(@Path("id") id: Int): Call<User> // ์ฌ๊ธฐ์ User๋ ์๋ฒ ์๋ต์ผ๋ก ๋ฐ์์ฌ ๋ฐ์ดํฐ ๋ชจ๋ธ ํด๋์ค
}
3. Retrofit ์ธ์คํด์ค ์์ฑํ๊ธฐ
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create(ApiService::class.java)
// ์ด์ apiService ๊ฐ์ฒด๋ฅผ ํตํด ์ ์๋ API ์์ฒญ์ ์ฌ์ฉ ๊ฐ๋ฅ
4-1. ์๋ต ์ฒ๋ฆฌํ๊ธฐ (๋๊ธฐ์ vs ๋น๋๊ธฐ์ ์์ฒญ)
// ๋๊ธฐ์ ์์ฒญ: ํ์ฌ ์ค๋ ๋์์ ์คํ๋๋ฉฐ, ์๋ต์ด ์ฌ ๋๊น์ง ๋ค์ ์ฝ๋์ ์คํ์ด ์ค๋จ
val response: Response<User> = apiService.getUser(id).execute()
// ๋น๋๊ธฐ์ ์์ฒญ: ์ฝ๋ฐฑ์ ์ฌ์ฉํ์ฌ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋๋ฉฐ, ์๋ต์ด ์ค๋ฉด ํด๋น ์ฝ๋ฐฑ์ด ํธ์ถ
apiService.getUser(id).enqueue(object: Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
// ์ฒ๋ฆฌ
}
override fun onFailure(call: Call<User>, t: Throwable) {
// ์ค๋ฅ ์ฒ๋ฆฌ
}
})
4-2. ์๋ต ์ฒ๋ฆฌํ๊ธฐ (์๋ต ๊ฐ์ฒด ์ฌ์ฉํ๊ธฐ)
// Response ๊ฐ์ฒด๋ฅผ ํตํด HTTP ์๋ต์ ์ฌ๋ฌ ์ ๋ณด์ ์ ๊ทผ ๊ฐ๋ฅ
if (response.isSuccessful) {
val user: User? = response.body()
} else {
// ์ค๋ฅ ๋ฉ์์ง ์ฒ๋ฆฌ
val error: String = response.errorBody()?.string() ?: "Unknown error"
}
4-3. ์๋ต ์ฒ๋ฆฌํ๊ธฐ (์ค๋ฅ ์ฒ๋ฆฌํ๊ธฐ)
// Retrofit์ onFailure ์ฝ๋ฐฑ์ ๋คํธ์ํฌ ์ค๋ฅ๋ ๋ฐ์ดํฐ ๋ณํ ์ค๋ฅ ๋ฑ์์ ํธ์ถ
override fun onFailure(call: Call<User>, t: Throwable) {
// ์ค๋ฅ ๋ฉ์์ง ํ์
Log.e("API_ERROR", t.message ?: "Unknown error")
}
+ (์ถ๊ฐ) ๋ณด๋ค ์์ธํ Retrofit ์ฌ์ฉ๋ฒ
ํ๊ฒฝ ์ธํ
Gradle ์ค์ (app ์์ค build.gradle.kts ์ข ์์ฑ ์ถ๊ฐ)
implementation(libs.retrofit)
implementation(platform(libs.okHttpBom)) // Bom - ํ๋์ ๋ญ์น (๋น๋ฆฌ์ธ์ค ์ค๋ธ ๋จธํธ๋ฆฌ์ผ?)
implementation(libs.logging.interceptor) // ๋คํธ์ํฌ ์ก์์ ์ด ์ด๋ป๊ฒ ์ด๋ฃจ์ด์ง๋์ง ๋ณด๊ธฐ์ํจ
// moshi
implementation(libs.moshi.kotlin) // ์ฝํ๋ฆฐ์์ ์ฐ๋ moshi
implementation(libs.converter.moshi)
implementation(libs.moshi.adapters)
libs.versions.toml (๋ฒ์ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ)
[versions]
retrofitVersion = "2.11.0"
okHttpBomVersion = "4.12.0"
moshiVersion = "1.15.1"
moshiAdapters = "1.15.1"
[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
okHttpBom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okHttpBomVersion" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okHttpBomVersion" }
converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitVersion" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiVersion" }
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" }
- ๊ธฐ์กด ๋ฐฉ์๊ณผ ๋น๊ตํ์ฌ ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ๋ฒ์ ์นดํ๋ก๊ทธ ๋ฐฉ์์ผ๋ก ํ๋ ๊ฒ์ ์ฅ์ ?
: ๋ฉํฐ ๋ชจ๋ ํ๋ก์ ํธ์ ๊ฒฝ์ฐ ๋ชจ๋๋ง๋ค gradle์ด ์ค์ ๋์ด ์๊ธฐ ๋๋ฌธ์ ๊ฐ๊ฐ ์์ฑํ๊ธฐ์ ๋ฒ๊ฑฐ๋กญ๊ณ ๋ฌธ์ ์๊น
์ฌ์ฉํ๊ณ ์ ํ๋ API์ Key๋ฅผ local.properties์ ์ถ๊ฐ (ํค๋ ์์ ์ด ๋ฐ๊ธ๋ฐ์ ํค)
REST_API_KEY=abcdefghijklmnopqrstuvwxyz
KAKAO_BASE_URL = https://dapi.kakao.com/
- ๋ฒ ์ด์ค url: dapi.kakao.com
- ์๋ ํฌ์ธํธ: ๊ทธ ์ดํ url
์: @POST("v2/search/app") ์ฒ๋ผ ๋์์๋ ๋ฉ์๋์ ๊ทธ์ ๋ง๊ฒ ์๋ํฌ์ธํธ ๋ฃ์ด์ค
build.gradle.kts์ ์๋์ ๊ฐ์ด ๊ฐ ์ถ๊ฐ
buildTypes {
val gradleLocalProperties = gradleLocalProperties(
projectRootDir = rootDir,
providers = providers
)
val apiKey = gradleLocalProperties.getProperty("REST_API_KEY")
val baseUrl = gradleLocalProperties.getProperty("KAKAO_BASE_URL")
debug {
buildConfigField("String", "REST_API_KEY", "\"$apiKey\"")
buildConfigField("String", "KAKAO_BASE_URL", "\"$baseUrl\"")
isDebuggable = true
isMinifyEnabled = false
}
release {
buildConfigField("String", "REST_API_KEY", "\"$apiKey\"")
buildConfigField("String", "KAKAO_BASE_URL", "\"$baseUrl\"")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
// ...
buildConfig = true
}
- ๋ณดํต release๋ง ์๋๋ฐ, debug ์ถ๊ฐ
- buildConfig = true๋ก ์ง์ ํ๋ฉด key๊ฐ์ ์ด๋์๋(์ฝ๋๋จ)์์ ์ฌ์ฉ ๊ฐ๋ฅ
ex. BuildConfig.REST_API_KEY ์ฒ๋ผ ์ฌ์ฉ ๊ฐ๋ฅ
์ฝ๋ ์ธํ
์ฝ๋ ์ธํ
<uses-permission android:name="android.permission.INTERNET"/>
- ๋คํธ์ํฌ ์์ ์ Presentation, Domain, Data Layer ์ค์์ Data Layer์ ์ํจ
- ๋คํธ์ํฌ ์์ ์ด๋ฏ๋ก ํญ์ ์๋ ๊ถํ์ AndroidManifest.xml ์ ๋ฃ์ด์ฃผ์!
NetworkModule ๋ง๋ค๊ธฐ
import com.spartabasic.www.data.network.api.KakaoApiService
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.Date
import java.util.concurrent.TimeUnit
// ์๋ ๊ฑฐ ๋ณต๋ถํด์ ์ด๋์๋ ์ฌ์ฉ ๊ฐ๋ฅ
object NetworkModule {
val retrofit by lazy { // ์ด๋ ์์น์์๋ ์ฌ์ฉํ ์ ์๋๋ก ์ ์ญ๋ณ์๋ก ์ค์
provideKakaoApiService()
}
private fun provideMoshi(): Moshi { // ๋ชจ์ ๋น๋
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(Date::class.java, Rfc3339DateJsonAdapter()) // ๋ ์ง ํ์ ๋ณํ : (2017-) ์ด์ฉ๊ตฌ๋ฅผ ์๋ฐ๋ก ๋ณํ
.build()
}
private fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { // ์ธํฐ์
ํฐ
val interceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
} else { // ๋๋ฒ๊ทธ๊ฐ ์๋ ๋๋ ๋ก๊ทธ๊ฐ ์ฐํ์ง ์๋๋ก ์ค์ . ์ถ์ํ ๋ ๋ก๊ทธ ๋ชจ๋ ์ญ์ ํด์ผ ํจ!
interceptor.level = HttpLoggingInterceptor.Level.NONE
}
return interceptor
}
private fun provideOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor = provideHttpLoggingInterceptor(),
): OkHttpClient {
return OkHttpClient
.Builder() // ๋น๋ ๋์์ธํจํด - ์๋ฐ ๋ฐฉ์ (์๋ ์ฝํ๋ฆฐ์์ ํ์ ์๊ธฐ ๋๋ฌธ์ ์๋ฐ ๋ฐฉ์์์ ์ถ๋ก )
.connectTimeout(timeout = 20, unit = TimeUnit.SECONDS)
.readTimeout(timeout = 20, unit = TimeUnit.SECONDS)
.writeTimeout(timeout = 20, unit = TimeUnit.SECONDS)
.addInterceptor(interceptor = httpLoggingInterceptor) // ์ธํฐ์
ํฐ : ๋คํธ์ํฌ ์ก์์ ์ด ๋ก๊น
๋๋ ๊ฒ์ ํ์ธํ๊ธฐ ์ํด์ ์ถ๊ฐ
.build()
}
private fun provideRetrofitModule(moshi: Moshi = provideMoshi()): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi)) // ์ง๋ ฌํ์ ์ญ์ง๋ ฌํ. Gson์ธ ๊ฒฝ์ฐ GsonConverterFactory
}
// ์นด์นด์ค api ์ถ๊ฐ
private fun provideKakaoApiService(
client: OkHttpClient = provideOkHttpClient(),
retrofit: Retrofit.Builder = provideRetrofitModule()
): KakaoApiService {
return retrofit.baseUrl(BuildConfig.KAKAO_BASE_URL) // ๋ฒ ์ด์ค url ์ค์
.client(client) // ํด๋ผ์ด์ธํธ ์ค์
.build()
.create(KakaoApiService::class.java) // ์ด๋ค api ์๋น์ค๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ํ ๊ฑฐ๋
}
}
- retrofit์ ์ค์ ํ๋ ํ์ผ์ด ํ์. ๋ฒ ์ด์ค url๊ณผ ํด๋ผ์ด์ธํธ ์ค์ ๋ฑ
Response ํด๋์ค ๋ฐ Dto ๋ง๋ค๊ธฐ
1. KakaoApiResponse
import com.spartabasic.www.data.network.model.MetaDto
data class KakaoApiResponse<out T>( // T : ์ ๋ค๋ฆญ ํ์
. ์ด๋ฏธ์งapi๋ฅผ ์ค์ ํ๋ฉด ์ด๋ฏธ์ง ๋ฆฌ์คํฐ์ค, ๋น๋์คapi๋ฅผ ์ค์ ํ๋ฉด ๋น๋์ค ๋ฆฌ์คํฐ์ค
// out T๋ Tํ์
์ด๋ฉด ๋ค ๋๋ค๋ ๊ฒ์ธ๋ฐ out๊ณผ in ๊ตฌ๋ถํ๊ธฐ. ์ง์ด๋ฃ๋ ๊ฒฝ์ฐ ํจ์๋ช
(T)<>๋ฉด in
val meta: MetaDto,
val documents: T,
)
2. ImageSearchDto
import com.squareup.moshi.Json
import java.util.Date
data class ImageSearchDto(
val collection: String,
@Json(name = "thumbnail_url")
val thumbnailUrl: String,
@Json(name = "image_url")
val imageUrl: String,
val width: Int,
val height: Int,
@Json(name = "display_sitename")
val displaySiteName: String,
@Json(name = "doc_url")
val docUrl: String,
@Json(name = "datetime")
val dateTime: Date
)
- DTO๋?
= Data transfer object : ๋ฐ์ดํฐ๋ฅผ ์ฎ๊ฒจ์ฃผ๋(์ก์์ ์ ๋ด๋นํ๋) ๊ฐ์ฒด, DT์ ํ์ํ ๊ฐ์ฒด
๊ฐ๋ DTO๊ฐ ์๋๋ผ Entity๋ผ๊ณ ์ ๋ ์ฌ๋๋ ์์. ๊ทผ๋ฐ ์ด๊ฑด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํท๊ฐ๋ ค์ ๋คํธ์ํฌ ์์ ์ DTO๊ฐ ๋์
cf. DT (Data Transfer) ๋ฅผ ํ๋ ๊ฒ์ Retrofit - @Json ์ด๋ ธํ ์ด์ : moshi์์ ์ฌ์ฉํ๋ ์ด๋ฆ ๋ณ๊ฒฝ ๋ฐฉ์ (gson์์ @Serializedname)
3. VideoSearchDto
import com.squareup.moshi.Json
import java.util.Date
data class VideoSearchDto(
val title: String,
val url: String,
@Json(name = "datetime")
val dateTime: Date,
@Json(name = "play_time")
val playTime: Int,
val thumbnail: String,
val author: String
)
Api Service ์ธํฐํ์ด์ค ๋ง๋ค๊ธฐ
import com.spartabasic.www.data.network.model.ImageSearchDto
import com.spartabasic.www.data.network.model.VideoSearchDto
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface KakaoApiService { // Retrofit ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฌ์ฉ๋ฒ์ ๋ฐ๋ผ interface๋ก ์ฌ์ฉ, ๋๋จธ์ง Retrofit์ด ๋ค ํด์ค!
@GET("v2/search/image")
suspend fun getSearchedImages(
@Header("Authorization") header: String = "KakaoAK ${BuildConfig.REST_API_KEY}",
@Query("query") query: String,
@Query("sort") sort: String,
@Query("page") page: Int,
@Query("size") size: Int,
): Response<KakaoApiResponse<List<ImageSearchDto>>> // Response๋ ์ฑ๊ณตํ๋์ง ์๋์ง??
@GET("v2/search/vclip")
suspend fun getSearchedVideos(
@Header("Authorization") header: String = "KakaoAK ${BuildConfig.REST_API_KEY}",
@Query("query") query: String,
@Query("sort") sort: String,
@Query("page") page: Int,
@Query("size") size: Int,
): Response<KakaoApiResponse<List<VideoSearchDto>>>
}
- ๋ก๊ทธ์์ response๊ฐ ๋ณด๋ฉด 2xx๋ฉด ๋ชจ๋ ์ฑ๊ณต, 3xx์ ๋ฆฌ๋ค์ด๋ ์
, 4xx๋ ํด๋ผ์ด์ธํธ ์๋ฌ, 5xx๋ ์๋ฒ ์๋ฌ
404๋ not found ์๋ฌ (์: ์ ๋ ฅํ ์๋ ํฌ์ธํธ๊ฐ ์ค์ ์ ๋ค๋ฅธ ๊ฒฝ์ฐ)
400์ Bad Request ์๋ฌ๋ก ๊ฐ์ ๋ญ๊ฐ ์๋ชป ๋ณด๋์ ๋ (์: ํ ๋ฒ์ 10๋ง๊ฐ ์์ฒญํ๋ ๊ฒฝ์ฐ)
401์ Unauthorized ์๋ฌ๋ก ๊ถํ ์ค๋ฅ (์: ํค ๊ฐ์ ์ ๋ฃ์์ ๋) - ๋ก๊ทธ ๋ณด๋ฉด ๊ฐ์ ธ์จ ๊ฐ๋ค๋ ๋ค ๋ณผ ์ ์์
์ผ๋ฐ์ ์ธ ์ฌ์ฉ๋ฒ
์ผ๋ฐ์ ์ธ ์ฌ์ฉ๋ฒ (์์: SearchFragment.kt)
1. Retrofit์ Call ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
private fun fetchVideos() {
NetworkModule.retrofit.getSearchedVideos(
query = "test",
page = 1,
size = 10,
sort = "recency"
).enqueue(object : Callback<KakaoApiResponse<List<VideoSearchDto>>> {
// ์ฝ๋ฐฑ : ๊ณ์ ๊ธฐ๋ค๋ฆด ์ ์๊ธฐ ๋๋ฌธ, ์คํ์ด ์ฑ๊ณต์ ์ผ๋ก ๋๋๋ฉด ์๋ ๋ธ๋ญ ์คํ
override fun onResponse(
call: Call<KakaoApiResponse<List<VideoSearchDto>>>,
response: Response<KakaoApiResponse<List<VideoSearchDto>>>
) {
if (response.isSuccessful) {
Log.i(TAG, "Successful! ${response.code()}")
val body = response.body()
if (body != null) {
val meta = body.meta
val documents = body.documents
binding.tvMeta.text = "$meta"
binding.tvDocuments.text = "$documents"
} else {
Log.e(TAG, "Something went wrong!")
}
}
}
override fun onFailure( // ์คํจํ์ ๋
p0: Call<KakaoApiResponse<List<VideoSearchDto>>>,
p1: Throwable
) {
Log.e(TAG, "Error: ${p1.message}")
}
})
}
- call์ retrofit์์ ์ ๊ณตํด์ฃผ๋ ์ธํฐํ์ด์ค
- call์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ์ธํฐํ์ด์ค๊ธฐ ๋๋ฌธ์ ๋ฌด์กฐ๊ฑด enqueue(Retrofit์์ ์ง์ํ๋ ํจ์) ๋ถ๋ถ ๋ฑ์ด ํ์ํจ
-> ์ฝ๋๊ฐ ๊ธธ์ด์ง๊ณ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ, ์ฝ๋ฐฑ ์ง์ฅ(callback hell) ๊ฐ๋ฅ์ฑ...
2. Coroutine์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
private fun fetchImages() {
lifecycleScope.launch {
val response = NetworkModule.retrofit.getSearchedImages(
query = "test",
page = 1,
size = 10,
sort = "recency"
)
if (response.isSuccessful) {
Log.i(TAG, "Successful! ${response.code()}")
val body = response.body()
if (body != null) {
val meta = body.meta
val documents = body.documents
binding.tvMeta.text = "$meta"
binding.tvDocuments.text = "$documents"
} else {
Log.e(TAG, "Something went wrong!")
}
} else {
Log.e(TAG, "Error: ${response.errorBody()}")
}
}
}
- ๋ณต์กํ ์์ ์ ํ๊ฑฐ๋, ์์๋ฅผ ๋ณด์ฅํด์ผ ๋ ๋ ์ฌ์ฉ
- suspend๋ผ๊ณ ์จ ์๋ ํจ์๋ค์ ์์ฐจ์ ์ผ๋ก (์->์๋) ์งํํด ์ค. ๋ค๋ง!!!
1) ํจ์๊ฐ suspend๋ก ๋์ด ์๋์ง ํญ์ ํ์ธํด์ค์ผ ํจ
2) ์ฒซ ๋ฒ์งธ suspend๊ฐ ์คํจํ๋ฉด ๋ ๋ฒ์งธ suspend ์คํ ์ ๋จ (Supervisorjob ์ค์ ํ๋ฉด ๊ด์ฐฎ์์ง) - ์ฝ๋ฃจํด์ ๊ฒฝ์ฐ job.cancel()๋ก ์์ฒญ ์ฝ๊ฒ ์ทจ์ ๊ฐ๋ฅ -> call ์ธํฐํ์ด์ค๋ ์ด๋ป๊ฒ ์ทจ์ํ๋์ง ์ ๋ชจ๋ฆ
3. Rx๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
- ์์ ๋ชจ๋ฅด์ง๋ง, ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ๊ฐ๋ฅ์ฑ์ด ๋ง์
๊ณ ๋ฏผ ์ฌํญ
1. ๊ตณ์ด ViewModel์ ์ฌ์ฉํด์ผ ํ๋๊ฐ?
- ์ํฉ์ ๊ณ ๋ ค ํ์ ๋ ๋ฐ์ดํฐ ์ํ๋ฅผ ๋ณด์ํ๊ธฐ ์ํจ
- ํ๋ฉด์ด ์ ํ๋๊ฑฐ๋, Fragment ๋ฆฌํ๋ ์ด์ค ๋ ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํด Livedata ์ฌ์ฉ
- ์ํฉ์ ๋ง๊ฒ ํ๋จ ํ์ ์ ์ฐ๋ฉด ๋จ
- ๋ณดํต ๊ทธ๋ฐ๋ฐ ์ด๋ฐ ํ๊ฒฝ/์ํฉ์ด ๋ง๊ธฐ ๋๋ฌธ์ ๋ทฐ๋ชจ๋ธ์ ๋ง์ด ์ฐ๋ ๊ฒ
- ํ๋๋ฒ ์ฐ๋ ๊ฒ ์๋ ์ด์ ๋ทฐ์ ํ์๋๋ ๋ฐ์ดํฐ (๋ฐ์ดํฐ ๊ฐฑ์ ์์ ๋ฑ ์์ ) ์์ ์ ๋ชจ๋ ๋ทฐ๋ชจ๋ธ์์ ํ ๊ฒ
2. ์ ๋คํธ์ํฌ ๋ชจ๋์ object(Singleton)์ด์ด์ผ ํ ๊น?
- ์๋ก์ด ๊ฐ์ฒด๊ฐ ๊ณ์ ์์ฑ๋๋ฉด ์ ๋จ → ๋๊ฐ์ ์์ ์ ํ ๋ฒ๋ง ํด์ผ ๋คํธ์ํฌ๊ฐ ์ ํฐ์ง
- ๊ฐ์ ๋คํธ์ํฌ ์ํ๋ฅผ ์ ์งํ๊ธฐ ์ํจ