Android(kotlin), 클라이언트-서버 통신을 위한 Retrofit, Retrofit 구현을 위한 모듈 분리와 의존성 주입

이도현·2023년 10월 16일
0

Android 공부

목록 보기
26/30
post-thumbnail

0. Retrofit이란?, 모듈 분리를 하는 이유

  • Retrofit: Rest API 통신을 위한 OkHttp라이브러리의 상위 구현체

    API: 프로그램들이 서로 통신하기 위한 매개체(함수, 데이터 등을 주고 받을 수 있다.)
    REST: 자원을 이름으로 구분하여 해당 자원의 상태를 주고 받는 모든 것
    REST API란 보다 복잡하고 원칙이 많은 개념이다.

  • 모듈 분리: 개발은 지속적인 유지보수가 필요하다. 한 모듈의 모든 소스코드가 중구난방하게 배치되어 있다면 무언가를 수정해야할 때 매우 불편할 것이다. 이를 위해 클린아키텍처가 필요하며 이는 보다 복잡한 개념이므로 다음에 알아보고
    우선은 data, presentation으로 개념적으로 소스코드를 구분하여 모듈과 패키지를 분리하여 구현하여보자.

1) data 모듈 생성 및 api 패키지 생성

  • 우선 data라는 이름으로 모듈을 생성한다.

  • data모듈에서 api 패키지를 생성해준다.(di는 일단 무시하자)

    app 모듈에선 UI와 app의 생명주기(Activity)를 위한 소스를 모아놓고
    data 모듈에선 app 모듈에서 사용될 데이터를 위한 소스들이 모여있다.

1. Retofit 기본 세팅

1) build.gradle(:module), 의존성 추가

  • gsonConverter는: json타입의 응답결과를 객체로 매핑(변환) 해주는 변환기이다.
// build:gradle(Modlue :data)
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
implementation 'com.squareup.retrofit2:retrofit-gson:2.6.4'

그냥 Json타입의 데이터를 호출한다면 데이터를 사용하기위해 어떤 형태로든 변환해주는 것이 데이터를 통제하는데 유리하다.
Json데이터가 아니더라도 어떤 데이터를 불러오는지 그 데이터의 타입에 변환이 필요한지 데이터 타입의 변환을 위해 유용한 라이브러리가 있는지 찾고 고려하는 것이 중요하다.

  • app 모듈에 data를 추가해줌으로써 data에 있는 소스를 app에서 사용할 수 있다.
// build:gradle(Modlue: app)

implementation project(:data)1

2) app/../AndroidManifest.xml

  • 네트워크 통신을 위해 빌드하기 위한 프로젝트 모듈에 권한을 추가해주어야 한다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
                 ....
                 ....
                 ...

2. Retrofit 기본적인 구조

  • Retrofit을 사용하여 api를 호출하기 위해선 4개의 파일이 필요하다.
    - 데이터를 위한 data class
    • API호출을 도와주는 Interface
    • retrofit 사용을 위한 객체
    • API를 호출히는 Class
  • 위 3개의 파일은 모두 data/.../api에 들어갈 것이다.
  • https://im.sexy.guy/hong 이라는 주소에 아래 데이터를 준비해두었다고하자.
{
  "Name": "홍길동",
  "age": 18, 
}

1) 데이터 클래스 생성

data class ApiResponse{
	@SerializedName("Name") val name : String,
    val age: Int, // @SerializedName이 없다면 변수명이 일치하여야 한다.
}

2) api호출을 도와주는 Interface

interface ApiService{
	@GET("{name}/") 
    suspend fun getData(@Path("name") currency: String): Call<ApiResponse> 
    //만약 im.sexy.guy/jung에서 데이터를 호출할 수 있으니 주소에서 @Path를 사용해
    //호출 주소에 변수를 넣을 수 있게한다. 필요없다면 변수 없이 함수 선언하고 GET안에
    //필요한 변수를 직접 넣어주면된다.
}

suspend란 코루틴스코프롤 통한 작업 처리중 언제든지 일시중지할 수있도록 하는 함수이다. suspend를 붙이지 않아도 됩니다. 필요애 따라서 fun getApi로 교체해도 좋습니다.

3) Retrofit 사용을 위한 객체, data/.../di

  • di패키지를 생성해준다. DI란 의존성 주입이며 전체 프로젝트를 보다 직관적으로 구성할 수 있고, 생성한 클래스들을 사용하는데 용이하게 해준다.
Object RetrofitModule{//

	fun proviedRetrofit(): Retrofit{
      return Retrofit.builder()
          .baseUrl("https://im.sexy.guy/")
          .addConverterFactory(GsonConverterFactory.create())
          .build()
    }
    
   	fun provideApiService(retrofit: Retrofit): ApiService{
    	return retrofit.create(ApiService::class.java)
    }
    
    fun provideApiServiceManager(ApiService: ApiService): ApiServiceManager {
    	return ApiServiceManager(ApiServiceManager)
    }
}

원래 의존성 주입을 위해선 라이브러리를 사용하는 것이 좋다.
'3. 의존성 주입'에서 다룰 것이다.

4) Api를 호출하는 클래스

class ApiServiceManager(private val apiService: ApiService) {

    // API 호출 메서드 예시
    suspend fun fetchData(name: String): ApiResponse? {
        try {
            val response: Response<ApiResponse> = apiService.getData(name)
            
            if (response.isSuccessful) {
                return response.body()
            } else {
                // API 호출이 실패한 경우 처리할 내용을 여기에 추가할 수 있습니다.
                return null
            }
        } catch (e: Exception) {
            // 네트워크 오류 등 예외 발생 시 처리할 내용을 여기에 추가할 수 있습니다.
            return null
        }
    }
}

