
Manifest Android Interview 책을 읽고 Practical Questions 에 대한 답변을 작성해보고, 카테고리 내에 특정 개념에 대한 딥다이브 및 스터디 시간에 얘기 나누면 좋을 내용들을 적어보는 글입니다.
답변 정리는 LLM의 도움을 받았습니다.
Q) 59. 장시간 실행되는 백그라운드 작업을 어떻게 관리하는가?
Q) Android 앱에서 원격 서버로부터 대용량 파일(수백 MB)을 다운로드하는 기능을 구현해야 합니다. 앱이 종료되더라도 다운로드가 계속되어야 하며, 성능과 네트워크 상태 측면에서 효율적이어야 합니다. WorkManager, 포그라운드 서비스, 또는 JobScheduler 중 어떤 백그라운드 실행 메커니즘을 선택하시겠습니까? 그리고 그 이유는 무엇입니까?
포그라운드 서비스 선택
이유:
Q) 60. JSON 형식을 객체로 어떻게 직렬화하는가?
Q1) API로부터 받은 JSON 응답을 Kotlin 데이터 클래스로 어떻게 역직렬화 하겠는가? Kotlin 우선 프로젝트를 위해 어떤 라이브러리를 선택하고, 그 이유는 무엇인가?
kotlinx.serialization 선택
이유:
코틀린 공식 지원 및 언어 친화성: kotlinx.serialization은 JetBrains에서 공식적으로 제공하며, Kotlin의 언어 특성과 기능(예: data class, default parameter, null-safety 등)을 완벽하게 지원
코틀린 데이터 클래스의 기본값, null-safety 지원: Gson, Moshi와 달리, 코틀린의 기본값(default value)이나 null-safety를 그대로 반영.
예를 들어, JSON에 없는 필드는 데이터 클래스에 지정된 기본값으로 자동 할당.
-> 이는 런타임 예외를 줄이고, 명확한 타입 안전성을 제공.
컴파일 타임 코드 생성(Reflection 미사용): 런타임 리플렉션이 아닌 컴파일 타임에 직렬화/역직렬화 코드를 생성하므로, 성능이 우수하고, jar 크기와 시작 시간이 줄어듬. 이는 서버리스 환경이나 성능이 중요한 환경에서 큰 장점.
빌드 타임 타입 안전성: 컴파일 시점에 직렬화/역직렬화 로직이 생성되어, 런타임 오류 가능성이 줄어들고, 타입 안정성이 보장됨.
멀티플랫폼 지원: Kotlin/JVM, Kotlin/JS, Kotlin/Native 등 다양한 플랫폼에서 동일하게 사용할 수 있음
Q2) Kotlin 데이터 클래스에 정의되지 않은 누락되거나 추가 필드가 있는 JSON 객체를 역직렬화해야 하는 경우, 이 시나리오를 어떻게 처리하겠는가?
누락 필드:
val name: String = "")val name: String?)추가 필드:
@SerialName, @JsonClass(generateAdapter = true) 등으로 매핑 조절val lenientJson = Json {
ignoreUnknownKeys = true // 알 수 없는 키 무시 (기본값: false)
isLenient = true // 느슨한 파싱 허용
encodeDefaults = false // 기본값 인코딩 생략
coerceInputValues = true // null을 기본값으로 강제 변환
}

