다크 모드
네트워크 레이어
Retrofit + OkHttp 기반 네트워크 구조
truloop Android의 네트워크 레이어는 Retrofit (3.0) + OkHttp (5.2) 위에 구축되어 있으며, Kotlin Serialization을 JSON 변환에 사용합니다.
구성 요소
OkHttpClient 구성
NetworkModule에서 용도별로 여러 OkHttpClient를 제공합니다:
| Client | Qualifier | 용도 | 특징 |
|---|---|---|---|
| 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 | 파일 업로드 API | Header 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 구성
| Retrofit | Qualifier | 사용 Client | 특징 |
|---|---|---|---|
| truloop Retrofit | @TruloopRetrofit | @TruloopOkHttpClient | TruloopResponseAdapterFactory + Kotlin Serialization |
| File Upload Retrofit | @FileUploadRetrofit | @FileUploadOkHttpClient | Kotlin 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 | 커버 템플릿 |
SecretariesApi | AI 비서 |
MissionApi | 미션 |
NotificationApi | 알림 |
ProfilePhotoPresignedUrlApi | 프로필 이미지 Presigned URL |
ProfilePhotoUploadApi | 프로필 이미지 업로드 |
JsonRpcApi | JSON-RPC 통신 |
별도 모듈에서 등록하는 API:
| API Interface | 등록 모듈 | 역할 |
|---|---|---|
ChatApi | SendbirdChatModule (core:chats:data) | 채팅 세션 토큰, 다이렉트 채널 |
FcmApi | FcmModule (core:fcm) | FCM 토큰 등록/삭제 |
UpdateApi | UpdateApiModule (core:update) | 앱 버전 체크 |
FileUploadApi | UploaderModule (core:uploader) | 파일 업로드 (@FileUploadRetrofit 사용) |
TokenRefreshApi | AuthenticatorModule (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_SNACKBAR | Snackbar/Toast |
DISPLAY_TYPE_DIALOG | Dialog |
DISPLAY_TYPE_NONE | 표시하지 않음 |
DISPLAY_TYPE_UNSPECIFIED | 기본값 |
TruloopResponseAdapterFactory
커스텀 CallAdapter.Factory를 통해 모든 Retrofit 응답에 대해 통일된 에러 변환을 수행합니다. ErrorBodyConverter와 NetworkErrorConverter를 사용하여 HTTP 에러를 도메인 에러로 변환합니다.
공통 헤더
| 헤더 | 값 |
|---|---|
Accept-Language | 디바이스 Locale (예: ko-KR) |
X-Platform | "Android" |
X-App-Version | 앱 버전 (예: 1.16.1) |
X-Local-Timezone | 디바이스 타임존 (예: Asia/Seoul) |
Authorization | Bearer {accessToken} (선택적) |
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-10 | OkHttpClient 테이블에 Shortform Client 추가, X-Platform 헤더 값 수정 ("android" → "Android"), 별도 모듈에서 등록되는 API 인터페이스 목록 추가 (ChatApi, FcmApi, UpdateApi, FileUploadApi, TokenRefreshApi), ShortformApi 추가, 클래스명 PascalCase 통일 (실제 코드 기준) |