캡스톤 프로젝트를 진행하면서 환경구성 관련 내용에 대해 공부를 하게 되었습니다. 그 중
안드로이드 <-> 서버에 대한 통신쪽을 맡게 되었는데, 어떻게 동작하는지 알아보다가 Retrofit이라는 library를 사용하기로 결정했습니다.
이전 포스팅에서 어느정도 다루긴 했지만, 실제로 사용하는 것을 해보지 않아서 직접해보려고 합니다.
생각보다 너무 어려운 것......
Retrofit은 Square Inc.에서 개발된 오픈 소스 라이브러리로, 안드로이드 앱 개발을 위한 RESTful API 호출을 간편하게 처리할 수 있습니다.
- 인터넷 퍼미션 추가
- retrofit 라이브러리 추가
- api구성(서버) 후 어떻게 받을 지 명세(클라이언트)
이전 포스팅에서 설정관련은 다루었으니, 실제로 동작하는 법에 대해 포스팅하겠습니다.
많은 retrofit 예제가 블로그에 존재했고, 사용자별로 모두 다 다르게 설명해주었지만 큰 틀은 같았습니다.. 하지만 이해가 안되었고 그 중 제일 간단하게 설명해주신 잡다한 IT 개발이야기님의 블로그가 매우 참고되었습니다.
우선 API명세를 기반으로 서버에서 데이터를 어떻게 주고 받을 것인지 dataclass를 생성해줍니다.
이말은 즉슨 REQ, RES로 주고 받을 데이터 클래스를 생성해주어야 한다는 뜻입니다.
저희 팀의 경우 POST요청으로 REQEST - login data(email, pw)를 보내면, 서버에서는 RESPONSE - httpCode, message, token(JWT)를 받는 것을 예제로 들어보겠습니다.
먼저, RES로 서버로부터 받을 데이터클래스를 생성해줍니다.
// PostResLoginData
data class PostResLoginData(
@SerializedName("code") val status: Int,
@SerializedName("message") val message: String,
@SerializedName("token") val token: String
)
여기서 @SerializedName은 서버에서 전송한 데이터(Json)의 속성명과@SerializedName의 속성명을 일치시킨 후, 뒤의 변수는 안드로이드에서 사용할 변수명으로 지정할 수 있다.
그리고, REQ로 서버에 줄 데이터를 기반으로 데이터클래스를 생성해줍니다.
// PostReqLoginData
data class PostReqLoginData(
val email: String,
val pw: String
)
@GET, @POST, @Header, @Query, @Body ...interface JwtApi{
@Headers("Content-Type: application/json") //@Headers 어노테이션 이용해 헤더값 넣어주기
@POST("user/signin") //HTTP 메소드를 설정해주고 API와 URL 작성
fun postLogin(
//@Body 어노테이션을 통해 RequestBody 데이터를 넣어준다.
@Body postReqLoginData: PostReqLoginData) : Call<PostResLoginData>
}
위의 api인터페이스의 내용은
@Header -> Json으로 받겠다.
@POST(url) -> 이 url로 서버에 POST통신을 하겠다
@Body REQ 변수명 : REQ 데이터클래스 -> REQ 변수의 내용을 Body에 넣어서 서버에 전달할거다.
postLogin함수 -> 이 함수의 타입은 RES 객체다.
Retrofit을 사용하기 위해 Retrofit 객체를 object타입으로 생성해줍니다.
// ServiceCreator.kt
object ServiceCreator { //서비스를 생성해주는 구현체 부분
private const val BASE_URL = "http://10.0.2.2:8000/" // 안드로이드 localhost주소: 10.0.2.2
//Retrofit 객체 생성
private val retrofit: Retrofit = Retrofit
//레트로핏 빌더 생성 (생성자 호출)
.Builder()
//빌더 객체의 baseUrl 호출. 서버의 메인 URL 전달
.baseUrl(BASE_URL)
//gson 컨버터 연동
.addConverterFactory(GsonConverterFactory.create())
//Retrofit 객체 반환
.build()
//인터페이스 객체를 create에 넘겨 실제 구현체 생성
val jwtApi : JwtApi = retrofit.create(JwtApi::class.java)
}
// MainActivity.kt
class MainActivity : AppCompatActivity() {
//val email : String = "comet1010@naver.com"
//val pw : String = "1234"
//val requestData = PostReqLoginData(email,pw)
private lateinit var binding : ActivityMainBinding // viewbinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val bntEvent = binding.subBtn
val jwtApi : JwtApi = ServiceCreator.jwtApi
bntEvent.setOnClickListener{ // 버튼을 눌렀을 때 이벤트 발생 리스너
// 조금 헷갈릴 수 있지만, TextInputEditText을 사용하였고,
// text.toString()으로 데이터클래스의 String 타입 맞춰줌
val email : String = binding.ETemail.text.toString()
val pw : String = binding.ETpw.text.toString()
// api인터페이스에 postLogin함수를 사용하기 위해
// PostReqLoginData 데이터클래스에 값을 넣어서 객체 생성해줌
val requestData = PostReqLoginData(email,pw)
// api인터페이스의 postLogin함수에 PostReqLoginData의 객체를 넣음으로서
// @Body 객체가 생성되고, url에 데이터를 쏘게됨. 그리고 반환 값으로 서버에서 데이터를 받을 텐데,
// 타입은 PostResLoginData로 받음.
val callPostJwtApi = jwtApi.postLogin(requestData)
// 여기는 함수로 구현
JwtPost(callPostJwtApi);
// token값의 nullcheck. 이거는 jwt받으면 출력.
App.token_prefs.accessToken?.let { it1 -> Log.d(TAG, it1) }
}
}
// jwt사용 함수
fun JwtPost(callPostJwtApi : Call<PostResLoginData>) {
// 비동기 처리하겠다는 의미
callPostJwtApi.enqueue(object : Callback<PostResLoginData> {
// 정상적으로 RES데이터를 받았을 경우
override fun onResponse(
call: Call<PostResLoginData>,
response: Response<PostResLoginData>
) {
Log.d(TAG, "성공 : ${response.body()}")
// sharedPreference저장을 사용함. 이거는 따로 포스팅하겠음
App.token_prefs.accessToken = response.body()?.token
// Log.d(TAG, "token : ${App.token_prefs.accessToken}")// sharedPreference에 데이터 저장 후 호출
}
// RES 데이터를 받는데 실패한 경우
override fun onFailure(call: Call<PostResLoginData>, t: Throwable) {
Log.d(TAG, "POST 실패 : $t")
}
})
}
}