Q) 61. 데이터를 가져오기 위한 네트워크 요청을 어떻게 처리하고, 효율성과 신뢰성을 위해 어떤 라이브러리나 기술을 사용하는가?
효율적이고 신뢰성 높은 네트워크 요청 처리를 위해 Android에서는 Retrofit과 OkHttp, 그리고 Kotlin Coroutines를 조합하는 것이 표준적
Retrofit은 타입 안전성과 간결한 API 설계, OkHttp는 커넥션 풀링과 HTTP/2 지원 등으로 네트워크 효율을 높여주며, 코루틴은 비동기 처리를 간결하게 만듬.
커넥션 풀링:
네트워크 요청 시 매번 새로운 연결을 생성하는 대신, 미리 만들어진 연결(커넥션)을 재사용하는 기술
HTTP/2:
HTTP/2는 HTTP/1.1의 한계를 극복하고 네트워크 효율성을 극대화하기 위해 도입된 프로토콜
멀티플렉싱(Multiplexing):
하나의 연결(TCP 커넥션)에서 여러 요청과 응답을 동시에 주고받을 수 있음
HTTP/1.1처럼 요청이 순차적으로 처리되어 병목이 생기는 현상을 제거
바이너리 프레이밍(Binary Framing):
데이터를 바이너리로 전송해 파싱 속도와 신뢰성을 높임
헤더 압축(Header Compression):
반복적으로 전송되는 HTTP 헤더를 압축해 네트워크 트래픽을 줄이고, 성능을 개선
서버 푸시(Server Push):
클라이언트가 요청하기 전에 서버가 미리 필요한 리소스를 전송할 수 있어, 리소스 로딩 속도가 빨라짐
스트림 우선순위(Stream Prioritization):
중요한 리소스를 먼저 받을 수 있도록 요청의 우선순위를 지정할 수 있음
지속적 연결(Persistent TCP Connection):
페이지가 열려 있는 동안 TCP 연결을 계속 유지해, 여러 리소스 요청을 빠르게 처리
보안(Encryption):
HTTP/2는 TLS(암호화)를 기본적으로 지원하여 데이터의 안전한 전송을 보장
Q1) 앱이 여러 개의 동시 API 요청을 하고 UI를 업데이트하기 전에 결과를 결합해야 한다. Retrofit과 코루틴을 사용하여 이를 효율적으로 달성하는 방법은 무엇인가?
동시(병렬) API 요청을 효율적으로 처리하고, 모든 결과를 결합한 후 UI를 업데이트하려면 다음과 같이 구현할 수 있음.
1. CoroutineScope(예: ViewModelScope)에서 launch
2. async 블록으로 각 API 요청을 병렬 실행
3. awaitAll로 모든 결과를 한 번에 수집
4. 결과 결합 후 UI 업데이트
viewModelScope.launch {
val deferred1 = async { api.getUserInfo() }
val deferred2 = async { api.getPosts() }
val deferred3 = async { api.getComments() }
// 모든 요청이 끝날 때까지 대기, 실패 시 예외 발생
val results = awaitAll(deferred1, deferred2, deferred3)
// results는 [userInfo, posts, comments] 형태
// 결과 결합 및 UI 업데이트
updateUi(results[0], results[1], results[2])
}
Q2) API 호출 실패시 어떻게 처리하고 재시도 메커니즘을 구현하겠는가?
API 호출 실패 시 신뢰성을 높이려면, 체계적인 에러 처리와 재시도 로직이 필요
에러 처리:
Result API를 활용하면 성공/실패 상태를 명확하게 관리할 수 있음재시도 메커니즘 (Exponential Backoff):
retry 연산자(Flow 사용 시) 또는 직접 반복문 + delay로 구현할 수 있음suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 1000,
maxDelay: Long = 8000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
// 네트워크 오류만 재시도
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // 마지막 시도
}
retryWithBackoff { api.getData() }처럼 사용Q) 62. 대용량 데이터셋 로딩 시 페이징 시스템이 왜 필수적인지, 그리고 RecyclerView와 함께 어떻게 구현할 수 있는지 설명하라.
대용량 데이터셋을 한 번에 모두 로드하면 앱의 메모리 사용량이 급증하고, UI가 느려지거나 심지어 앱이 강제 종료될 수 있음. 페이징 시스템은 데이터를 필요한 만큼만(즉, “페이지” 단위로) 불러오게 하여 네트워크, 메모리, CPU 자원을 효율적으로 사용하게 해줌. Android Jetpack의 Paging 라이브러리는 이러한 페이징을 쉽게 구현할 수 있도록 지원하며, RecyclerView와 자연스럽게 연동됨
Q1) 앱이 API에서 대용량 데이터셋을 가져와 RecyclerView에 표시한다. 원활한 스크롤을 보장하고 메모리 사용량을 줄이기 위해 효율적인 페이징 시스템을 어떻게 구현하겠는가?
효율적인 페이징 시스템 구현 방법 (RecyclerView + Paging):
예시코드:
val adapter = UserPagingAdapter()
recyclerView.adapter = adapter
lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
Q2) RecyclerView와 함께 수동 페이징 시스템을 구현할 때 어떤 문제가 발생할 수 있으며, 원활한 사용자 경험을 제공하기 위해 이러한 문제를 어떻게 완화할 수 있겠는가?
수동 페이징 구현 시 발생할 수 있는 주요 문제점:
원활한 사용자 경험을 위한 완화 방안:
Q) 63. 네트워크에서 이미지를 가져와 렌더링하는 방법은?
Q) 앱이 원활한 스크롤을 보장하면서 RecyclerView에서 원격 서버에서 고해상도 이미지를 로드하고 표시해야 한다. 어떤 이미지 로딩 라이브러리를 선택하고, UI 지연을 방지하기 위해 성능을 어떻게 최적화하겠는가?
coil을 선택
이유:
ImageView.load(url) 한 줄로 비동기 이미지 로딩, 플레이스홀더·에러 이미지 지정, 크기 조정, 변환 등이 가능UI 지연 방지 및 성능 최적화 방법:
size() 옵션 사용Q) 64. 데이터를 로컬에 어떻게 저장하고 유지하는가?
Q) 오프라인 접근을 위해 네트워크 API에서 대용량 JSON 응답을 저장해야 하는 시나리오에서, 어떤 로컬 저장 메커니즘을 사용하고, 그 이유는 무엇인가?
대용량 JSON 데이터를 오프라인 접근을 위해 로컬에 저장해야 하는 경우, Android에서는 Room 데이터베이스를 사용하는 것이 가장 권장됨
Room 데이터베이스를 선택하는 이유:
대안: 파일 시스템에 JSON 저장
Q) 65. 오프라인 우선 기능을 어떻게 처리하는가?
Q1) 네트워크를 사용할 수 없을 때 원활한 사용자 경험을 보장하기 위해 Android 애플리케이션에서 오프라인 우선 기능을 어떻게 설계하겠는가?
오프라인 우선(Offline-first) 앱은 네트워크 연결이 없어도 핵심 기능이 정상적으로 동작하도록 설계되어야 함. 이를 위해 다음과 같은 전략을 적용:
ConnectivityManager 등으로 네트워크 연결 상태를 실시간 감지하고, 오프라인일 때는 자동으로 로컬 데이터만 사용하도록 전환.Q2) 로컬 Room 데이터베이스 변경 사항을 원격 서버와 동기화하기 위해 어떤 전략을 사용하고, 로컬 및 원격 데이터가 둘 다 수정된 경우 충돌 해결을 어떻게 처리하겠는가?
변경 사항 추적 및 동기화:
충돌 해결(Conflict Resolution):
동기화 실패/재시도 처리:
Q) 66. 초기 데이터 로딩을 위한 작업을 어디에서 시작하나요? LaunchedEffect vs. ViewModel.init()
Q1) Jetpack Compose에서 ViewModel의 init 블록과 LaunchedEffect의 장단점은 무엇이며, 언제 어떤 접근 방식을 선택하겠는가? 다른 대안을 선호한다면, 그것은 무엇인가?
ViewModel.init:
장점: 데이터 로딩이 ViewModel 생성과 동시에 이뤄져 UI와 분리
단점: ViewModel 생성 시점에 부수효과 발생(테스트·디버깅 어려움)
LaunchedEffect:
장점: Composition 진입 시점에 맞춰 데이터 로딩
단점: Composition이 재실행될 때마다 재호출 위험
Lazy Observation
장점: 데이터가 실제로 구독(관찰)될 때만 로딩 시작
단점: 구현 복잡도 증가 가능
공식 문서 및 커뮤니티에서는 ViewModel의 init 블록에서 초기 데이터를 로드하는 것이 일반적이지만, 이는 라이프사이클 불일치, 예기치 않은 부수효과 등으로 인해 최근에는 권장되지 않는 패턴으로 간주되고 있음
LaunchedEffect는 Composition 진입 시점에 맞춰 동작하지만, 키 관리가 미흡하면 불필요하게 여러 번 실행될 수 있어 주의가 필요
Ian Lake(구글 Android Toolkit 팀) 등 전문가들은 두 방식 모두 부수효과(side-effect) 관리에 취약하므로, 실제로 데이터를 구독하는 시점에만 로딩이 시작되는 lazy observation (예: cold Flow, LiveData 구독) 방식을 권장
대안:
Cold Flow/Lazy Observation: Flow, LiveData 등 cold stream을 ViewModel에서 제공하고, Composable에서 collectAsState 등으로 구독할 때 데이터 로딩이 시작되도록 설계
Navigation Graph 범위 ViewModel: 화면 단위가 아닌 네비게이션 그래프 범위로 ViewModel을 관리하면, 데이터 로딩과 UI 라이프사이클을 더 잘 맞출 수 있음
Q2) 지연 관찰과 cold flows를 사용하는 것이 ViewModel.init() 또는 LaunchedEffect에 비해 초기 데이터를 로드할 때 효율성을 어떻게 향상시키는가? 이 접근 방식이 유익한 예시 시나리오를 제공하라.
Cold Flow(지연 관찰)란?:
Flow, LiveData 등 cold stream은 실제로 구독(collect)되는 시점에만 데이터 로딩(네트워크, DB 등)이 시작됨
즉, ViewModel이 생성되어도 아무도 데이터를 구독하지 않으면, 불필요한 네트워크/DB 작업이 발생하지 않음
효율성 향상 이유:
유익한 예시 시나리오:
예시 코드:
// ViewModel
val userData: Flow<User> = flow {
emit(api.getUser())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
// Composable
val user by viewModel.userData.collectAsStateWithLifecycle()
Composable에서 collectAsStateWithLifecycle()가 실행될 때만 Flow가 시작되어 네트워크 요청이 발생

Retrofit/OkHttp와의 차이점 요약
간편함/생산성:
Retrofit은 어노테이션 기반으로 API 정의가 쉽고, 일반적인 REST API 연동에 매우 빠르게 적용할 수 있음
반면, Ktor는 더 많은 자유도와 커스터마이즈가 가능하지만, DSL과 코루틴에 대한 이해가 필요해 진입장벽이 다소 높을 수 있음
복잡한 네트워크 요구사항:
고도의 커스터마이즈, 멀티플랫폼 지원, 실시간 통신(예:WebSocket) 등 복잡한 네트워크 계층이 필요한 경우 Ktor가 더 적합
단순한 REST API 연동, 빠른 개발에는 Retrofit이 여전히 강점을 가짐
결론
Retrofit과 Ktor의 WebSocket(소켓통신) 구현 비교
Ktor를 사용한 WebSocket 통신 구현이 Retrofit보다 훨씬 쉽고 자연스러움
Retrofit의 한계
Ktor의 장점
ktor-client-websockets 플러그인만 추가하면 손쉽게 WebSocket 세션을 열고, 메시지를 주고받을 수 있음https://ktor.io/docs/client-websockets.html
https://ktor.io/docs/client-websockets.html
Jetpack Compose에서 Paging LoadStateFooter를 구현 하는 방법
reference)
The ONLY Correct Way to Load Initial Data In Your Android App?
Loading Initial Data in LaunchedEffect vs. ViewModel
Loading Initial Data on Android Part 2: Clear All Your Doubts
위의 skydoves 님 글 번역글
[Android] 초기 데이터 로드 : LaunchedEffect vs ViewModel
[Android] 초기 데이터 로드 2 : 의문점 해소하기