[면접][CS] 안드로이드 개발자 면접 준비 1일차

윤찬·2025년 8월 24일

Android

목록 보기
19/38

모든 텍스트는 음성으로 텍스트 인식했기 때문에 발음 때문에 단어가 조금 틀릴 수 있다.

GPT 모의고사 안드로이드 면접 1일차

나의 모의 면접 결과

피드백 내용

확실히 흐름만 간단하게 알지만 동작원리나, 개념의 정의를 자세하게 알지 못하는 것 같다. 이 부분을 계속 보면서 기억해야할 것 같고, 특히 예시를 제대로 못 든다는점이 약점이라는 것을 알게 되었다. 계속 모의면접을 해보면서 이 예시와 정의를 말하는 습관을 가지려고 노력해보자

1. 안드로이드의 Activity 생명주기(Activity Lifecycle)에 대해 설명해 주세요.

내가 질문한 답변

안드로이드 액티비티는 onCreate, onStart, onResume, onPause, onStop, onDestroy로 이루어져 있습니다. onCreate는 액티비티가 생성되고 한번 초기화할 수 있을 때 onCreate를 사용합니다. onStart는 화면이 보이기 전에 호출이 되는 시점입니다. onResume는 사용자와 UI가 상호작용하는 부분입니다. onPause는 화면이 사라지는 시점입니다. onStop은 화면이 없어지는 시점입니다. onDestroy는 액티비티가 소멸되는 시점입니다.

1-1 꼬리질문 : onPause()와 onStop()은 모두 화면이 보이지 않는 시점과 관련이 있는데, 두 메서드의 차이점을 조금 더 구체적으로 설명해 주실 수 있을까요?

나의 답변

onPause는 해당 화면에서 팝업으로 넘어갔을 때 주로 발생하는 부분이고요. onStop은 다른 액티비티에 이동했을 때 onStop이 호출이 됩니다. 한마디로 정의하자면, 해당 화면에서 다른 화면을 넘어갈 때는 onStop이 나오고, 해당 화면에서 팝업같은 화면을 유지하지만 다른 화면을 유지될 때 onPause가 발생합니다.

중요한 키워드
onPause는 화면 일부가 가려져서 사용자와 상호작용할 수 없는 상태가 되었을 때 호출
onStop은 화면 전체가 사용자에게 완전히 보잊 않게 되었을 때 호출이된다.

즉, onPause는 부분 가림, onStop은 완전 가림일 때 호출이 된다는 것.

1-2 꼬리질문 : 그렇다면 onPause() 상태일 때와 onStop() 상태일 때, 리소스 관리는 어떻게 달라야 할까요? 예를 들어 카메라나 GPS 같은 리소스를 사용할 때 어느 시점에 해제하는 게 적절할까요?

중요 키워드
onPause() = 즉시 중단(독점,고소모): 카메라/마이크/애니메이션/오디오 포커스 등
onStop() = 완전 해제: 연결 종료, 리시버 해제, 세션/자원 폐기 등
카메라 같은 경우 onPause 해제, GPS는 화면 의존 시 onStop 해제, 배터리 민감 시 onPause 선중단.

