이전 글에서 서버의 요청을 안전하게 처리할 수 있는 방법을 소개했다. 이번 글에선 실제 요청을 보내는 코드 예시를 작성하고자 한다.
일단, 서버로부터 데이터를 받아올 Dto 클래스를 생성하기 위해서 서버의 Api 문서를 확인해야한다.
서버 Api 형식 확인
- Dto 클래스를 생성하기 전 서버에서 어떤 형식으로 요청을 받는지, 응답은 어떻게 주는지를 확인해야 한다.
- 서버의 API Docs (Swagger 등)을 확인하여 요청/응답 형식을 확인한다.
요청 형식
- Get 방식으로 요청을 받고 있고, 별도의 파라미터는 존재하지 않는다.
응답 형식
- SimpleHospitalInfoDto 형태로 요청을 받는 것을 알 수 있으며, 앱 단에서는 이에 맞춰서 응답을 받을 클래스를 작성해주면 된다.
Response Dto 클래스 작성
- 서버가 주는 응답 형식에 맞게 클래스를 작성한다.
- 여기서 @SerializedName에 있는 이름은 서버와 동일하게 작성해준다. (이 이름에 맞춰서 응답 데이터가 매핑 되기 때문에 이름이 다르면 데이터를 받아올 수 없다.)
- 변수명은 내가 사용하고 싶은 변수명으로 지정해주면 된다. 서버와 같을 필요는 없다.
SimpleHospitalResponse (Data Layer)
@Parcelize
class SimpleHospitalResponse(
@SerializedName("hpid") val hpid : String,
@SerializedName("dutyName") val dutyName : String,
@SerializedName("dutyAddr") val dutyAddress : String
) : BaseResponse {
companion object: DataMapper<SimpleHospitalResponse, SimpleHospital> {
override fun SimpleHospitalResponse.toDomainModel(): SimpleHospital {
return SimpleHospital(
hpid = this.hpid,
dutyName = this.dutyName,
dutyAddress = this.dutyAddress.split(" ").run {
"${this[0].substring(0..1)} ${this[1]}"
}
)
}
}
}
- 응답을 받을 클래스이므로 이전 글에서 생성했던 BaseResponse를 구현한다.
- 그 후, DataMapper를 이용하여 필요한 데이터만 뽑은 Entity 클래스로 변환해준다.
Entity 클래스 작성
- 앱 내에서 필요한 데이터만 작성해놓은 클래스이다.
- 서버와 형태가 동일해도 별도로 작성해주는 것이 좋다. 그래야 서버에서 응답 형식이 변경 되어도 최대한 영향을 적게 받을 수 있다.
SimpleHospital (Domain Layer)
@Parcelize
class SimpleHospital(
val hpid : String,
val dutyName : String,
val dutyAddress : String,
) : BaseModel
Api 인터페이스 작성
- 응답으로 받을 클래스가 준비 되었으므로, 서버에 요청을 보내기 위한 코드를 작성해야한다.
- 인터페이스로 아래와 같이 작성해주면 Retrofit이 이 형식에 맞춰서 요청을 보내준다.
HospitalApi (Data Layer)
internal interface HospitalApi {
@GET("hospitals/simpleinfo")
suspend fun getAllHospitals(): Response<SimpleHospitalListResponse>
@GET("hospitals/{hpid}")
suspend fun getHospitalByHpid(@Path("hpid") hpid:String): Response<HospitalResponse>
}
- Spring Boot의 JPA를 사용할 때와 매우 유사하다.
- interface를 생성하고, 요청 방식(GET, POST, PATCH, DELETE 등)을 명시한 뒤, url을 적어주면 된다.
- 응답은 retrofit2의 Response 클래스안에 우리가 작성했던 Response Dto 클래스를 넣어주면 된다.
서버로 요청 하기
- 이제 요청할 준비는 끝났으므로, 요청하는 코드를 작성해야 한다.
- 서버로 요청을 보내는 코드는 Repository 클래스 내에서 작성해주면 된다.
HospitalRepository (Domain Layer, 선택)
- ❕ 해당 예시는 클린 아키텍쳐 기준이므로 HospitalRepository 인터페이스를 만든 후 HospitalRepositoryImpl에서 요청을 수행하고 있다. 클린 아키텍쳐를 적용하지 않는 경우 그냥 HospitalRepository만 클래스로 만들어준 후 요청 코드를 작성하면 된다.
interface HospitalRepository {
suspend fun getAllHospitals(): Flow<ResponseState<SimpleHospitalList>>
suspend fun getHospitalByHpid(hpid: String): Flow<ResponseState<Hospital>>
}
HospitalRepositoryImpl (Data Layer)
internal class HospitalRepositoryImpl @Inject constructor(
private val hospitalApi: HospitalApi
): HospitalRepository{
/**
* 전체 병원 리스트를 조회하는 메소드
* @return Flow<ResponseState<HospitalList>>
*/
override suspend fun getAllHospitals(): Flow<ResponseState<SimpleHospitalList>> {
return flow { // Coroutine-Flow로 데이터를 내보내기 위해 flow 블럭으로 감싸준다.
// ApiResponseHandler 안에 요청을 보내는 코드를 넣어준다.
ApiResponseHandler().handle {
hospitalApi.getAllHospitals()
}.onEach { result ->
when(result) {
// result가 Success 타입인 경우 (요청에 성공한 경우)
is ApiResponse.Success -> {
emit(ResponseState.Success(result.data.toDomainModel()))
}
// result가 Error 타입인 경우 (요청에 실패한 경우)
is ApiResponse.Error -> {
emit(ResponseState.Error(result.error.toDomainModel()))
}
}
}.collect()
}
}
}
- Flow에서 emit은 생산, collect는 소비라고 보면 되는데, ApiResponseHandler에서 응답을 Flow로 emit(생산) 해주면 그 응답을 다시 ResponseState 타입으로 변경하여 emit(생산) 해주는 과정이라고 보면 된다.
GetAllHospitalUseCase (Domain Layer, 선택)
- ❕ 이부분 역시 클린 아키텍쳐를 적용하지 않는 경우 생략해도 된다. ❕
@Reusable
class GetAllHospitalUseCase @Inject constructor(
private val hospitalRepository: HospitalRepository
) {
suspend operator fun invoke(): Flow<ResponseState<SimpleHospitalList>> {
return hospitalRepository.getAllHospitals()
}
}
HospitalViewModel (Presentation Layer)
- 이제 ViewModel에서 값을 받아와서 그에 맞는 작업을 진행하면 된다.
- ❕ 클린 아키텍쳐를 적용했기 때문에 ViewModel에서 UseCase를 가져와서 사용하고 있다.❕
- ❕ 클린 아키텍쳐를 적용하지 않는 경우 Repository를 바로 가져오면 된다.❕
@HiltViewModel
class HospitalViewModel @Inject constructor(
private val getAllHospitalUseCase: GetAllHospitalUseCase
) : ViewModel() {
/* 생략 */
fun requestAllHospitals(){
// 코루틴 실행 (네트워크 요청은 IO 쓰레드에서 처리해주는게 좋음)
viewModelScope.launch(Dispatchers.IO) {
getAllHospitalUseCase()
.onStart { // 요청이 시작될 때 할 작업 작성 }
.catch { exception ->
// 예외 발생시 처리 코드 작성
}
.collect{ state ->
when(state) {
is ResponseState.Success -> {
// 성공적으로 응답을 받은 경우의 처리 코드 작성
}
is ResponseState.Error -> {
// 에러가 발생했을 때의 처리 코드 작성
}
}
}
}
}
}
ViewModel 전체 코드
더보기
이 글에서 설명되지 않은 State 패턴이 적용되어 있으므로 전체 코드는 분리해서 첨부 함
@HiltViewModel
class HospitalViewModel @Inject constructor(
private val getAllHospitalUseCase: GetAllHospitalUseCase
) : ViewModel() {
private var _state = MutableStateFlow<HospitalSearchFragmentState>(HospitalSearchFragmentState.Init)
val state : StateFlow<HospitalSearchFragmentState> get() = _state
private fun setLoading(){
_state.value = HospitalSearchFragmentState.IsLoading(true)
}
private fun hideLoading(){
_state.value = HospitalSearchFragmentState.IsLoading(false)
}
private fun showToast(message: String){
_state.value = HospitalSearchFragmentState.ShowToast(message)
}
fun requestAllHospitals(){
viewModelScope.launch(Dispatchers.IO) {
getAllHospitalUseCase()
.onStart { setLoading() }
.catch { exception ->
hideLoading()
showToast(exception.message.toString())
Log.d("병원 조회 에러", exception.stackTraceToString())
}
.collect{ state ->
hideLoading()
when(state) {
is ResponseState.Success -> {
_state.value = HospitalSearchFragmentState.SuccessLoadHospital(state.data)
}
is ResponseState.Error -> {
_state.value = HospitalSearchFragmentState.ErrorLoadHospital(state.error.message)
}
}
}
}
}
}
sealed class HospitalSearchFragmentState {
object Init : HospitalSearchFragmentState()
data class SuccessLoadHospital(val simpleHospitalList: SimpleHospitalList) : HospitalSearchFragmentState()
data class ErrorLoadHospital(val error: String) : HospitalSearchFragmentState()
data class IsLoading(val isLoading: Boolean) : HospitalSearchFragmentState()
data class ShowToast(val message: String) : HospitalSearchFragmentState()
}
전체 코드
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] WebView 기본 사용법 (0) | 2023.07.06 |
---|---|
[ 안드로이드 ] 클릭 가능한 모든 뷰에 반투명 효과 넣기 (0) | 2023.05.20 |
[ 안드로이드 ] Retrofit2를 이용하여 서버와 통신하기 (1) - 안전하게 통신하기 (SafeApiCall) (0) | 2023.05.04 |
[ 안드로이드 ] Room DB - Dao 사용법 (0) | 2023.04.08 |
[ 안드로이드 ] RecylcerView + Filterable을 이용하여 실시간 검색 기능 구현하기 (0) | 2023.04.07 |