Skip to content

네트워크 레이어

Retrofit + OkHttp 기반 네트워크 구조

truloop Android의 네트워크 레이어는 Retrofit (3.0) + OkHttp (5.2) 위에 구축되어 있으며, Kotlin Serialization을 JSON 변환에 사용합니다.

구성 요소

OkHttpClient 구성

NetworkModule에서 용도별로 여러 OkHttpClient를 제공합니다:

ClientQualifier용도특징
truloop Client@TruloopOkHttpClient일반 API 호출Header Interceptor + Token Authenticator, 10초 타임아웃
Shortform Client@ShortformOkHttpClient하이라이트 영상 캐시/스트리밍 (ExoPlayer)인증 없음, 10초 타임아웃
External Upload Client@ExternalUploadOkHttpClient외부 URL 업로드 (Presigned URL)인증 없음, 30분 타임아웃
File Upload Client@FileUploadOkHttpClient파일 업로드 APIHeader Interceptor + Token Authenticator, 30분 타임아웃
kotlin
@Provides
@TruloopOkHttpClient
fun provideTruloopOkHttpClient(
    @ApplicationContext context: Context,
    tokenAuthenticator: TokenAuthenticator,
    headerInterceptorFactory: TruloopHeaderInterceptorFactory
): OkHttpClient {
    return OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .addInterceptor(headerInterceptorFactory.create(includeAccessToken = true))
        .addInterceptor(ChuckerInterceptor(context))
        .addBeagleOkHttpLoggerInterceptor()
        .authenticator(tokenAuthenticator)
        .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
        .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
        .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
        .build()
}

Retrofit 구성

RetrofitQualifier사용 Client특징
truloop Retrofit@TruloopRetrofit@TruloopOkHttpClientTruloopResponseAdapterFactory + Kotlin Serialization
File Upload Retrofit@FileUploadRetrofit@FileUploadOkHttpClientKotlin Serialization만 사용
kotlin
@Provides
@TruloopRetrofit
fun provideTruloopRetrofit(
    @TruloopOkHttpClient okHttpClient: OkHttpClient,
    json: Json,
    @TruloopResponseAdapterFactoryQualifier truloopResponseCallAdapterFactory: CallAdapter.Factory
): Retrofit {
    return Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addCallAdapterFactory(truloopResponseCallAdapterFactory)
        .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
        .build()
}

API Interface 패턴

Retrofit의 interface를 사용하여 API를 정의합니다. 모든 API 함수는 suspend fun으로 정의되어 Coroutine과 자연스럽게 통합됩니다.

예시: LoopApi

kotlin
internal interface LoopApi {
    @POST("/core/api/v3/loops")
    suspend fun createLoop(@Body createLoopRequestDto: CreateLoopRequestDto): CreateLoopDto

    @GET("/core/api/v3/loops/{eid}")
    suspend fun getLoopById(@Path("eid") eid: String): GetLoopResponseDto

    @PATCH("/core/api/v3/loops/{eid}")
    suspend fun updateLoop(
        @Path("eid") eid: String,
        @Body request: UpdateLoopRequestDto
    ): UpdateLoopResponseDto

    @DELETE("/core/api/v3/loops/{eid}")
    suspend fun deleteLoop(@Path("eid") eid: String)

    @GET("/core/api/v3/loops")
    suspend fun getLoops(
        @Query("sort") sort: String? = null,
        @Query("next_token") nextToken: String? = null,
        @Query("limit") limit: Int? = null,
        @Query("filter") filter: String? = null,
        @Query("q") query: String? = null,
        @Query("status") status: String? = null
    ): LoopListResponseDto
}

API 인터페이스 목록

core:data ApiModule (18개):

API Interface역할
AuthApi인증 (로그인, 로그아웃, 토큰 갱신)
VerificationApi전화번호 인증
UsersApi사용자 정보 (회원가입, 프로필)
LoopApi룹 CRUD, 미디어, 리캡(내부명: Story), 초대
LoopEtaApi룹 ETA (도착 예정 시간)
FeedApi홈 피드
ShortformApi하이라이트 콘텐츠
CommentApi댓글 CRUD, 좋아요
ContactsApi연락처 동기화
BlockApi차단 관리
ContentApi콘텐츠 템플릿
CoverTemplateApi커버 템플릿
SecretariesApiAI 비서
MissionApi미션
NotificationApi알림
ProfilePhotoPresignedUrlApi프로필 이미지 Presigned URL
ProfilePhotoUploadApi프로필 이미지 업로드
JsonRpcApiJSON-RPC 통신

별도 모듈에서 등록하는 API:

