본문 바로가기

안드로이드 앱개발

Retrofit2를 이용한 안드로이드와 스프링 서버 통신(안드로이드편)(안드로이드 서버통신)

반응형

안녕하세요. 오늘은 Retrofit2를 이용한 안드로이드와 스프링 서버 통신에 대하여 작성해보겠습니다.

 

1. Retrofit2란

Retrofit2는 Android 및 Java 애플리케이션에서 HTTP 네트워크 통신을 쉽게 수행할 수 있도록 해주는 

타입 안전 HTTP 클라이언트 라이브러리입니다. 주로 RESTful API와의 통신을 위해 사용됩니다.
Retrofit은 간결하고 사용하기 쉽도록 설계되었으며, 

Gson, Moshi 등의 JSON 파서와 함께 사용하여 데이터를 직렬화/역직렬화할 수 있습니다. 

또한, Retrofit2는 비동기/동기 요청, 파일 업로드, 멀티파트 폼 데이터 전송 등을 지원합니다.

 

2. Retrofit2를 이용한 Android와 Spring 연결하기

1) Spring

https://pinlib.tistory.com/entry/스프링으로-모바일-앱-서버-만들기

 

스프링으로 모바일 앱 서버 만들기

다시 실습편으로 찾아오게 되었습니다. 전에 이론편에서 말씀드렸지만, 사실 이미 제가 현재 진행하고 있는 mobile application의 sever는 이미 개발 중이었습니다. 허나, 추후에 mobile application을 추가

pinlib.tistory.com

 

제가 이전에 작성한 글입니다. 사전에 이 글을 참조하시면 좋을 것 같습니다.

또한 이전에도 retrofit2관련 글을 작성한적 있는데 이번에 업그레이드해서 2편으로 가져오겠습니다.

 

 

2) Android

제가 실제로 Secret Diary라는 개인 프로젝트를 진행하며 사용했던 코드들로 예시를 보여드리겠습니다.

 

1) Dependency

buildGradle(App수준)

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

 

build.gradle(app수준)에 해당 dependencies를 추가해주세요

 

2) singleton object

object SecretDiaryObject {

    private const val BASE_URL = "http://10.0.2.2:8080/"


    private val okHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(RedirectInterceptor())
            .build()
    }

    private val getRetrofitSD by lazy {
        Retrofit.Builder()
            .baseUrl(SecretDiaryObject.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
            .build()
    }

    val getRetrofitSDService : SecretDiaryAPI by lazy { getRetrofitSD.create(SecretDiaryAPI::class.java) }

    fun getInstance(): SecretDiaryAPI? {
        return getRetrofitSDService
    }
}

 

이 코드는 kotlin의 object 싱글톤을 정의하여, Retrofit을 통해 네트워크 통신을 간편하게 할 수 있도록 설정해둔 객체입니다.

 

BASE_URL의 경우 현재는 Android Emulator에서 spring이 작동하는 localHost를 가르키는 ip입니다.
추후 다른 PC나 사설 서버에서 작동하는 spring서버에 연결하려면 우리가 아는 일반적인 ip주소로 설정하시면 됩니다.

 

 

또한, 해당 코드를 보시면 RedirectInterceptor라는 함수를 호출하고 있습니다.

class RedirectInterceptor : Interceptor {
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        var request: Request = chain.request()
        var response: Response = chain.proceed(request)
        var redirectCount = 0

        while (response.isRedirect && redirectCount < 5) { //5
            val location = response.header("Location") ?: break
            request = request.newBuilder().url(location).build()
            response = chain.proceed(request)
            redirectCount++
        }

        if (redirectCount >= 5) { //5
            throw java.net.ProtocolException("Too many redirects: $redirectCount")
        }

        return response
    }
}

 

해당 코드의 경우 HTTP 요청이 여러번 redirect되는 상황을 처리하고, 

redirect가 너무 많이 발생하면 예외를 발생시키기 위해 설정하였습니다.

 

3) Retrofit2 interface

interface SecretDiaryAPI {

}

 

이 파일 안에는 retrofit2를 사용하여 spring API와 통신하기 위한 SecretDiaryAPI라는 interface를 정의하고 있습니다. 
이 interface에는 다양한 endPoint를 정의하여 CRUD와 같은 다양한 API method들을 작성해주시면 됩니다.

 

 

