디자인이 확정되지 않아 XML과 액티비티에서 최소한의 레이아웃만 개발한 후, 서버 통신 부분을 먼저 구현하였습니다.
우선 간단한 GET 방식의 API부터 연결해보았습니다.
백엔드 쪽에서 포스트맨(Postman)을 이용해 테스트할 수 있는 API를 정의했고, 이를 통해 테스트를 진행했습니다.
해당 API는 버전(version) 값을 응답하며, 이는 스플래시 화면에서 조회 후 강제 업데이트 등 버전 관리 기능에 사용될 예정입니다.

서버 통신에 사용할 엔드포인트와 요청 메소드를 정의한 인터페이스 작성
interface ApiService {
@GET("api/v1/version")
fun getVersion(): Call<ApiResponse<VersionPayload>>
// 생략
}
API 응답 데이터를 체계적으로 관리하기 위해, 응답에서 특정 필드를 추출할 수 있도록 데이터 클래스를 정의
data class VersionPayload(
val id: Number,
val version: String
)
API 호출에 필요한 Retrofit 객체를 전역에서 쉽게 사용할 수 있도록 이를 다른 파일에서 선언하기 위해 NetworkService 객체를 생성
object NetworkService {
fun getService(): ApiService = retrofit.create(ApiService::class.java)
private val retrofit =
Retrofit.Builder()
.baseUrl(SERVER_URL)
.client(provideOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build()
// 생략
}
getService() 메서드를 통해 ApiService 인터페이스에서 정의된 엔드포인트를 가져옵니다.GsonConverterFactory를 추가하였습니다.provideOkHttpClient() 함수에는 타임아웃 설정 및 Interceptor를 추가해, 헤더에서 토큰 추출 등의 기능을 구현했습니다.
서버에서 받아온 데이터를 처리할 수 있도록 네트워크 요청을 처리하는 객체를 만들어, 응답 결과에 따라 성공 또는 실패를 처리할 수 있도록 구성했습니다.
object CommonRepo {
fun getVersion(
networkFail: (String) -> Unit,
success: (ApiResponse<VersionPayload>) -> Unit,
failure: (Throwable) -> Unit
) {
NetworkService.getService().getVersion()
.enqueue(object : Callback<ApiResponse<VersionPayload>> {
override fun onResponse(
call: Call<ApiResponse<VersionPayload>>,
response: Response<ApiResponse<VersionPayload>>
) {
if (response.isSuccessful) {
val data = response.body() ?: return
success(data)
} else {
networkFail(response.code().toString())
}
}
override fun onFailure(call: Call<ApiResponse<VersionPayload>>, t: Throwable) {
failure(t)
}
})
}
}
getService()를 통해 getVersion() 메서드를 호출하여 실제 API 요청을 실행합니다.response.body())를 success()로 전달하고, 실패 시에는 networkFail()로 처리합니다.VersionPayload 데이터 클래스를 통해 response.body()에서 값을 바로 사용할 수 있으며, it.payload.version과 같이 데이터를 쉽게 추출할 수 있습니다.
JWT 토큰을 자동으로 처리하고, 서버와의 보안 통신을 원활하게 하기 위해 Interceptor 설정을 추가했습니다.
NetworkService 소스에서 Retrofit 객체를 설정할 때 사용한 provideOkHttpClient() 함수는 OkHttpClient를 구성하고 다양한 설정을 추가합니다. 그 중 주요 Interceptor는 다음과 같습니다:
서버에서 받은 응답에서 토큰을 추출하여 저장할 수 있도록 ReceiveInterceptor를 작성했습니다.
class ReceiveInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val pref = Constants.context?.getSharedPreferences("dataPreferences", Context.MODE_PRIVATE)
val original = chain.proceed(chain.request())
try {
Log.d(
"응답 값", GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(Gson().fromJson(original.peekBody(Long.MAX_VALUE).string(), JsonObject::class.java))
)
} catch (e: Exception) {
Log.d("응답 에러", e.message ?: "")
}
val jwtAccessToken = original.headers("X-ACCESS-TOKEN").getOrNull(0)
val jwtRefreshToken = original.headers("X-REFRESH-TOKEN").getOrNull(0)
val jwtTokenTime = original.headers("X-TOKEN-TIME").getOrNull(0)
jwtAccessToken?.let { pref?.edit()?.putString("access-token", it)?.apply() }
jwtRefreshToken?.let { pref?.edit()?.putString("refresh-token", it)?.apply() }
jwtTokenTime?.let { pref?.edit()?.putString("token-time", it)?.apply() }
return original
}
}
저장된 토큰을 API 요청에 자동으로 추가할 수 있도록 AddInterceptor를 구성했습니다.
class AddInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response = with(chain) {
val pref = Constants.context?.getSharedPreferences("dataPreferences", Context.MODE_PRIVATE)
val accessToken = pref?.getString("access-token", "")
val builder = request().newBuilder()
if (DataProvider.isLogin) {
builder.addHeader("X-ACCESS-TOKEN", accessToken ?: "")
}
builder.addHeader("User-Agent", "aos")
proceed(builder.build())
}
}
true로 변경되며, 이후의 요청부터는 accessToken이 자동으로 첨부됩니다.