본문 바로가기
KUIT-앱 개발 프로젝트 동아리

9주차 실습(1)- Retrofit 사용 준비

by 농농씨 2023. 11. 21.

당근마켓 클론을 진행하던 프로젝트에 로그인 기능을 만들 것이다.

 

// 한 xml 단위가 끝날때(또는 기억할 수 있는 단위로), 블로그 글 쓰기- 실제 코드 따라쳐보기 순서대로

 

1. 먼저 회원가입 xml을 만든다

 

2. api 요청/응답 구조 알아보기

우리는 api를 구현할 것이므로 서버에서 api 명세서를 보내올 것이다.

api 명세서 예시

 

예를들어 위 사진처럼

REQUEST를 위에 명시한 대로 요청하면

응답을 그 아래에 있는 형식으로 받아볼 수 있을 거란 뜻이다.

안드로이드에서는 명세서에 적힌 대로 api를 구현하면 된다.

 

위 사진을 보면 

응답에 실패했을 때도(Error) isSuccess 값은 누락되지 않고 오고 만약 응답에 성공하면 거기에 추가 정보가 딸려오는 형식으로 구현되어 있다.

 

로그인도 데이터를 받아오는 게 비슷한 구조이다.

 

토큰은 jwt 토큰인데, 최초에 로그인할때 서버에서 jwt 토큰을 반환한다.

 

그럼 우리는 result를 해석해서 토큰값을 추출한 다음

sharedPreference에 등에 저장했다가

 

유저정보 가져오기(세번째 사진)와 같은 api 요청에서 그 jwt 토큰을 서버에 보낸다.

이때 jwt토큰을 바디가 아닌 "헤더"에 담아야 한다.

(<->로그인할때는 아이디 패스워드를 바디에 담았다.)

 

 

 

3. 기능 구현을 하기에 앞서 application calss를 만든다

New kotlin class/file 에서 class 선택해서 ApplicationClass.kt 파일을 만든다.

이게 뭐냐면, application을 상속하는 클래스로서,

앱화면을 생성할때 보통 Activity나 Activity를 상속하는 클래스를 상속받았었는데, 

여기서는 application을 상속받음으로써 앱 전체가 공유할 수 있는 전역변수 등을 관리하도록 하겠다.

어플을 켰을 때 제일 먼저 초기화되고, 앱 켜고있는 내내 살아있는 그런 변수들을 다룰 것이다.

 

companion object(: 클래스 안에 있는, 공통 변수를 가진 객체)를 그 안에 선언해준다.

이 안에 배포용 url과 개발용 url을 담을 것이다.

 

// ApplicationClass 중간 코드
package com.example.carrotmarket

import android.app.Application
import retrofit2.Retrofit

class ApplicationClass : Application() {
    companion object{
        const val DEV_URL : String = "http://13.125.254.172:23899"
        const val PROD_URL : String = "http://kuit_prod_url"

        const val BASE_URL : String = DEV_URL

        lateinit var retrofit: Retrofit
    }
}

 

 

4. Retrofit 이용하기

retrofit을 이용하려면 3가지가 필요하다

1. builder

2. retrofit 메서드 정의하는 인터페이스

3. 데이터 정의하는 data class

 

4-1. retrofit 이용하기 위한 설정 import해주기

우선 구글에서 retrofit2 git 검색하고 공식으로 배포된 문서에서 download 코너 가서 필요한 설정 복사해서 gradle에 붙여넣기 하자(앱 수준 그래들)

 

필요한게 한개더 있다

retrofit converter Factory

데이터 클래스를 JSON 형태의 객체로 바꿔줘야 한다.

그걸 바꿔주는역할을 하는 converter Factory를 retrofit과 함께 써야하므로 얘도 구글링해서 import 해준다

// app 수준 gradle에 추가해주기

// retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// retrofit converter Factory
implementation("com.squareup.retrofit2:converter-gson:2.9.

 

4-2. builder 생성

그리고 application class 에서도 onCreate()가 있는데, 앱이 실행될 때 가장 실행되는 메서드이다.

여기에 아까 lateinit var로 초기화를 지연시킨 retrofit 변수를 여기 onCreate에서 초기화시켜주겠다.

// 레트로핏 빌더
retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL) // 빌더 옵션. baseUrl도 달아주고
    .addConverterFactory(GsonConverterFactory.create()) // 아까 다운받은 컨버터 팩토리도 달아줌
    .build()

빌더로 retrofit을 초기화시키고 몇가지 설정도 달아준다.

컨버터 팩토리도 여러가지가 있는데 우리는 Gson을 사용하는 걸 공부할 것이므로 GsonConverterFactory를 사용해보도록 할것이다

 

// ApplicationClass.kt 전체코드
package com.example.carrotmarket

import android.app.Application
import retrofit2.Retrofit
import retrofit2.Retrofit.Builder
import retrofit2.converter.gson.GsonConverterFactory

class ApplicationClass : Application() {
    companion object{
        const val DEV_URL : String = "http://13.125.254.172:23899"
        const val PROD_URL : String = "http://kuit_prod_url"

        const val BASE_URL : String = DEV_URL

        lateinit var retrofit : Retrofit
    }