3. CRUD 구현

1. API Method 작성방법

@POST("security/join") //client ->db
    suspend fun joinUser(@Body userModel: UserModel) : Response<Void>

 

대부분의 API Method는 위와 같은 형식으로 작성하며 위에서 본 SeacretDiaryAPI와 같은 interface 파일안에 작성하시면 됩니다.

 

1) suspend란

위 예시 코드를 보시면 suspend라는 키워드가 있습니다.

기본적으로 retrofit2 통신을 이용한 method의 구현에서는 상단에 suspend를 붙이고 사용합니다.
suspend란 kotlin에서 coroutine 내에서 비동기 처리를 할 때 사용됩니다. 
suspend는 호출이 비동기적으로 이뤄지며, 해당 함수가 완료될 때까지 호출한 스레드는 차단되지 않습니다.
즉, suspend를 붙임으로서 coroutine내에서 비동기적으로 실행시키고, 

coroutine이 완료될 때까지 다른 작업이 차단되지 않아 애플리케이션이 계속 작동되게 합니다.
네트워크와 같은 비동기처리를 필요로하는 기능 구현을 위해서는 suspend는 반드시 필요합니다.


물론, 사용하지 않는 방법도 있는데 retrofit2에서 제공하는 Call객체를 이용하는 방법입니다.

@GET("findAll2")
fun readAll(): Call<List<RNoticeModel>>

 

허나 이 방법은 추후 viewModel에서 해당 method를 사용하는데 더 많은 코드를 작성해야 하므로
kotlin coroutine이 권장하는 suspend를 사용하는 것을 추천드립니다.

 

3) Response란

위에서 보여드린 예시 코드에서는 return을 Response객체로 하고 있습니다.
Response<T>는 Retrofit2에서 네트워크 요청의 결과를 직접적으로 반환하는 타입입니다.
Response 타입의 객체는 해당 요청의 응답 값을 즉시 반환할 수 있으며, 이 객체를 통해 요청의 성공 또는 실패를 판단할 수 있습니다. 
Response는 메소드 호출 시점에 결과를 바로 제공하므로 coroutine기반 비동기 처리에 적합합니다.

 

이제부터 제가 SecretDiary를 만들며 실질적으로 사용한 CRUD 코드의 예시를 하나씩 보여드리겠습니다.


이번 게시물에서는 안드로이드 코드 구현 방식만 작성하고, 2편에서 spring코드 구현 방식을 보여드리겠습니다.

 

2. viewModel에서 API Method호출하며 retrofit2통신 구현

1) Create

@POST("security/join") //client ->db
    suspend fun joinUser(@Body userModel: UserModel) : Response<Void>

 

API 메소드는 위와 같은 형식으로 작성합니다. 이 코드는 회원가입한 유저의 정보를 DB에 저장하기 위한 코드입니다.

여기에 있는 @Body annotation의 경우 Retrofit2 interface에서 HTTP 요청의 본문에 데이터를 매핑하는 역할을 합니다.

메서드의 파라미터 앞에 @Body 어노테이션을 붙이면, 해당 파라미터가 요청 본문으로 변환됩니다.

일반적으로 @Body는 JSON 형태로 데이터를 서버에 전송할 때 사용되며,

Retrofit은 내부적으로 데이터 객체를 JSON 형식으로 자동 변환하여 요청 본문에 포함합니다.

 

저의 경우 userModel이라는 DataClass를 정의하여 해당 Data를  @Body annotation으로 감싸서 한번에 spring서버로 보내줍니다.

data class UserModel(

    @SerializedName("id")
    val id: String,

    @SerializedName("name")
    val name: String,

    @SerializedName("email")
    val email: String,

    @SerializedName("password")
    val password: String,

)

 

 

@SerializedName은 Gson 라이브러리에서 사용되는 어노테이션으로, JSON 필드 이름과 Kotlin 클래스 속성 간의 매핑을 정의하는 데 사용됩니다. 이 어노테이션을 사용하면 JSON에서 특정 필드 이름이 다른 이름으로 Kotlin 객체의 속성에 매핑될 수 있습니다.

예를 들어, JSON에서의 필드 이름과 Kotlin 클래스의 필드 이름이 다를 때, @SerializedName을 통해 어떤 JSON 키가 어떤 클래스 필드에 매핑되는지 명시적으로 지정할 수 있습니다.

 