API Interface등록 모듈역할
ChatApiSendbirdChatModule (core:chats:data)채팅 세션 토큰, 다이렉트 채널
FcmApiFcmModule (core:fcm)FCM 토큰 등록/삭제
UpdateApiUpdateApiModule (core:update)앱 버전 체크
FileUploadApiUploaderModule (core:uploader)파일 업로드 (@FileUploadRetrofit 사용)
TokenRefreshApiAuthenticatorModule (core:network)토큰 갱신 (별도 OkHttpClient 사용, Access Token 미포함)

Interceptors

TruloopHeaderInterceptor

모든 요청에 공통 헤더를 추가하는 Interceptor입니다:

kotlin
internal class TruloopHeaderInterceptor(
    private val truloopPreferences: TruloopPreferences,
    private val appVersionProvider: AppVersionProvider,
    private val includeAccessToken: Boolean = true
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val header = mutableMapOf(
            "Accept-Language" to Locale.getDefault().toLanguageTag(),
            "X-Platform" to "Android",
            "X-App-Version" to appVersion,
            "X-Local-Timezone" to TimeZone.getDefault().id
        )

        if (includeAccessToken && accessToken.isNotBlank()) {
            header += "Authorization" to "Bearer $accessToken"
        }

        return chain.proceed(
            chain.request().newBuilder()
                .headers((header + chain.request().headers).toHeaders())
                .build()
        )
    }
}

TruloopHeaderInterceptorFactory를 통해 Access Token 포함 여부를 선택할 수 있습니다.

TokenAuthenticator

OkHttp Authenticator를 구현하여 401 응답 시 자동 토큰 갱신을 처리합니다:

kotlin
internal class TokenAuthenticator @Inject constructor(
    private val truloopPreferences: TruloopPreferences,
    private val tokenRefreshApi: TokenRefreshApi,
    private val forceLogoutEmitter: ForceLogoutEmitter,
    private val errorBodyConverter: ErrorBodyConverter
) : Authenticator {

    private val lock = Any()

    override fun authenticate(route: Route?, response: Response): Request? {
        synchronized(lock) {
            // 1. 다른 스레드에서 이미 갱신했는지 확인
            // 2. TokenRefreshApi로 새 토큰 요청
            // 3. 성공 시 새 토큰 저장 + 원래 요청 재시도
            // 4. 실패 시 (만료, 미인증) 강제 로그아웃
        }
    }
}

주의

synchronized(lock)을 사용하여 여러 스레드에서 동시에 토큰 갱신을 요청하는 것을 방지합니다. 한 스레드가 갱신에 성공하면 다른 스레드는 새 토큰을 사용합니다.

Kotlin Serialization

네트워크 JSON 직렬화에 kotlinx.serialization을 사용합니다:

kotlin
// DTO 정의 예시
@Serializable
data class CreateLoopRequestDto(
    @SerialName("title") val title: String,
    @SerialName("participant_eids") val participantEids: List<String>,
)

// Retrofit Converter 설정
json.asConverterFactory("application/json".toMediaType())

에러 처리

TruloopErrorBody

서버 에러 응답은 다음 구조로 파싱됩니다:

kotlin
@Serializable
internal data class TruloopErrorBody(
    @SerialName("type") val type: String = "",
    @SerialName("display_type") val displayType: DisplayType = DisplayType.DISPLAY_TYPE_UNSPECIFIED,
    @SerialName("message") val localizedMessage: LocalizedMessage = LocalizedMessage(),
    @SerialName("metadata_proto_json") val metadataProtoJson: String? = null
)
DisplayType표시 방식
DISPLAY_TYPE_SNACKBARSnackbar/Toast
DISPLAY_TYPE_DIALOGDialog
DISPLAY_TYPE_NONE표시하지 않음
DISPLAY_TYPE_UNSPECIFIED기본값

TruloopResponseAdapterFactory

커스텀 CallAdapter.Factory를 통해 모든 Retrofit 응답에 대해 통일된 에러 변환을 수행합니다. ErrorBodyConverterNetworkErrorConverter를 사용하여 HTTP 에러를 도메인 에러로 변환합니다.

공통 헤더

헤더
Accept-Language디바이스 Locale (예: ko-KR)
X-Platform"Android"
X-App-Version앱 버전 (예: 1.16.1)
X-Local-Timezone디바이스 타임존 (예: Asia/Seoul)
AuthorizationBearer {accessToken} (선택적)

변경 이력

날짜내용
2026-03-10OkHttpClient 테이블에 Shortform Client 추가, X-Platform 헤더 값 수정 ("android""Android"), 별도 모듈에서 등록되는 API 인터페이스 목록 추가 (ChatApi, FcmApi, UpdateApi, FileUploadApi, TokenRefreshApi), ShortformApi 추가, 클래스명 PascalCase 통일 (실제 코드 기준)