“리소스 관리는 onPause()와 onStop()의 의미 차이에 맞춰 경량 중단 vs 완전 해제로 나눠서 합니다.
onPause()는 화면이 부분 가림/포커스 상실 상태이므로, 즉시 중단해야 하는 리소스(사용자와 상호작용·오디오 포커스·배터리 급소모·독점 하드웨어)는 여기서 멈춥니다. 반면 onStop()은 화면이 완전히 보이지 않을 때 호출되므로, 무거운 자원 해제·연결 종료·리시버 해제 같은 ‘완전 정리’를 수행합니다.
카메라처럼 독점 하드웨어는 다른 앱 차단을 피하려고 보통 onPause()에서 프리뷰/세션을 멈추고 리소스를 즉시 해제합니다. CameraX를 쓰면 Lifecycle에 바인딩해 onStart()/onStop()에 맞춰 자동으로 열고 닫게 하거나, 레거시 카메라/특수 요구가 있으면 onResume()/onPause()에서 직접 열고 닫습니다.
카메라처럼 독점 하드웨어는 다른 앱 차단을 피하려고 보통 onPause()에서 프리뷰/세션을 멈추고 리소스를 즉시 해제합니다. CameraX를 쓰면 Lifecycle에 바인딩해 onStart()/onStop()에 맞춰 자동으로 열고 닫게 하거나, 레거시 카메라/특수 요구가 있으면 onResume()/onPause()에서 직접 열고 닫습니다.
GPS(고빈도 위치 업데이트)는 화면이 안 보이는데 계속 가져오면 배터리 낭비라서 포그라운드에서만 필요한 경우 onStop()에 removeUpdates로 끊고, 상호작용이 멈춘 시점에 민감하면 onPause()`에서 먼저 중단하기도 합니다. 반대로 백그라운드 추적이 필요한 내비/러닝앱이라면, 액티비티 수명과 분리해 Foreground Service + 지속 알림으로 관리합니다.
정리하면, onPause() = 즉시 중단이 필요한 것(카메라/마이크/애니메이션/플레이백/센서 등록), onStop() = 연결/세션/리시버 등 무거운 것 완전 해제가 기본 원칙이며, 지속 동작이 필요하면 액티비티 수명에서 분리(서비스·워크매니저)합니다.”


2. Kotlin 코루틴의 Structured Concurrency가 무엇이며, GlobalScope 남용이 왜 문제인지 설명해 주세요. 또한 viewModelScope / lifecycleScope를 사용할 때 취소 전파가 어떻게 동작하는지도 함께 말씀해 주세요.

Structured Concurrency는 구조적 동시성이라 불리며 CoroutineScope가 자식 코르틴의 생명주기를 소유하고, 부모가 끝날 때 자식이 자동 취소되며, 부모는 자식이 모두 끝날 때까지 정상 종료되지 않는다는 규칙입니다. 이 덕분에 리소스 누스, 고아 문제를 방지하고, 에러/취소 처리 지점을 명확히 할 수 있다.

GlobalScope 남용은 각 코루틴을 루트(job hierarchy 밖)로 만들어 생명주기와 분리시킵니다. 화면이 닫혀도 작업이 계속 돌거나, 테스트에서 끝나지 않거나, 예외가 상위로 묶여 처리되지 않아 메모리/리소스 누수·크래시·디버깅 난이도 상승을 야기합니다. 때문에 GlobalScope는 앱 초기화나 프로세스 전역 관리처럼 정말 프로세스 수명에 맞는 드문 경우에만 신중히 사용하고, 그렇지 않으면 명시적인 스코프를 써야 합니다.

viewModelScope은 ViewModel의 onCleared() 시 스코프 전체가 취소되어, 네트워크/DB/Flow 수집 등 모든 자식 코루틴이 함께 중단됩니다. 화면 회전·내비게이션 이동 시 자동 정리돼 누수를 줄입니다.

lifecycleScope은 LifecycleOwner(Activity/Fragment)의 DESTROYED 시점에 스코프가 취소됩니다. 또한 repeatOnLifecycle(STARTED/RESUMED)를 쓰면 해당 상태에서만 블록을 재시작/중단해 UI 수집을 수명에 맞춰 안전하게 제어합니다.

2-1 꼬리 질문 supervisorScope {}와 SupervisorJob의 차이와 선택 기준을 설명해 주세요. 또, viewModelScope가 Supervisor 성격을 갖는다는 점을 활용해 동시에 3개의 네트워크 요청을 던질 때, 한 요청이 실패해도 나머지를 살려서 결과를 합치는 코드 스케치를 간단히 말씀해 주세요(예: 어떤 빌더를 쓰고, 예외는 어디서 처리할지).

주요 문장

“supervisorScope {}와 SupervisorJob은 둘 다 ‘자식 간 실패 격리(부분 실패 허용)’를 제공합니다.

SupervisorJob은 코루틴을 만드는 코루틴 빌더이며, 블록 안 형제 코루틴끼리 실패 전파를 막아 부분 실패를 허용. 부모가 취소되면 전부 취소. 구조적 동시성 유지. 특정 구간만 부분 실패 허용이 필요할 때 사용

supervisorScope은 코루틴 스코프를 만드는 스코프 빌더이다. 오래 사는 스코프 자체에 “부분 실패 허용” 성격을 부여. 보통 스코프 생성 시에만 사용. 스코프 자체가 항상 형제 격리를 가져야 할 때 사용

부분 허용일 경우

class MyViewModel : ViewModel() {

    fun loadAll() = viewModelScope.launch {
        val results: List<Item> = supervisorScope {
            val a = async { repo.fetchA() }   // 실패해도 다른 async로 전파 안 됨
            val b = async { repo.fetchB() }
            val c = async { repo.fetchC() }

            listOf(a, b, c)
                .map { deferred -> runCatching { deferred.await() } } // 여기서만 예외 관찰
                .mapNotNull { it.getOrNull() }                        // 성공만 취합
        }

        state.update { it.copy(items = results) }
    }
}

전체 성공해야할 경우

class MyViewModel : ViewModel() {

    fun loadAllOrFail() = viewModelScope.launch {
        try {
            val (a, b, c) = coroutineScope {
                val a = async { repo.fetchA() }
                val b = async { repo.fetchB() }
                val c = async { repo.fetchC() }
                awaitAll(a, b, c) // 하나라도 실패하면 예외 던지고 모두 취소됨
            }
            state.update { it.copy(items = merge(a, b, c)) }
        } catch (e: Throwable) {
            // 전체 실패 처리 (에러 상태 갱신/로깅 등)
        }
    }
}

3. HashMap의 동작원리를 설명해주세요

HashMap은 기본적으로 배열 기반의 해시 테이블 구조입니다.
키를 저장할 때는 먼저 hashCode()를 계산하고, 이 값을 해시 함수로 변환해서 배열의 인덱스, 즉 버킷 위치를 정합니다. 만약 그 버킷이 비어 있으면 바로 저장되지만, 이미 다른 키가 들어 있다면 충돌(collision)이 발생합니다. 충돌은 체이닝 방식으로 해결합니다. Java 7까지는 LinkedList로 연결했지만, Java 8 이후에는 일정 길이 이상으로 충돌이 많아지면 Red-Black Tree로 변환해서 성능을 보장합니다. get을 호출하면 다시 hashCode()로 버킷을 찾고, 그 안에서는 equals()로 실제 키를 비교해서 일치하는 값을 찾습니다.
그래서 HashMap을 사용할 때는 hashCode()와 equals()를 반드시 올바르게 구현하는 것이 중요합니다. 평균적으로 put과 get은 O(1)이고, 충돌이 심해서 체이닝이 길어지면 최악에는 O(n)까지 갈 수 있습니다. 하지만 Java 8 이후 트리로 바뀌면 충돌 시에도 O(log n)으로 성능을 보장합니다.

주요 키워드

  • 배열 + 버킷 구조, hashCode()로 인덱스 계산, equals()로 최종 비교
  • 충돌 해결: 체이닝(LinkedList), Java 8 이후 Red-Black Tree 변환
  • 시간 복잡도: 평균 O(1), 최악 O(n), 트리화 시 O(log n)
  • hashCode()/equals() 계약 준수 필수

3-1 hashMap과 Hashtable, ConcurrentHashMap의 차이를 설명해 주실 수 있을까요? 그리고 멀티스레드 환경에서 HashMap을 그냥 쓰면 어떤 문제가 생기는지도 말해 주세요.

“HashMap은 비동기 안전하지 않은 일반 해시 테이블로, 단일 스레드 성능이 좋고 null 키 1개와 null 값 허용, fail-fast 이터레이터를 갖지만 멀티스레드 환경에서는 외부 동기화가 없으면 사용할 수 없습니다. Hashtable은 레거시 동기화 맵으로 모든 메서드가 synchronized라 전역 락을 잡고, null 키/값을 허용하지 않으며, 동시성에서 안전하지만 성능 병목이 큽니다. ConcurrentHashMap은 세밀한 동시성 제어(Java 8 기준: 분할 락 + CAS, 버킷 트리화, non-blocking 읽기 위주)를 통해 높은 스루풋을 제공하고, null 키/값을 허용하지 않으며, 이터레이터는 weakly-consistent로 동작해 순회 중 구조 변경에도 안전하게 진행됩니다. 실무에선 Hashtable 대신 ConcurrentHashMap을 쓰고, 단일 스레드이거나 외부에서 명확히 보호되는 경우에만 HashMap을 씁니다.

멀티스레드 환경에서 HashMap을 그대로 쓰면, 첫째 경쟁 조건으로 인한 업데이트 손실(마지막 쓰기만 남음), 둘째 가시성 문제(happens-before 부재로 다른 스레드가 최신 값을 보지 못함), 셋째 리사이즈/체이닝 중 구조 손상(Java 7 계열에선 드물게 무한 루프/사이클), 넷째 순회 중 ConcurrentModificationException 및 불일치 상태 등이 발생할 수 있습니다. 따라서 동시 접근이 있다면 ConcurrentHashMap(또는 필요한 범위의 외부 동기화/불변 스냅샷/Collections.synchronizedMap)을 선택해야 합니다.”

필수 키워드 문장

  • HashMap: 비동기 안전 아님, null 허용, 평균 O(1), fail-fast
  • Hashtable: 전역 synchronized, null 불가, 레거시, 병목
  • ConcurrentHashMap: 세밀 락 + CAS, weakly-consistent iterator, null 불가, 높은 동시성
  • 멀티스레드에서 HashMap: 레이스 조건/가시성 문제/구조 손상/무한 루프 가능/ConcurrentModificationException
  • 선택 기준: 동시성 필요 → ConcurrentHashMap, 단일 스레드 또는 외부 락 명확 → HashMap

모르는 단어
fail-fast: 시스템이나 프로세스가 잠재적인 오류나 문제가 발생했을 때, 즉시 오류를 보고하고 중단하여 더 큰 문제로 이어지는 것을 방지하는 동작 방식

CAS : 여러 스레드가 공유 자원에 동시에 접근할 때 데이터의 일관성을 보장하는 락-프리(Lock-Free) 동시성 제어 기법. 특정 변수의 현재 값을 읽어 예상하는 값과 비교한 후, 두 값이 일치하면 새로운 값으로 바꾸는 원자적 연산을 수행하여, 멀티스레드 환경에서 안전하게 데이터를 처리할 수 있게 해줍니다.

락-프리 : 전통적인 락(Lock) 기반 동기화 방식과 달리, 자원을 점유하기 위해 대기(blocking)하는 방식이 아니므로 성능 저하를 줄일 수 있습니다.

3-2 해시맵에서 해시 함수(hashCode)와 equals가 잘못 구현되면 어떤 문제가 발생할까요?

“HashMap은 키를 저장하거나 조회할 때 hashCode()로 버킷 위치를 정하고, 같은 버킷 안에서는 equals()로 실제 키를 구분합니다. 그래서 이 두 메서드가 올바르게 구현되지 않으면 여러 문제가 발생할 수 있습니다.
먼저 hashCode()가 항상 같은 객체에 대해 일관된 값을 반환하지 않으면, 같은 키임에도 불구하고 매번 다른 버킷에 들어가서 get()으로 값을 찾지 못하거나, 중복된 키가 여러 개 저장되는 문제가 생깁니다.
또 equals()가 논리적으로 동등한 객체를 올바르게 비교하지 못하면, 다른 객체인데 같은 키로 잘못 판단해 덮어쓰기 되거나, 반대로 같은 객체임에도 불구하고 서로 다른 값으로 인식되어 검색이 실패합니다.
마지막으로 hashCode()와 equals()의 계약을 지키지 않으면 HashMap의 핵심 성질인 평균 O(1) 조회 성능이 깨지고, 불필요한 충돌 증가나 데이터 정합성 문제가 발생할 수 있습니다. 따라서 두 메서드는 반드시 동일한 객체는 동일한 hashCode를 반환하고, hashCode가 같으면 equals로 최종 비교되도록 구현해야 합니다.”

✅ 필수 키워드 문장

  • HashMap은 hashCode()로 버킷 결정, equals()로 최종 비교
  • hashCode 불일치 → 같은 키를 못 찾거나 중복 저장
  • equals 잘못 구현 → 덮어쓰기, 검색 실패, 데이터 정합성 문제
  • 계약 불이행 시 → O(1) 성능 저하, 충돌 증가
  • hashCode/equals 계약 준수 필수: 동일 객체 → 동일 hashCode, hashCode 같으면 equals로 비교

3-3 HashSet에 객체를 넣었을 때, 동일 객체를 또 넣으면 내부적으로 어떤 일이 일어나는지 설명해 주실 수 있을까요?

“HashSet은 내부적으로 HashMap을 이용해서 동작합니다. 우리가 객체를 HashSet에 넣으면, 실제로는 그 객체를 키(key)로 해서 내부 HashMap에 저장하고 값(value)은 항상 같은 상수 객체를 사용합니다.
동일한 객체를 다시 넣으려고 할 때는 먼저 hashCode()를 통해 버킷을 찾고, 같은 버킷 안에서는 equals()로 기존 원소와 같은지 비교합니다. 만약 equals() 결과가 true라면 이미 존재하는 키로 간주되어 새로운 값이 추가되지 않고 기존 원소 그대로 유지됩니다. 즉, HashSet은 중복 삽입 시 실제로는 아무런 변화가 일어나지 않습니다.
결국 HashSet의 중복 여부 판단은 hashCode()와 equals() 구현에 달려 있으며, 이 두 메서드가 잘못 구현되면 동일 객체가 중복으로 저장되거나 반대로 다른 객체가 중복으로 인식되는 문제가 발생할 수 있습니다.”

✅ 필수 키워드 문장

  • HashSet은 내부적으로 HashMap 기반
  • 객체 삽입 시 hashCode()로 버킷 결정, equals()로 중복 여부 확인
  • 동일 객체 다시 넣으면 저장되지 않고 무시됨
  • 중복 판단은 hashCode/equals 계약에 의존
  • 잘못 구현 시 중복 저장 or 잘못된 중복 인식 발생

4. 안드로이드 ViewModel은 액티비티/프래그먼트의 생명주기와 어떤 관계를 맺고 있나요? 그리고 SavedStateHandle은 어떤 상황에서 유용하게 쓸 수 있는지 설명해 주세요.

“ViewModel은 액티비티나 프래그먼트의 UI 생명주기보다 더 길게 살아남는 객체입니다. 화면 회전 같은 구성 변경이 일어나도 ViewModel 인스턴스는 재생성되지 않고 그대로 유지되기 때문에, UI 데이터를 보존하면서 불필요한 네트워크 호출이나 상태 초기화를 막을 수 있습니다. 하지만 액티비티나 프래그먼트가 완전히 종료되어 더 이상 필요하지 않을 때는 onCleared()가 호출되면서 정리됩니다. 즉, ViewModel은 LifecycleOwner의 수명에는 종속되지만, 화면 재생성에는 독립적이라는 특징이 있습니다.
SavedStateHandle은 프로세스 킬이나 강제 종료 같은 경우에도 작은 단위의 UI 상태를 보존할 수 있도록 도와줍니다. 예를 들어 검색 화면에서 사용자가 입력한 검색어, 현재 선택된 탭, 스크롤 위치 같은 데이터를 저장해 두면 프로세스가 죽었다가 다시 복원될 때 해당 상태를 복원할 수 있습니다. 따라서 단순히 구성 변경을 넘어, 프로세스가 완전히 종료되더라도 필요한 상태를 복구해야 하는 경우에 유용하게 사용할 수 있습니다.”

✅ 필수 키워드 문장

  • ViewModel: 화면 회전 시에도 유지, LifecycleOwner 종료 시 onCleared()로 정리
  • UI 생명주기보다 길게 살아남음, 구성 변경 독립성
  • SavedStateHandle: 프로세스 킬/강제 종료 대비 상태 저장
  • 활용 예시: 검색어, 선택된 탭, 스크롤 위치 복원
  • 구성 변경 대응 + 프로세스 종료 복원에 유용

4-1 만약 ViewModel 안에서 코루틴을 돌리고 있다가 onCleared()가 호출되면 어떻게 될까요? 이때 코루틴을 안전하게 정리하려면 어떤 방법을 써야 할까요?

“ViewModel에서 코루틴을 돌리는 표준 방법은 viewModelScope를 사용하는 것이고, onCleared()가 호출되면 이 스코프의 Job이 취소되어 모든 자식 코루틴이 구조적으로 함께 취소됩니다. 따라서 별도의 전역 스코프를 쓰지 않았다면, 네트워크/DB/Flow 수집 같은 작업은 협력적 취소로 안전하게 중단되고, 예외는 상위로 전파된 뒤 정리 루틴이 실행됩니다.

코루틴을 안전하게 정리하려면 첫째, viewModelScope만 사용하고 GlobalScope나 부모와 분리된 Job()은 피합니다. 둘째, 작업 본문을 취소 친화적으로 작성합니다: suspend 호출(예: Retrofit/Room)은 기본적으로 취소 가능하므로 별도 스레드 블로킹을 피하고, 불가피한 블로킹 구간은 isActive/ensureActive()로 중간 중간 취소를 인지하도록 합니다. 셋째, 리소스 해제가 필요한 경우(채널/플로우/파일/락 등)에는 try { … } finally { … }를 사용하고, 취소 시점에도 반드시 실행되어야 하는 마무리는 withContext(NonCancellable) 안에서 처리합니다. 넷째, ViewModel 내부에 커스텀 스코프를 썼다면 onCleared()에서 그 스코프를 cancel() 또는 cancelAndJoin()으로 명시적으로 종료하고, 필요 시 invokeOnCompletion 콜백에서 부가 정리를 합니다. 마지막으로, UI 수집은 화면 쪽에서 repeatOnLifecycle로 수명에 맞춰 자동 시작/중단하게 설계해 ViewModel 취소와 중복된 해제 로직을 겹치지 않도록 합니다. 요약하면, ViewModel은 onCleared() 취소 전파를 신뢰하고, 작업 본문은 취소 가능·finally 해제로 누수 없이 종료되게 만드는 것이 핵심입니다.”

필수 키워드 문장

  • onCleared() 시 viewModelScope 취소 → 모든 자식 코루틴 중단(구조적 동시성)
  • GlobalScope 금지, 커스텀 스코프는 onCleared()에서 cancel/cancelAndJoin
  • 취소 친화적 코드: 블로킹 회피, isActive/ensureActive() 활용
  • 정리 보장: try/finally + withContext(NonCancellable)로 리소스 해제
  • UI 수집은 repeatOnLifecycle로 화면 수명에 맞춰 별도 정리 중복 방지

repeatOnLifecycle: 특정 Lifecycle 상태에서만 코루틴 블록을 실행하고 상태를 벗어나면 자동으로 중단, 다시 진입 시 재시작시켜주는 API입니다. 주로 Flow 수집이나 UI 업데이트 같은 반복 작업을 Lifecycle과 맞추기 위해 사용합니다. 기존의 launchWhenStarted와 달리 cancellation-safe하게 동작하기 때문에, Android 공식 가이드에서도 권장되는 방식입니다.


5. Kotlin 제네릭의 공변성(out), 반공변성(in), 무공변(invariant)**의 차이를 설명해 주세요. 또, 왜 List<out T>는 읽기 전용으로 설계되었는지와, 함수 파라미터에서 in이 유용한 실제 사례를 들어 설명해 주세요.

“코틀린 제네릭에서 공변성(out), 반공변성(in), 무공변(invariant)은 타입 간 대체 가능성을 제어하는 개념입니다.

먼저 무공변(invariant)은 제네릭 타입이 정확히 동일할 때만 대입이 가능합니다. 예를 들어 List<String>은 List<Any>에 대입할 수 없습니다.
공변성(out)은 생산(producer) 역할만 하는 경우에 씁니다. out T로 선언하면 Producer<String>을 Producer<Any>에 대입할 수 있습니다. 이는 T를 읽기 전용으로만 사용할 수 있다는 제약을 가집니다. 그래서 List<out T>는 요소를 꺼낼 수만 있고, 임의의 값을 넣는 것은 허용되지 않습니다. 이 덕분에 안전하게 공변 대입이 가능합니다.
반대로 반공변성(in)은 소비(consumer) 역할일 때 사용합니다. in T로 선언하면 Consumer<Any>를 Consumer<String>에 대입할 수 있습니다. 즉, 상위 타입을 요구하는 자리에 하위 타입을 넣을 수 있습니다. 실제 사례로는 Comparator<in T>가 대표적입니다. Comparator<Any>는 Comparator<String> 대신 쓸 수 있는데, 어떤 객체든 받아서 비교할 수 있기 때문입니다. 또 하나의 예는 콜백 파라미터로 상위 타입을 받는 인터페이스를 정의할 때 in 키워드가 유용합니다.
정리하면, out은 producer, in은 consumer, invariant는 그대로라고 이해할 수 있고, List가 읽기 전용인 이유는 공변성을 안전하게 보장하기 위해서입니다.”

✅ 필수 키워드 문장

  • Invariant: 동일 타입만 대입 가능 → List<String> ≠ List<Any>
  • out 공변성: Producer<String> → Producer<Any> 가능, 읽기 전용(producer), 예시 : List<out T>
  • in 반공변성: Consumer<Any> → Consumer<String> 가능, 쓰기 전용(consumer), 예시: Comparator
  • List<out T>: 안전한 공변 대입을 위해 읽기 전용 설계
  • 실제 사례: Comparator<in T> / 콜백 인터페이스 파라미터

5-1 그렇다면, 왜 MutableList<out T> 같은 타입은 허용되지 않을까요?

“MutableList<out T> 같은 타입이 허용되지 않는 이유는 타입 안전성(type safety) 때문입니다.

공변성 out T는 ‘이 리스트에서는 T를 꺼낼 수만 있고, 넣을 수는 없다’는 의미입니다. 그런데 MutableList는 본질적으로 읽기와 쓰기 모두를 지원해야 합니다. 만약 MutableList<out String>을 허용한다면 이것을 MutableList<Any>로 간주할 수 있고, 결국 Any 타입 객체를 리스트에 추가할 수 있게 됩니다. 하지만 실제 내부는 String 리스트일 수도 있기 때문에, 런타임에서 타입 불일치(ClassCastException)가 발생하게 됩니다.

즉, MutableList는 타입 안정성을 깨지 않으려면 무공변(invariant)이어야 하고, 그래서 코틀린에서는 MutableList<out T> 같은 선언 자체를 허용하지 않습니다. 안전한 공변성을 제공하려면 List<out T>처럼 읽기 전용 인터페이스를 별도로 두는 방식이 필요합니다.”

✅ 필수 키워드 문장

  • MutableList는 읽기+쓰기 모두 지원
  • out T는 읽기 전용 제약 → MutableList 성격과 충돌
  • 허용 시 타입 불일치(ClassCastException) 위험 발생
  • 따라서 MutableList는 무공변(invariant)으로 설계
  • 공변성은 List<out T> 같은 읽기 전용 인터페이스에서만 제공

6. Deadlock(교착상태), Livelock, Starvation(기아)의 차이를 설명해 주세요. 또, Deadlock의 필요 조건(코프만 4조건)과 이를 예방/회피/탐지하는 대표 전략도 함께 말씀해 주세요.

“동시성 프로그래밍에서 교착상태(Deadlock), 활락상태(Livelock), 기아(Starvation)는 모두 스레드가 정상적으로 진척되지 못하는 문제지만 원인은 다릅니다.

  • Deadlock(교착상태): 여러 스레드가 서로가 가진 자원을 기다리면서 영원히 대기에 빠지는 상태입니다. 예를 들어 스레드 A는 자원 X를, 스레드 B는 자원 Y를 잡고 서로 상대 자원을 기다릴 때 발생합니다.

  • Livelock(활락상태): 스레드들이 교착을 피하려고 양보나 재시도를 계속하면서, 실제로는 아무 진전 없이 반복 동작만 하는 상태입니다. 멈춰 있지는 않지만 결과적으로 작업은 진행되지 않습니다.

  • Starvation(기아): 특정 스레드가 스케줄링에서 우선순위나 자원 경쟁 때문에 기회를 얻지 못해 계속 실행되지 못하는 상태입니다.

교착상태가 발생하기 위한 필요 조건, 즉 코프만(Koopman)의 4조건은 다음과 같습니다.

  • 상호 배제(Mutual Exclusion): 자원은 동시에 한 스레드만 사용 가능하다.
  • 점유 대기(Hold and Wait): 스레드가 자원을 점유한 채로 다른 자원을 기다린다.
  • 비선점(No Preemption): 자원을 강제로 빼앗을 수 없다.
  • 순환 대기(Circular Wait): 스레드들이 서로의 자원을 원하며 원형으로 기다린다.
    이 네 가지가 동시에 성립하면 Deadlock이 발생할 수 있습니다.

예방/회피/탐지 전략으로는,

  • 예방(Prevention): 아예 조건을 하나 이상 깨뜨리는 방식입니다. 예를 들어 자원을 모두 한 번에 요청하게 하여 ‘점유 대기’를 방지하거나, 순서를 정해 ‘순환 대기’를 없애는 방식입니다.
  • 회피(Avoidance): 은행가 알고리즘(Banker’s Algorithm)처럼 시스템 상태를 미리 점검하여 교착이 안전하지 않은 상태로 들어가는 것을 피합니다.
  • 탐지 및 복구(Detection & Recovery): 교착을 허용하되 주기적으로 대기 그래프를 검사해 사이클을 탐지하고, 발견되면 트랜잭션 롤백이나 스레드 강제 종료로 복구합니다.

정리하면, Deadlock은 자원 고갈로 멈춤, Livelock은 양보 반복으로 진척 없음, Starvation은 스케줄링 불균형으로 기회 상실입니다.”

✅ 필수 키워드 문장

  • Deadlock: 서로 자원 기다리며 영원히 멈춤
  • Livelock: 계속 양보/재시도 → 진전 없음
  • Starvation: 스케줄링/우선순위 때문에 기회 못 얻음
  • Deadlock 필요 조건(코프만 4조건): 상호 배제, 점유 대기, 비선점, 순환 대기
  • 예방: 조건 깨뜨리기 / 회피: 은행가 알고리즘 / 탐지: 대기 그래프 사이클 검사 후 복구

6-1 만약 운영체제에서 세마포어(semaphore)를 사용한다면, Deadlock과 Starvation 문제를 어떻게 예방하거나 완화할 수 있을까요?

“세마포어는 운영체제에서 공유 자원의 동시 접근을 제어하는 동기화 도구인데, 이를 잘못 사용하면 Deadlock이나 Starvation 문제가 생길 수 있습니다. 다만 적절히 설계하면 두 문제를 예방하거나 완화할 수 있습니다.

먼저 Deadlock의 경우, 세마포어를 사용할 때 모든 스레드가 자원을 일정한 순서로 획득하도록 규칙을 정하거나, 세마포어 획득 시 타임아웃(time-out)을 두어 무한 대기에 빠지지 않게 할 수 있습니다. 또 다수의 세마포어를 동시에 요구하는 상황을 피하거나, 필요 없는 경우 즉시 해제하는 방식으로 Deadlock 가능성을 줄일 수 있습니다.

Starvation의 경우에는 세마포어의 큐 관리 방식을 공정한 정책(FIFO 큐잉)으로 두면 특정 스레드가 무한히 기회를 잃는 문제를 완화할 수 있습니다. 우선순위 기반 스케줄링을 쓰더라도 에이징(aging) 기법을 적용해서 대기 시간이 길어질수록 우선순위를 점점 높여주는 방식으로 기아 상태를 방지할 수 있습니다.

즉, 세마포어 자체가 Deadlock과 Starvation을 자동으로 막아주는 것은 아니지만, 자원 획득 순서 규칙·타임아웃·공정한 큐 정책·에이징 같은 전략을 세마포어와 함께 쓰면 두 문제를 크게 줄일 수 있습니다.”

✅ 필수 키워드 문장

  • Deadlock 예방: 자원 획득 순서 규칙, 타임아웃 설정, 즉시 해제
  • Starvation 완화: FIFO 큐잉 정책, 에이징(aging) 기법
  • 세마포어 자체는 완벽한 해결책 아님 → 운영 정책 설계로 Deadlock/Starvation 방지

7. 안드로이드에서 XML 기반 UI와 Jetpack Compose UI의 차이를 설명해 주세요. 특히 성능, 빌드 구조, 상태 관리(State Management) 측면에서 어떻게 다른지 말씀해 주시면 좋겠습니다.

“XML 기반 UI는 명령형(View 트리 + 레이아웃 인플레이트) 방식이고, Compose는 선언형(함수가 상태→UI로 그려지는) 방식입니다.
성능 측면에서 XML은 레이아웃 인플레이트/측정/배치가 고정 비용이고 계층이 깊어질수록 오버드로·트리 변형 비용이 커집니다. 반면 Compose는 상태 변화가 닿는 지점만 미세 재구성(recomposition) 하므로 트리 전체를 건드리지 않고, 리스트도 Lazy* 컴포넌트로 필요한 범위만 컴포지션/측정합니다. 다만 Compose는 컴포지션·리컴포지션 비용이 있으므로 remember/derivedStateOf, 안정성(@Stable/@Immutable), key 제공, 불필요한 recomposition 차단이 중요합니다. 결과적으로 동적 UI/빈번한 상태 변경에서는 Compose가 유리하고, 정적·단순 레이아웃은 XML도 충분히 빠릅니다.

빌드 구조는 XML이 리소스/레이아웃 파일 + ViewBinding/DataBinding + Fragment/Adapter 중심으로 모듈이 리소스에 강결합됩니다. Compose는 Kotlin 소스만으로 UI를 정의하고 미리보기(Preview)·멀티모듈 분리가 용이해 UI 모듈의 순수 Kotlin화가 가능하며, 단일 액티비티 + 네비게이션 패턴에서 보일러플레이트가 크게 줄어듭니다. 또한 View와의 상호운용(ComposeView/AndroidView)으로 단계적 전환이 가능합니다.

상태 관리는 XML이 보통 ViewModel + LiveData/Flow + (DataBinding 선택)로 이벤트/바인딩을 연결하며, 구성 변경은 ViewModel로 완화하고 프로세스 킬 복원은 SavedStateHandle을 사용합니다. Compose는 단방향 데이터 흐름(UDF)을 전제해 state → UI, event → ViewModel로 흐르게 하고, state hoisting, remember/rememberSaveable로 수명 범위를 제어합니다. Flow 수집은 collectAsStateWithLifecycle로 생명주기 안전성을 확보하고, 네비게이션-상태도 호이스팅하여 테스트·재사용성을 높입니다. 요약하면 XML은 바인딩/어댑터 중심의 명령형, Compose는 상태 주도 선언형으로, 빈번한 상태 변화·모듈성·개발 생산성에서 Compose의 이점이 큽니다.”

필수 키워드 문장

  • 성능: XML = 인플레이트/깊은 트리 비용, Compose = 부분 리컴포지션 + Lazy 컴포넌트, remember/derivedStateOf로 최적화
  • 빌드 구조: XML = 리소스/Binding/Fragment 중심, Compose = Kotlin-only UI, Preview, 단일 액티비티, 쉬운 모듈화
  • 상태 관리: XML = ViewModel + LiveData/Flow(+DataBinding), Compose = UDF, state hoisting, remember/rememberSaveable, collectAsStateWithLifecycle
  • 전환/호환: ComposeView/AndroidView 상호운용, 단계적 마이그레이션 가능
  • 복원: SavedStateHandle(공통), Compose는 rememberSaveable로 구성 변경/프로세스 복원 보조

7-1 Compose의 Recomposition은 언제 일어나나요?

“Compose에서 Recomposition은 이미 그려진 UI를 처음부터 다시 그리는 게 아니라, 상태(State)가 변경될 때 해당 상태를 참조하는 컴포저블 함수만 다시 실행되는 과정을 말합니다.

구체적으로는, remember나 mutableStateOf, StateFlow/LiveData를 collectAsState()로 구독하고 있을 때 값이 바뀌면 그 값을 사용하는 컴포저블 범위만 다시 호출됩니다. 또, 부모의 파라미터가 달라지거나, derivedStateOf 등으로 감싼 계산된 값이 변경될 때도 Recomposition이 발생합니다.

하지만 Compose는 똑똑하게 최적화해서, 값이 동일하면 불필요한 Recomposition을 건너뛰고, 변경된 부분만 다시 실행합니다. 그래서 Recomposition은 UI 전체를 다시 그리는 게 아니라 상태 변경 → 관련 UI 부분만 재구성이라는 점이 핵심입니다.”

✅ 불필요한 Recomposition 줄이는 방법

  • remember / rememberSaveable 사용
    • 함수가 다시 호출돼도 상태를 유지 → 불필요한 UI 갱신 방지.
  • derivedStateOf 사용
    • 다른 State로부터 계산된 값을 메모이제이션. 불필요한 연산 감소.
  • stable / immutable 객체 활용
    • Compose는 객체의 안정성(@Stable)을 기반으로 변경 여부 판단 → 안정 객체 사용 시 불필요한 Recompose 방지.
  • key 사용 (특히 LazyColumn/Row)
    • items(key = { it.id }) → 특정 아이템만 Recompose, 리스트 전체 갱신 방지.
  • Recomposition Scope 최소화
  • UI를 잘게 나눠 Composable을 분리 → 일부만 Recompose 되도록 설계.

✅ 필수 키워드 문장

  • Recomposition: 상태 변경 시 참조하는 컴포저블만 다시 실행
  • 트리 전체 X, 관련 부분만 부분 재구성
  • 트리거: mutableStateOf / remember / collectAsState / 파라미터 값 변경
  • 값이 동일하면 건너뜀, Compose가 자동 최적화

8. 안드로이드에서 Room Database의 장점과 한계점을 설명해 주세요. 또, Room에서 마이그레이션(Migration)이 필요한 이유와, AutoMigration이 가능한 경우/불가능한 경우를 말해 주세요.

“Room은 안드로이드에서 SQLite를 추상화한 ORM 라이브러리로, 컴파일 시점 SQL 검사, 타입 안전성, LiveData/Flow 연계, 비동기 쿼리 지원 같은 장점이 있습니다. 개발자가 직접 Cursor를 다루지 않아도 되고, DAO 인터페이스로 간결하게 데이터 접근이 가능하며, 코루틴/Flow와 통합되어 반응형 패턴을 쉽게 구현할 수 있습니다.

하지만 한계도 있습니다. 복잡한 쿼리 최적화는 여전히 SQLite 지식이 필요하고, 대규모 데이터 처리에서는 제약이 있으며, 런타임 성능은 네이티브 SQLite에 비해 약간의 오버헤드가 있습니다. 또한 스키마 변경 시 반드시 마이그레이션 처리가 필요해 관리 비용이 늘어납니다.

마이그레이션(Migration)이 필요한 이유는, Room이 앱 설치 후 데이터베이스 스키마를 변경하면 기존 사용자 데이터가 깨지지 않도록 보장하기 위함입니다. 예를 들어 테이블 컬럼을 추가하거나 삭제하면, Room은 변경된 엔티티와 실제 DB 스키마를 비교하고, 일치하지 않으면 예외를 던집니다. 이때 마이그레이션 코드를 제공해야 앱이 정상 동작합니다.

AutoMigration이 가능한 경우는 컬럼 추가, 컬럼 이름 변경, 간단한 인덱스 변경처럼 Room이 자체적으로 안전하게 SQL을 생성할 수 있을 때입니다. 반대로 AutoMigration이 불가능한 경우는 컬럼 타입 변경, 복잡한 데이터 변환, 테이블 합치기/분리하기처럼 데이터 변환 로직이 필요한 경우입니다. 이럴 땐 개발자가 Migration 객체를 직접 작성해 ALTER TABLE이나 임시 테이블 복사 같은 로직을 넣어야 합니다.

즉, Room은 생산성과 안전성을 높여주지만, 스키마 변경 시 마이그레이션 전략을 어떻게 관리하느냐가 실무 활용의 핵심 포인트입니다.”

✅ Room Database 장점

  • ORM 지원: SQL을 직접 쓰지 않고 Kotlin/Java 객체와 매핑 가능. (@Entity, @Dao)
  • SQL 안정성 보장: 컴파일 타임 쿼리 검증으로 런타임 에러 감소.
  • LiveData/Flow 연동: 데이터 변경 → UI 자동 반영 가능.
  • 오프라인 퍼스트 전략: 네트워크 없이도 캐싱된 데이터로 앱 동작 가능.
  • 간단한 트랜잭션 관리: @Transaction 지원.

✅ Room Database 한계점

  • 마이그레이션 부담: 스키마 변경 시 반드시 마이그레이션 필요.
  • 복잡한 쿼리 한계: JOIN/Window 함수 등 고급 SQL 사용은 제약이 있을 수 있음.
  • 성능 한계: SQLite 기반이라 대용량/고성능 트랜잭션에는 한계.
  • NoSQL 부재: 유연한 스키마 변경이 어려움 (MongoDB 같은 문서 기반 DB 아님).

✅ 필수 키워드 문장

  • Room 장점: 컴파일 시점 SQL 검사, 타입 안전성, DAO 추상화, LiveData/Flow/Coroutine 연계
  • Room 한계: 복잡 쿼리 최적화 어려움, 성능 오버헤드, 스키마 변경 시 마이그레이션 필요
  • 마이그레이션 필요 이유: 스키마 변경 시 기존 데이터 보존
  • AutoMigration 가능: 컬럼 추가/이름 변경/단순 인덱스 변경
  • AutoMigration 불가: 컬럼 타입 변경, 데이터 변환 필요, 테이블 구조 재설계

8-2 Room을 멀티스레드 환경에서 안전하게 사용하려면 어떤 점을 주의해야 할까요? 예를 들어, 동시에 여러 DAO에서 insert/update를 실행하면 어떤 일이 발생할 수 있고, 이를 어떻게 제어할 수 있을까요?

“Room은 내부적으로 SQLite를 사용하기 때문에 여러 스레드에서 동시에 읽는 건 가능하지만, 쓰기(insert/update)는 한 번에 하나만 처리됩니다. 그래서 동시에 여러 DAO에서 쓰기를 실행하면 락 경쟁이 생겨 성능이 떨어지거나 database is locked 예외가 발생할 수 있습니다.

이를 안전하게 제어하려면 트랜잭션으로 묶어서 한 번에 처리하거나, Room이 제공하는 @Transaction 어노테이션과 runInTransaction 같은 기능을 활용하는 게 좋습니다. 또, WAL 모드(Write-Ahead Logging)를 켜면 읽기와 쓰기를 병행할 수 있어서 경합이 줄어듭니다. 마지막으로, 충돌이 예상되면 OnConflictStrategy.REPLACE 같은 정책을 지정해서 안정적으로 처리할 수 있습니다.”

✅ 필수 키워드 문장

  • SQLite 기반 → 읽기 병렬 OK, 쓰기 직렬 1개만 가능
  • 동시에 여러 DAO에서 쓰기 → 락 경쟁, database is locked 예외
  • 해결: 트랜잭션 묶기(@Transaction, runInTransaction/withTransaction)
  • WAL 모드로 동시 읽기/쓰기 완화
  • OnConflictStrategy로 충돌 정책 지정

9. 안드로이드 네트워킹에서 Retrofit + OkHttp + 코루틴을 사용할 때,코루틴 취소가 네트워크 호출에 어떻게 전파되는지,connect/read/write timeout 차이, 재시도(retry)와 백오프(backoff)를 설계하는 기본 원칙을 설명해 주세요.

“Retrofit은 내부적으로 OkHttp를 사용하고, OkHttp는 Call 객체를 기반으로 동작합니다. 코루틴에서 suspend 함수로 네트워크 요청을 실행하면, 코루틴이 취소될 때 OkHttp의 Call.cancel()이 호출되어 연결 시도나 읽기/쓰기 작업이 즉시 중단됩니다. 따라서 코루틴 취소가 네트워크 호출에도 안전하게 전파되어 불필요한 리소스 낭비를 막을 수 있습니다.

Timeout은 세 가지로 구분됩니다.

  • connect timeout: 서버와 TCP 연결을 맺는 데 걸리는 시간 제한
  • read timeout: 연결 후 서버로부터 응답 데이터를 읽을 때의 대기 시간 제한
  • write timeout: 요청 본문을 서버에 전송할 때의 제한
    각각 독립적으로 설정되며, 상황에 맞게 적절히 조정해야 합니다.

재시도와 백오프(backoff)는 신뢰성 있는 네트워킹을 위해 중요한 설계 원칙입니다. 네트워크는 일시적인 오류가 잦기 때문에 재시도 자체는 필요하지만, 무한 반복이나 즉시 재시도는 서버 부하를 키우고 클라이언트 리소스를 낭비합니다. 기본 원칙은 최대 재시도 횟수 제한, 지수적 백오프(재시도 간격을 점점 늘림), 그리고 네트워크 상태/HTTP 에러 코드에 따른 조건부 재시도를 적용하는 것입니다. 예를 들어 서버 500번대 에러나 네트워크 단절 시에만 재시도를 하고, 클라이언트 오류(400번대)에는 재시도를 하지 않는 방식입니다.”

✅ 3) Retry & Backoff 원칙

  • Retry: 일시적 네트워크 오류, 서버 5xx 에러, 타임아웃 같은 경우 재시도 가능.
  • Backoff: 단순히 즉시 재시도하면 서버/네트워크에 부하 → 점진적 지연(Exponential Backoff) 적용.
    예: 1초 → 2초 → 4초 → 8초... (최대 지연 한도 있음).
  • Jitter 적용: 무작위 지연(Random Jitter)을 섞어 다수 클라이언트가 동시에 폭주하는 상황을 방지.

✅ 필수 키워드 문장

  • 코루틴 취소 → OkHttp Call.cancel() 호출, 네트워크 요청 즉시 중단
  • Timeout: connect = 연결 수립 시간, read = 응답 읽기 시간, write = 요청 쓰기 시간
  • 재시도 설계: 최대 횟수 제한, 지수적 백오프, 조건부 재시도(500번대/네트워크 오류만)

8-1 Retrofit에서 suspend 함수와 Call.enqueue() 콜백 방식의 차이를 설명해 주실 수 있을까요? 특히 취소(cancellation) 처리 관점에서 어떻게 다른지요?

“Retrofit은 기본적으로 OkHttp의 Call을 감싸고 있는데, 사용할 때 suspend 함수 방식과 콜백 방식(Call.enqueue)은 동작과 취소 처리에서 차이가 있습니다.

suspend 함수는 코루틴과 직접 연결됩니다. 코루틴에서 viewModelScope.launch 같은 스코프 안에서 호출하면, 코루틴이 취소될 때 Retrofit 내부에서 OkHttp의 Call.cancel()이 호출되어 네트워크 요청 자체가 바로 취소됩니다. 즉, 코루틴 취소 → 네트워크 취소가 구조적으로 전파됩니다.

반면 Call.enqueue()를 쓰는 콜백 방식은 코루틴과 무관합니다. 스코프가 취소되어도 OkHttp의 요청은 계속 진행되고, 개발자가 직접 call.cancel()을 호출하지 않으면 네트워크는 끝까지 수행됩니다. 따라서 콜백 방식은 취소 전파가 자동으로 되지 않기 때문에 별도로 관리해야 하고, Activity/Fragment 생명주기에 맞춰 수동으로 취소를 걸어주는 로직이 필요합니다.

정리하면, suspend는 구조적 동시성·자동 취소 전파 장점이 있고, enqueue는 레거시 코드 호환·직접 제어 필요가 있다는 차이가 있습니다.”

✅ 필수 키워드 문장

  • suspend: 코루틴과 연동, 스코프 취소 시 OkHttp Call.cancel() 자동 호출
  • enqueue: 콜백 기반, 스코프 취소와 무관 → 직접 call.cancel() 필요
  • 차이: suspend = 구조적 동시성 / 자동 취소 전파, enqueue = 수동 관리

9 안드로이드에서 WorkManager와 AlarmManager의 차이를 설명해 주세요.

“WorkManager와 AlarmManager는 둘 다 안드로이드에서 백그라운드 작업을 예약하거나 실행할 수 있는 도구지만, 목적과 동작 방식이 다릅니다.

WorkManager는 지연되거나 보장된 작업을 처리하는 고수준 API입니다. 네트워크 연결, 충전 중, 유휴 상태 같은 제약 조건을 걸 수 있고, 앱이 프로세스가 죽더라도 작업을 보장하려고 내부적으로 JobScheduler나 AlarmManager, Firebase JobDispatcher 등을 적절히 사용합니다. 또한 작업이 반드시 한 번은 실행됨을 보장하며, 체이닝, 고유 작업(UniqueWork), 백오프 정책, Coroutine/LiveData 연계 등 현대적인 기능을 제공합니다. 주로 데이터 동기화, 로그 업로드, 주기적 백업 등 보장성과 유연성이 중요한 작업에 적합합니다.

반면 AlarmManager는 정해진 시각이나 주기로 정확한 시각에 작업을 깨워 실행하는 저수준 API입니다. 시스템이 절전 모드일 때도 깨울 수 있고, WorkManager보다 더 정확한 타이밍 제어가 가능합니다. 그러나 제약 조건 설정, 작업 보장, 체이닝 같은 기능은 없고, 실행 로직은 보통 BroadcastReceiver나 Service로 직접 구현해야 합니다. 주로 정확한 알람, 알림 예약, 시계 기능처럼 정확한 시각이 중요한 작업에 적합합니다.

요약하면, WorkManager는 보장된 실행 + 제약 조건 + 현대적 기능, AlarmManager는 정확한 시각 + 단순 반복 알람에 강점이 있습니다.”

✅ 필수 키워드 문장

  • WorkManager: 보장된 실행, 제약 조건 가능, 체이닝/UniqueWork/백오프 지원
  • AlarmManager: 정확한 시각 예약, 절전 모드 깨우기 가능, 제약 조건/보장 없음
  • WorkManager = 데이터 동기화, 백업 등 유연성/보장성 중요 작업
  • AlarmManager = 정확한 알람, 알림 예약 등 타이밍 중요 작업

9-1 그렇다면, WorkManager의 OneTimeWorkRequest와 PeriodicWorkRequest 차이를 설명해 주시겠어요? 특히 최소 주기(Interval) 제약이 어떻게 다른지도 말씀해 주세요.

“WorkManager에서 작업을 예약할 때는 OneTimeWorkRequest와 PeriodicWorkRequest 두 가지가 있습니다.

OneTimeWorkRequest는 이름 그대로 한 번만 실행되는 작업을 예약합니다. 앱이 강제 종료되거나 기기가 재부팅돼도 WorkManager가 조건이 만족되는 시점에 다시 실행해 주기 때문에 ‘보장된 단일 실행’에 적합합니다. 예를 들어 로그 업로드나 DB 동기화 같은 작업에 자주 쓰입니다.

반면 PeriodicWorkRequest는 주기적으로 반복 실행되는 작업을 예약합니다. WorkManager는 내부적으로 JobScheduler를 이용하기 때문에 시스템 자원과 배터리 보호를 위해 최소 주기(Interval)에 제약이 있습니다. 공식 문서 기준으로 최소 15분 이상의 간격만 허용됩니다. 더 짧은 간격을 지정하면 자동으로 15분으로 보정됩니다. 또한 정확한 주기가 아니라 시스템 상황에 따라 약간 지연될 수 있으며, 네트워크 연결 등 제약 조건이 있으면 그 조건을 만족할 때까지 대기합니다.

정리하면, OneTimeWorkRequest는 단일 실행 보장, PeriodicWorkRequest는 반복 실행을 위한 것이고, 주기 최소 제약은 PeriodicWorkRequest에서만 15분이 적용된다는 점이 가장 큰 차이입니다.”

✅ 필수 키워드 문장

  • OneTimeWorkRequest: 한 번만 실행, 조건 만족 시 보장된 단일 실행
  • PeriodicWorkRequest: 주기적 실행, 최소 주기 15분 제약
  • Periodic은 정확한 주기 X, 시스템 상황 따라 지연 가능
  • 사용 구분: OneTime = 단발성 동기화/업로드, Periodic = 반복 백업/로그 전송

10. 안드로이드 앱에서 Cold Start와 Warm Start의 차이를 설명해 주세요. 그리고 Cold Start 최적화를 위해 개발자가 할 수 있는 대표적인 방법을 2~3가지 정도 말씀해 주세요.

Cold Start는 앱 프로세스가 메모리에 전혀 없는 상태에서 처음 실행될 때를 말합니다. 이 경우 Application 객체 생성, ContentProvider 초기화, onCreate 호출, 첫 번째 Activity 인플레이트까지 모든 과정을 거치므로 시간이 가장 오래 걸립니다.

Warm Start는 앱 프로세스가 이미 살아 있고, 최근에 사용한 Activity가 메모리에 캐시된 상태에서 다시 실행되는 경우입니다. 이때는 시스템이 Activity를 다시 포그라운드로 가져오면 되기 때문에 Cold Start보다 훨씬 빠릅니다.

Cold Start를 최적화하기 위해 개발자가 할 수 있는 대표적인 방법은 다음과 같습니다.

Application 클래스의 초기화 최소화: 무거운 SDK 초기화, DB 세팅, DI 컨테이너 준비 같은 작업을 Application onCreate에 몰아넣지 말고, 필요한 시점에 지연 로딩(Lazy init)합니다.

Splash 화면 최적화: Android 12부터는 SplashScreen API가 제공되므로, 불필요한 커스텀 뷰 인플레이트를 줄이고 기본 제공 API를 활용해 초기 화면 전환을 빠르게 합니다.

레이아웃 단순화 및 비동기 초기화: 첫 Activity의 XML을 과도하게 중첩하지 않고, ConstraintLayout이나 Compose의 Lazy 컴포넌트를 활용해 측정/인플레이트 비용을 줄입니다. 네트워크 호출이나 DB 읽기 같은 무거운 작업은 백그라운드 코루틴으로 미루는 것도 중요합니다.

정리하면 Cold Start는 프로세스가 새로 뜰 때의 초기 구동, Warm Start는 기존 프로세스 재활용이며, Cold Start 최적화는 초기화 최소화·Splash 최적화·레이아웃 단순화가 핵심입니다.”

✅ 필수 키워드 문장
Cold Start: 프로세스 없음 → Application/Activity 생성 전체 과정, 가장 느림
Warm Start: 프로세스/Activity 메모리 캐시 존재 → 빠른 재실행
최적화 방법: Application 초기화 최소화, SplashScreen API 활용, 첫 레이아웃 단순화/비동기 초기화

profile
좋은 개발자가 되기까지

0개의 댓글