fun onSignUp() {
        // Handle sign up logic
        val model = UserModel(id, name, email, password)
        viewModelScope.launch(Dispatchers.IO) {
            val response = SecretDiaryObject.getRetrofitSDService.joinUser(model)
            if(response.isSuccessful){
                Log.d("Join","Join Success")
            } else {
                Log.e("Join","Failed Join")
            }
        }
    }

 

이제는 interface에서 만든 API method를 viewModel에서 호출할 차례입니다.

네트워크 작업이므로 viewModelScope에 감싸서 coroutine으로 retrofit method를 호출하여 작업합니다.
UserModel에 각 값을 저장하여 한번에 데이터를 서버로 보내줍니다.

 

2) Read

@GET("read/notice/user")
    suspend fun readUserNotice(@Query("userEmail") query: String): Response<List<RNoticeModel>>

 

@Query는 Retrofit에서 사용되는 어노테이션으로, URL의 쿼리 파라미터(Query Parameter)를 지정할 때 사용됩니다.

이는 서버로 API 요청을 보낼 때, URL에 쿼리 파라미터를 추가하여 데이터를 필터링하거나 특정 조건을 전달할 수 있도록 해줍니다.

 

@Query("name") 어노테이션이 사용된 name 파라미터는 URL에 쿼리 파라미터로 추가됩니다.

예를 들어, name 파라미터에 "John" 값을 전달하면, 실제 호출되는 URL은 아래와 같이 생성됩니다

https://example.com/read/notice/user?name=John

 

해당 Api Method는 유저의 이메일에 따른 게시물을 읽어오기 위한 코드이므로 @Query를 통해 user의 이메일을 parameter로 notice를 가져오는 것입니다.

 

fun readMyNotice(){
        viewModelScope.launch(Dispatchers.IO) {
            val userEmail = userRepository.getMostRecentUserName()
            val response = SecretDiaryObject.getRetrofitSDService.readUserNotice(userEmail!!)
            if(response.isSuccessful){
                response.body()?.let {
                    _notices.value = it
                }
            } else {
                response.errorBody()?.let {
                    Log.d("read user notice", "Error body: ${it.string()}")
                }
            }
        }
    }

 

위 코드도 create예시와 비슷하나,  _notices.value에 서버로부터 받아온 값을 저장하고 있습니다.

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val userRepository: UsersRepository
) : ViewModel(){

    //read All Notice
    private val _notices = MutableStateFlow<List<RNoticeModel>>(emptyList())
    val notices: StateFlow<List<RNoticeModel>> = _notices

 

해당 method를 호출하는 HomeViewModel에서는 _notices와 notices를 변수로 작성하였습니다.

 

이를 이해하기 위해서는 우선적으로 homeViewModel을 사용하는 UI의 코드를 알 필요가 있습니다.

val notices by homeViewModel.notices.collectAsState()

 

이 코드는 UI에서 작성하며 read method를 이용해 받아온 notice를 UI에 적용시키기 위한 코드입니다.

 

viewModel에서 사용하는 MutableStateFlow 상태를 가지는 Flow로, 값의 변화를 지속적으로 관찰할 수 있습니다.

 

여기서 Flow란 Flow는 Kotlin의 비동기 데이터 스트림을 처리하기 위한 도구로, 데이터를 연속적으로 방출하고, 이를 소비자가 비동기로 처리할 수 있도록 도와줍니다. Flow는네트워크 요청, 데이터베이스 쿼리 등 비동기 작업을 더 간결하고 효율적으로 수행할 수 있습니다.

 

StateFlow MutableStateFlow의 읽기 전용 버전으로, 외부에서 값을 변경하지 못하도록 제한합니다.

 

StateFlow는 Jetpack Compose와 같은 UI 프레임워크와 잘 맞으며, 값이 변경될 때마다 collect를 통해 UI에서 이를 감지하고 업데이트할 수 있습니다.

 

collectAsState의 경우 Flow Compose 상태로 변환하여, Flow의 데이터를 UI와 쉽게 연결할 수 있도록 합니다.

Flow가 방출하는 데이터가 변경될 때마다, collectAsState를 통해 변환된 Compose 상태가 업데이트되고, 이에 따라 UI도 자동으로 재구성됩니다.

 

 

정리하자면

1. _notices

 

  • MutableStateFlow<List<RNoticeModel>> 타입으로, List 형태의 RNoticeModel 데이터를 저장합니다.
  • 초기값은 emptyList()로 설정되어, 처음에는 비어 있는 리스트로 시작합니다.
  • MutableStateFlow를 사용하여 공지사항 리스트가 변경될 때마다 구독하고 있는 UI 또는 다른 로직에서 자동으로 업데이트를 감지합니다.

2. notices

 

  • StateFlow<List<RNoticeModel>> 타입으로, notices는 MutableStateFlow인 _notices의 읽기 전용 속성입니다.
  • 외부에서는 notices를 통해서만 데이터를 읽을 수 있으며, 직접 값을 변경할 수 없습니다.
  • 이러한 방식으로 **캡슐화(encapsulation)**를 통해 데이터 변경을 ViewModel 내부에서만 제어하도록 합니다.

따라서 MutableStateFlow와 StateFlow를 사용함으로써, notices 리스트에 새로운 공지사항 데이터를 추가하거나, 읽어온 데이터를 저장할 때 readMyNotice() 메서드에서 _notices.value를 사용하여 값을 업데이트하고, UI에서는 notices를 구독하여 변경을 감지하게 됩니다.

 

읽기의 핵심은 retrofit2 method 정의보다는 MutableStateFlow와 StateFlow, collectAsState이므로 유의해주시기 바랍니다.

 

 

 

 

3) Update