3. 의존성 주입

1) 의존성 주입을 위한 Hilt 설치

  • Hilt는 안드로이드에서 사용되는 의존성 주입 라이브러리이며 dagger의 확장판이다.
//build.gradle(Project:example)
plugins{
	```
    ```
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}
//build.gradle(module:app)
plugins{
	kotlin("kapt")
    id "com.google.dagger.hilt.android"
    ...
	
}

dependencies{
    //hilt
    implementation "com.google.dagger:hilt-android:2.44"
    kapt "com.google.dagger:hilt-android-compiler:2.44"
}
//build.gradle(module:data)
plugins{
	kotlin("kapt")
    id "com.google.dagger.hilt.android"
    ...
	
}


dependencies{
    //hilt
    implementation "com.google.dagger:hilt-android:2.44"
    kapt "com.google.dagger:hilt-android-compiler:2.44"
}

간혹가다가 sync now후에 빌드가 안되는 경우가 있다.
이는 build.gradle(module:name)에서 compileOption과 kotlinOption의 자바버전과 jvm버전이 위의 kapt와 호환되지않는 경우이다. 필자의 경우에는 1.8 -> 17로 버전을 바꾸고 해결되었지만 혹시 1.8이 꼭 필요한 경우 방법을 잘 찾길 바란다.

2) data모듈에서 Hilt 사용

  • app 모듈에서 사용할 ApiServiceManager와 RetrofitModule을 변경한다.

  • ApiServiceManager

class ApiServiceManager @Inject constructor (
    private val songApiService: SongApiService
){
  • RetrofitModule
@Module
@InstallIn(SingletoneComponent::class)
Object RetrofitModule{

	@Provides
	fun proviedRetrofit(): Retrofit{
      return Retrofit.builder()
          .baseUrl("https://im.sexy.guy/")
          .addConverterFactory(GsonConverterFactory.create())
          .build()
    }
    
    @Provides
   	fun provideApiService(retrofit: Retrofit): ApiService{
    	return retrofit.create(ApiService::class.java)
    }
    
    @Provides
    fun provideApiServiceManager(ApiService: ApiService): ApiServiceManager {
    	return ApiServiceManager(ApiService)
    }
}

3) app 모듈에서 Hilt 사용

  • MVVM 패턴을 위해 app 모듈에 viewmodel과 view 패키지를 각각 생성한다.

    MVVM 패턴을 사용하여 가져온 데이터를 보여주는데 가져온 데이터를 Viewmodel에서 처리하여 view에서 보여주면 구조적으로도 깔끔하고 데이터를 통제하기에도 훨씬 편하다.
    단, viwemodel을 사용하기 위해 app모듈에서 lifecycle 등에 관련된 것을 implementation이 필요할 수 있다. 이 또한 내용이 방대함으로 생략하고 필요한 것을 찾아서 implementation하시길 바랍니다.
    ex) build.gradle.kts(module:app)

    val lifecycle_versions = "2.4.1"
        //lifecycle
        implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_versions") // kotlin을 위한 viewmodel
        implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_versions") // compose를 위한
        implementation ("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_versions") // 실시간 데이터 관리를 위한
        ```
  • viewmodel/ExampleViewModel

@HiltViewModel
class ExampleViewModel @Inject constructor(
    private val apiServiceManager: ApiServiceManager
) : ViewModel() {

	fun sampleDataLog(name: String){
    	val data = apiServiceManager.getData(name)
    	Log.d("Sample", "$data")
    
    }
}
  • view/MainActivity
@AndroidEntryPoint
class MainActivity : ComponentActivity(){
    @Inject
    lateinit var apiServiceManager: ApiServiceManager

    private lateinit var viewModel: ExampleViewModel


    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        viewModel = ExampleViewModel(apiServiceManager)
        
        var name: String = "hong"
        viewModel.sampleDataLog(name)
        
   	}
}
  • app/..(app에 MyApplcation 생성) 및 Androidmanifest.xml에 android:name 추가

    내용이 작성되지 않아도 꼭 필요한 구성요소이다.

@HiltAndroidApp
class MyApplication : Application() {
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission  android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:name=".MyApplication"
	...
}

4. 위 과정에서 오류가난다면 체크할 것

1) url 주소의 '/'가 올바른 위치에 있는지
2) 데이터구조에 맞게 interface와 data class를 제대로 적용되었는지(

{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  "active": true,
  "members": [
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
    },
    {
      "name": "Madame Uppercut",
      "age": 39,
      "secretIdentity": "Jane Wilson",
      "powers": [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    },
  ...  
}

이런 식의 구조라면 '데이터 클래스를 하나 더 사용한다거나','getData' 함수 뒤의 Call<List>하는 등의 추가 작업이 필요하다.

3) (Call, Callback), (Response,Result) 등 Retrofit관련 다양한 호출 객체들이 있다 이가 맞는지도
4) RetrofitModule의 객체가 사용될 때 Singleton으로 사용되어야 하는지 고민 했는가
5) Retrofit은 비동기 처리 라이브러리이다. 비동기 작업에서의 로직 오류는 없는치 체트
6) Kotlin과 Gradle 버전
7) dagger-hilt는 최근 변동이 많은 라이브러리이다. 사라진 함수가 있을 수도 있고 새로 생긴 함수가 있을 수도 있다.

Reference

https://jaejong.tistory.com/33

profile
좋은 지식 나누어요

0개의 댓글