값 넣어서 보내보겠습니다.

네~ 잘 받아와 지네요. 데이터를 sharedPreference 객체를 통해 저장하고, GET요청을 해볼텐데요. JWT토큰을 저장소에 저장해두었다가, api 호출을 할 때마다 사용하여 사용자 인증을 하겠습니다.
POST와 똑같이 서버에서 받을 데이터클래스를 생성해줍니다.
// GetResData.kt
data class GetResData (
val message : String
)
하지만, 여기서 POST와 약간 다른 점은 GET의 API의 경우 http Header에 token: JWT토큰 값만 입력하여 /signup/jwt에 접근하는 방식이기에 클라이언트에서 보낼 json데이터가 필요하지 않습니다.
포스트맨으로 보여드리면 다음과 같습니다.

Post와 같이 api인터페이스에 함수를 생성할 것입니다.
다른점은 @GET, @Header 어노테이션을 사용한다는 점입니다.
// JwtApi.kt
@GET("/user/signup/jwt")
fun getLogin(
@Header("token") token : String
//@Query()
) : Call<GetResData>
접근할 api명세를 하고, http Verb 값은 GET이기 때문에 @GET을 사용합니다.
그리고 @Header를 통해 key이름을 지정해줍니다.
value값은 나중에 메인에서 함수 호출할 때 토큰 값을 넣어줄 것입니다.
@Query는 따로 작성하지 않았지만, GET요청시 안드로이드에서 서버로 데이터를 보낼 때 사용하는 어노테이션입니다. json 데이터 보낼 때 사용됩니다.
// MainActivity.kt
// checkToken 버튼 눌렀을 때 이벤트.
checkEvent.setOnClickListener{
// app.token_prefs.accessToken = 저장되어 있는 JWT토큰.
// 아래 구문은 token의 null확인 후 동작하겠다는 의미이고, getLogin함수에 토큰값 입력
val callGetJwtCheck = App.token_prefs.accessToken?.let { it1 -> jwtApi.getLogin(it1) }
// token값이 null 확인을 하기 때문에 포함되어 있는 callGetJwtCheck도 null check
callGetJwtCheck?.let { it1 -> jwtCheck(it1) }
}
// JwtPost함수와 같이 데이터를 잘 받았는지 확인 후 동작하는 함수
fun jwtCheck(callGetJwtCheck : Call<GetResData>){
callGetJwtCheck.enqueue(object : Callback<GetResData> {
override fun onResponse(
call: Call<GetResData>,
response: Response<GetResData>
) {
Log.d(TAG, "GET성공 : ${response.body()}")
Log.d(TAG, "token : ${App.token_prefs.accessToken}")
// sharedPreference에 데이터 저장 후 호출
}
override fun onFailure(call: Call<GetResData>, t: Throwable) {
Log.d(TAG, "GET 실패 : $t")
}
})
}
이 부분은 크게 다르지 않습니다.
POST와 다른점은 저장소를 사용해서 token을 저장하였고, api인터페이스의 GetLogin함수에 토큰 값을 넣었다는 점입니다. token값은 null일 수 있기 때문에 null check과정이 포함됩니다.
토큰을 보내면 서버에서는 {message: "~님 환영합니다"} 로 응답할 것입니다.
동작되는지 확인해보겠습니다.

네 JWT검증을 마치고, GET요청이 성공했습니다!
이렇게 Retrofit으로 GET, POST작업을 수행해보았습니다.