@Multipart
    @PUT("security/update/{userEmail}")
    suspend fun updateUser(
        @Path("userEmail") userEmail: String,
        @Part("userNickName") userNickname: String,
        @Part("userText") userText: String,
        @Part userImg: MultipartBody.Part
    ): Response<ResponseBody>

 

유저정보를 update하기 위한 API Method입니다.

 

@Multipart와 @Part의 경우 추후 다룰 내용이니 간단하게만 설명드라자면

@Multipart의 경우 사용자가 프로필 이미지를 업로드하거나, 게시글에 이미지를 첨부할 때 멀티파트 요청을 사용합니다.

@Multipart 어노테이션이 선언된 메서드에서는 각 파라미터 앞에 @Part 어노테이션을 붙여 요청의 각 파트를 정의할 수 있습니다.

예를 들어, @Part를 사용하여 텍스트, 이미지, 동영상 등 서로 다른 종류의 데이터를 하나의 요청으로 보낼 수 있습니다.

 

@Path의 경우 @PUT의 parameter인 {userEmail}에 들어갈 변수를 지정하는 것으로 @Path에 지정한 값을 경로값으로 지정하는 것입니다.

 

ResponseBody는 네트워크 응답의 본문(response body)를 의미하며, Retrofit의 Response는 해당 본문을 감싸는 형태로 전체 응답을 처리합니다.

ResponseBody는 HTTP 응답 데이터(예: JSON, XML 등)를 원시형식으로 다루는 클래스입니다.

따라서, Response<ResponseBody>는 응답 데이터를 가공하지 않고 그대로 반환할 때 사용합니다.

 

viewModelel에서의 호출의 경우 위 Create와 같은 형식이므로 생략하겠습니다.

 

 

4) Delete

@DELETE("delete/{userEmail}")
    suspend fun deleteUser(/*@Header("Authorization") token: String, */@Path("userEmail") userEmail: String): Response<ResponseBody>

 

이 API Method는 회원탈퇴를 위한 method입니다.

 

@Header는 Retrofit에서 HTTP 요청 헤더(header)를 설정할 때 사용하는 어노테이션입니다.

이에 대한 내용은 추후 spring security편에서 작성하도록 하겠습니다.

Delete API Method에 대한 내용은 위 내용과 비슷하므로 생략하겠습니다.

 

viewModel에서의 호출 역시 위 내용들과 같은 형식이므로 생략하겠습니다.

 

 

여기까지 Retrofit2를 이용한 안드로이드와 스프링 서버 통신 안드로이드편을 마무리하겠습니다.

다음번에는 Retrofit2를 이용한 안드로이드와 스프링 서버 통신 스프링편으로 찾아뵙겠습니다.

반응형