    override fun onCreate() {
        super.onCreate()

        retrofit = Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

이렇게 빌더가 완성됐다.

 

(+ 강의에서는

retrofit = Retrofit.Builder()

이렇게 빌더를 달아주는데

내 코드에서 import 하려고 하면

retrofit = Builder()

이런식으로 된다.

 

import문도

(강의) : import retrofit2.Retrofit

(내 코드) :

import retrofit2.Retrofit
import retrofit2.Retrofit.Builder

이렇게 빌더가 따로 import된다.

안드로이드 스튜디오 버전이 달라서 그럴지도 모르겠다)

 

4-3. retrofit 인터페이스 만들기

데이터클래스를 먼저 만들어줄거다

4-3-1.디렉토리 생성

파일이 너무 많으니까 디렉토리를 새로 생성하자.

package를 새로 만들어주자

네트워크 통신이라는 의미에서 remote라고 덧붙여서 디렉토리를 생성한다.

 

4-3-2. 레트로핏 인터페이스 만들기

새로만들어진 remote라는 디렉토리에 RetrofitInterface라는 새로운 "Interface"를 만들어준다.

 

레트로핏은 어노테이션과 인터페이스 기반으로 동작하는 라이브러리이기 때문에,

인터페이스에서 아까 봤던 API 명세서에 대한 내용을 정의해줘야 한다.

// 레트로핏 인터페이스 중간코드
interface RetrofitInterface {
    @POST("users/signup")
    fun signup()

}

 

4-3-3. request의 body를 담을 데이터클래스 만들기

 

SerializedName 어노테이션을 선언해주고 변수에 실제 요청할때 사용될 키값을 매핑해준다.

val 뒤의 변수명은 코드 내부에서 사용되기 때문에 키값과 달라도 되지만, 편의상 통일해주면 좋다.

// AuthEntity.kt 파일
// Request body에 대한 data class
data class SignUpRequest( 
    @SerializedName("userId") val userId : String,
    @SerializedName("password") val password : String,
    @SerializedName("nickname") val nickname : String,

//    "userId" : "kuit@konkuk.ac.kr",
//    "password" : "2023kuit!@#",
//    "nickname" : "yaho"
)

 

Request body에 대한 data class를 만들어줬으므로 Response body에 대한 data class 도 만들어주자

 

4-3-4. response body를 담을 데이터 클래스도 만들기

 

api 명세서를 보면 공통점이 있는데, 응답 실패여부와 상관 없이 isSuccess 값은 들어온다는 것이다.

똑같은 부분을 매번 하드코딩할 필요는 없으므로

BaseResponse라는 객체를 만들어주자(데이터 클래스 파일)

 

근데 이때, isSuccess, code, message와 달리

result에는 

"result": {
        "userId": "hi"
    }

이렇게 알 수 없는 "객체"가 들어온다.

그러므로 이걸 데이터클래스에서는 String이나 Int 등의 정해진 타입이 아닌

제너릭(T)으로 받을 것이다.

 

제너릭이란, 어떤 객체가 들어와도 그걸 T로서 그대로 그냥 쓰겠다는 의미이다.

data class BaseResponse <T> () 이런식으로 데이터 클래스 이름 옆에도 제너릭을 사용하겠다는 선언을 해줘야 한다.

// BaseResponse 데이터 클래스 전체코드
package com.example.carrotmarket

import com.google.gson.annotations.SerializedName

data class BaseResponse<T>(
    @SerializedName("isSuccess") val isSuccess : Boolean,
    @SerializedName("code") val code : Int,
    @SerializedName("message") val message : String,
    @SerializedName("result") val result : T,
//    Response body 복붙
//    "isSuccess": true,
//    "code": 201,
//    "message": "요청에 성공하였습니다.",

)

 

다시 AuthEntity로 와서 Response body를 구현해주자.

아까 data class SignUpRequest()를 만들어줬듯이

그 아래에 data class SignUpResponse() 를 만들어주고.

api 로직에 맞는 객체만 생성해주면 된다.(?) (매번 똑같은 부분은 로직에 영향받지 않으므로 로직에 맞지 않는단건가)

아무튼 특이점이 있는건 result이다. 그 안의 userId를 받아주자.(❓왜 result가 아닌 userId를 받는 것인지???)

data class SignUpResponse(
@SerializedName("userId") val userId : String
)

 

// 데이터 클래스 전체 코드
package com.example.carrotmarket.remote

import com.google.gson.annotations.SerializedName

data class SignUpRequest( // Request body에 대한 data class
    @SerializedName("userId") val userId : String,
    @SerializedName("password") val password : String,
    @SerializedName("nickname") val nickname : String,

//    "userId" : "kuit@konkuk.ac.kr",
//    "password" : "2023kuit!@#",
//    "nickname" : "yaho"
)

data class SignUpResponse(
    @SerializedName("userId") val userId : String
)

 

이로써 레트로핏을 사용하기 위한 세가지(빌더, 인터페이스, 데이터클래스) 중에서 데이터클래스가 완성됐다.

마지막으로 레트로핏 인터페이스를 완성할 차례이다.

 

아까 요청에는 @POST로 명시를 해줬다.

함수 안에 @Body 어노테이션 써주고

데이터타입을 아까 만들어준 데이터 클래스로 SignUpRequest를 써준다.

그리고 그 함수의 반환형을 Call 객체로 해준다.(Response와의 차이점을 알아두자)

fun signup() : Call<BaseResponse<>> 여기까지 해주면 BaseResponse<>의 꺽쇠 안에 T를 넣어달라 한다.

왜냐하면 아까 BaseResponse(똑같은 부분이라서 따로 데이터 클래스 만든 파일)에 T를 선언해놔서 명시해줘야하기 때문이다.

fun signup() : Call<BaseResponse<T>>

이렇게 T를 넣으면

data class BaseResponse <T>(
    ...
    @SerializedName("result") val result : T // 이 부분이
    @SerializedName("result") val result : SignUpResponse // 마치 이 부분처럼 동작할 것이다.
)

 

// 레트로핏 인터페이스 전체 코드
package com.example.carrotmarket.remote

import com.example.carrotmarket.BaseResponse
import retrofit2.Call // Retrofit 관련된 걸 import 해야하므로 주의하자
import retrofit2.http.Body
import retrofit2.http.POST

interface RetrofitInterface { 
    @POST("users/signup")
    fun signup(
        @Body request: SignUpRequest
    ) : Call<BaseResponse<SignUpResponse>>
}

그니까 달라지는 부분만 받아서 뼈대(BaseResponse)에 넣어서 Call의 인자로 받는다.

 

이렇게 인터페이스까지 완성이 됐다.

 

다음 글에서는 실행하는 로직을 구현해보겠다.