| name | implementing-remote-api |
| description | Use when adding or changing remote API calls, Ktor services, request/response DTOs, repository contracts, or repository implementations in this Android repository. |
Implementing Remote API
원격 API 구현은 core:data-api의 repository contract와 core:data의 Ktor service, request/response DTO, repository impl을 함께 맞춘다.
작업 범위
core/data-api/src/main/java/com/neki/android/core/dataapi/repository/*Repository.kt
core/data/src/main/java/com/neki/android/core/data/remote/api/*Service.kt
core/data/src/main/java/com/neki/android/core/data/remote/model/request/*Request.kt
core/data/src/main/java/com/neki/android/core/data/remote/model/response/*Response.kt
core/data/src/main/java/com/neki/android/core/data/repository/impl/*RepositoryImpl.kt
- paging API면
core/data/src/main/java/com/neki/android/core/data/paging/*PagingSource.kt
- 새 repository를 추가하면
core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt
- data layer는 feature 모듈 참조 없이 core 계층 안에서 구현한다.
- 기본 API 서버 외 호스트(presigned 업로드, webhook 등)를 호출하면 기본
HttpClient 대신 remote/qualifier의 @UploadHttpClient·@WebhookHttpClient가 붙은 client를 주입한다.
구현 흐름
core:data-api에 repository interface를 정의한다.
core:data에 Ktor service method를 추가한다.
- request/response DTO와 mapper를 추가한다.
- repository impl에서 service를 호출하고
Result<core model>로 반환한다.
- paging API면
PagingSource와 Pager 흐름을 함께 맞춘다.
- 기본 client는 Bearer 토큰 첨부·갱신을 자동 처리하므로 service/repository에서 토큰을 직접 다루지 않는다. auth 없이 호출되는 endpoint면
core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt의 sendWithoutAuthUrls에 path를 추가한다(encodedPath 정확 일치 매칭이라 path variable 없는 고정 경로만 가능).
- 새 repository면
RepositoryModule에 @Binds @Singleton binding을 추가한다.
- 여러 repository를 조합하는 로직은
core/domain/src/main/java/com/neki/android/core/domain/usecase의 UseCase로 분리한다.
기본 형태
interface FeatureRepository {
suspend fun getFeature(id: Long): Result<Feature>
}
class FeatureService @Inject constructor(
private val client: HttpClient,
) {
suspend fun getFeature(id: Long): BasicResponse<FeatureResponse> {
return client.get("/api/features/$id").body()
}
}
@Serializable
data class FeatureResponse(
@SerialName("id") val id: Long,
@SerialName("name") val name: String,
) {
internal fun toModel(): Feature = Feature(
id = id,
name = name,
)
}
class FeatureRepositoryImpl @Inject constructor(
private val featureService: FeatureService,
) : FeatureRepository {
override suspend fun getFeature(id: Long): Result<Feature> = runSuspendCatching {
featureService.getFeature(id).data.toModel()
}
}
Nullable Response
class FeatureService @Inject constructor(
private val client: HttpClient,
) {
suspend fun getOptionalFeature(): BasicNullableResponse<FeatureResponse> {
return client.get("/api/features/optional").body()
}
}
Request Body
override suspend fun updateFeature(
id: Long,
name: String,
): Result<Unit> = runSuspendCatching {
featureService.updateFeature(
requestBody = UpdateFeatureRequest(
id = id,
name = name,
),
)
}
Unit Response
등록/수정/삭제처럼 반환 model이 없으면 service는 BasicNullableResponse<Unit>, repository는 Result<Unit>로 둔다.
suspend fun updateFeature(requestBody: UpdateFeatureRequest): BasicNullableResponse<Unit> {
return client.put("/api/features") { setBody(requestBody) }.body()
}
override suspend fun updateFeature(name: String): Result<Unit> = runSuspendCatching {
featureService.updateFeature(
requestBody = UpdateFeatureRequest(name = name),
)
}
에러 처리
기본 client는 expectSuccess = true라 non-2xx 응답은 예외로 던져지고, runSuspendCatching이 Result 실패로 감싼다(CancellationException은 재던져 취소를 전파). 특정 상태코드를 도메인 에러로 다루려면 repository impl에서 ClientRequestException을 잡아 core:common의 NekiApiException 하위 예외(ApiErrorCode 상수 사용)로 변환해 던진다.
Paging
interface FeatureRepository {
fun getFeaturesFlow(): Flow<PagingData<Feature>>
}
override fun getFeaturesFlow(): Flow<PagingData<Feature>> {
return Pager(
config = PagingConfig(
pageSize = PAGE_SIZE,
initialLoadSize = PAGE_SIZE,
prefetchDistance = PREFETCH_DISTANCE,
enablePlaceholders = false,
),
pagingSourceFactory = { FeaturePagingSource(featureService) },
).flow
}
PAGE_SIZE·PREFETCH_DISTANCE는 repository impl 파일 상단에 private const val로 둔다(현행 20/10).