주니어 모바일 엔지니어 면접 질문
주니어 모바일 엔지니어 면접의 핵심, 자주 나오는 질문, 그리고 즉시 AI 피드백으로 연습하는 방법.
주니어 레벨에서 기대되는 것
플랫폼 기초, 기본 UI, 가이드가 주어진 기능 작업이 요구됩니다.
모바일 엔지니어 면접 질문 예시
- 기술 면접액티비티/뷰 컨트롤러 생명주기와 흔한 함정을 설명하세요.좋은 답변이 다루는 것
- Activity/Fragment 생명주기 콜백 순서 (onCreate → onStart → onResume 등)
- onSaveInstanceState와 복원 시점 (onCreate vs onRestoreInstanceState)
- Configuration 변경 시 생명주기 재시작과 Fragment 중첩 문제
- AsyncTask나 코루틴 Job 생명주기 미연동으로 인한 메모리 누수
- onPause와 onStop에서의 UI 상태 저장 타이밍 차이
샘플 답변 보기
액티비티 생명주기는 onCreate, onStart, onResume, onPause, onStop, onDestroy로 이어집니다. ViewController의 경우 viewDidLoad, viewWillAppear, viewDidAppear 등이 유사합니다. 가장 흔한 함정은 (1) onSaveInstanceState에서 저장한 데이터를 onRestoreInstanceState가 아닌 onCreate에서 복원하려다 null이 발생하는 경우입니다. (2) Configuration 변경(회전 등) 시 액티비티가 재생성되면서 Fragment가 중첩되어 보이는 문제는 FragmentManager의 상태 복원 순서를 잘못 처리하거나 중복 add할 때 발생합니다. (3) AsyncTask나 코루틴을 생명주기와 연결하지 않으면 액티비티 종료 후에도 작업이 계속되어 메모리 누수나 크래시가 발생합니다. (4) onPause에서는 UI 관련 일시 중지 작업(애니메이션, 센서)을, onStop에서는 무거운 리소스 해제를 해야 하며, onDestroy에서 해제하면 Activity가 백그라운드에 오래 있는 경우 불필요한 리소스 점유가 발생합니다. 이러한 함정을 피하려면 LifecycleObserver나 코루틴의 LifecycleScope를 활용하고, Fragment는 FragmentContainerView와 함께 사용하는 것이 좋습니다.
- 기술 면접저사양 기기에서 긴 스크롤 리스트를 어떻게 부드럽게 유지하나요?좋은 답변이 다루는 것
- ViewHolder 패턴과 재사용 (RecyclerView/LazyVStack)
- 이미지 지연 로딩과 캐싱 (LRU 캐시, 디스크 캐시)
- 레이아웃 계산 최소화 (고정 크기, ConstraintLayout, prefetch)
- 백그라운드 스레드에서 데이터 로딩 (DiffUtil, AsyncListDiffer)
- 하드웨어 가속 및 오버드로우 감소
샘플 답변 보기
부드러운 스크롤을 위해 가장 중요한 것은 RecyclerView(Android)나 UICollectionView(iOS)에서 ViewHolder를 재사용하여 뷰 생성 비용을 줄이는 것입니다. 이미지 로딩은 Glide/Picasso나 SDWebImage/Kingfisher와 같은 라이브러리를 사용해 메모리/디스크 LRU 캐시를 적용하고, 썸네일 크기로 리사이즈하여 다운샘플링합니다. 레이아웃 성능을 위해 ConstraintLayout 또는 Auto Layout을 효율적으로 사용하고, 아이템 높이를 고정하거나 예측 가능하게 해서 measure/layout 횟수를 최소화합니다. 또한 RecyclerView의 prefetch 기능(setItemPrefetchEnabled)이나 LazyVStack(LazyList)의 prefetch를 활성화하여 다음 아이템을 미리 로드합니다. 데이터 변경 시에는 DiffUtil을 사용해 최소한의 업데이트만 수행하고, 리스트 아이템 내에 불필요한 뷰 계층을 줄여 오버드로우를 방지합니다. 마지막으로 프로파일러로 병목을 확인하고, 스크롤 중에는 무거운 연산(네트워크, DB)을 백그라운드 스레드로 옮겨 메인 스레드를 블로킹하지 않는 것이 핵심입니다.
- 기술 면접나중에 동기화하는 오프라인 지원을 어떻게 설계하나요?좋은 답변이 다루는 것
- 로컬 DB(SQLite, Room)와 네트워크 API 간 데이터 모델 분리
- 오프라인 쓰기 큐와 충돌 해결 전략 (Last-Write-Wins, 작업 기반)
- 네트워크 상태 모니터링과 자동 동기화 트리거
- 동기화 중복 방지 (멱등성, 버전 관리)
- 사용자 경험: 오프라인 표시, 충돌 알림, 백그라운드 동기화
샘플 답변 보기
오프라인 지원을 위해 먼저 로컬 DB(Room, CoreData)에 데이터를 저장하고 네트워크 요청은 별도 큐에 쌓습니다. 데이터 모델은 서버 응답과 로컬 엔티티를 분리하여, 로컬 엔티티에는 동기화 상태(synced/pending)와 버전 필드를 추가합니다. 오프라인에서 생성/수정/삭제된 작업은 'change log' 또는 'operation queue' 형태로 저장하고, 네트워크 재연결 시 순차적으로 전송합니다. 충돌 해결은 간단한 Last-Write-Wins가 일반적이지만, 작업 기반 병합(CRDT)을 적용할 수도 있습니다. 동기화 과정에서 멱등성을 보장하기 위해 각 요청에 고유 ID를 포함시키고, 서버는 중복 요청을 무시합니다. 네트워크 상태는 ConnectivityManager나 NWPathMonitor로 감지하고, 연결이 복원되면 자동으로 동기화를 시작합니다. 사용자 경험을 위해 오프라인 상태를 명확히 표시하고, 충돌 시 사용자에게 선택권을 주거나 자동 병합 결과를 표시합니다. 동기화는 백그라운드에서 수행하며, 진행 상태와 오류를 사용자에게 알립니다.
- 코딩메모리와 디스크 계층을 갖는 이미지 캐시를 구현하세요.좋은 답변이 다루는 것
- 메모리 캐시: LRU 정책, LinkedHashMap 또는 LruCache
- 디스크 캐시: 파일 시스템, 제한된 공간, LRU eviction
- 이미지 키: URL 해시 또는 고유 식별자
- 동시성 제어: 읽기/쓰기 락 또는 ConcurrentHashMap
- 캐시 계층 구조: 먼저 메모리, 그다음 디스크, 없으면 네트워크
샘플 답변 보기
이미지 캐시는 두 계층으로 구성합니다. 메모리 캐시는 LruCache(Android) 또는 NSCache(iOS)를 사용하며, 일반적으로 최대 힙의 1/8 정도로 제한합니다. 디스크 캐시는 파일 시스템에 저장하며, LRU 정책을 구현하기 위해 각 파일의 마지막 접근 시간을 기록하거나 디렉터리 구조를 이용합니다. 키는 URL을 MD5나 SHA-1로 해싱하여 파일 이름으로 사용합니다. 동시성 문제를 피하기 위해 메모리 캐시는 Collections.synchronizedMap이나 ConcurrentHashMap을 래핑하고, 디스크 캐시는 읽기/쓰기 시 적절한 락을 사용합니다. 이미지 요청 시 먼저 메모리 → 디스크 → 네트워크 순서로 찾고, 네트워크에서 받은 이미지는 디스크와 메모리에 순차적으로 저장합니다. 디스크 공간이 제한에 도달하면 가장 오래된 파일을 삭제합니다. 다음은 간단한 구현 예시입니다.
참고 코드kotlin import java.security.MessageDigest import java.io.File import java.util.LinkedHashMap import java.util.concurrent.locks.ReentrantReadWriteLock class ImageCache(private val cacheDir: File, private val maxMemSize: Int, private val maxDiskSize: Long) { // 메모리 캐시: URL 해시 -> ByteArray (LRU) private val memCache = object : LinkedHashMap<String, ByteArray>(0, 0.75f, true) { override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, ByteArray>): Boolean { return size > maxMemSize } } private val memLock = ReentrantReadWriteLock() // 디스크 캐시: URL 해시 -> 파일 private val diskLock = ReentrantReadWriteLock() fun get(url: String): ByteArray? { val key = hash(url) // 메모리 검색 memLock.readLock().lock() try { memCache[key]?.let { return it } } finally { memLock.readLock().unlock() } // 디스크 검색 diskLock.readLock().lock() try { val file = File(cacheDir, key) if (file.exists()) { val data = file.readBytes() // 메모리에 저장 memLock.writeLock().lock() try { memCache[key] = data } finally { memLock.writeLock().unlock() } return data } } finally { diskLock.readLock().unlock() } return null } fun put(url: String, data: ByteArray) { val key = hash(url) // 메모리에 저장 memLock.writeLock().lock() try { memCache[key] = data } finally { memLock.writeLock().unlock() } // 디스크에 저장 (파일 쓰기) diskLock.writeLock().lock() try { val file = File(cacheDir, key) file.writeBytes(data) // 디스크 공간 관리: LRU eviction (단순화: 크기 체크 후 큰 파일 삭제) ensureDiskSpace() } finally { diskLock.writeLock().unlock() } } private fun hash(url: String): String { val digest = MessageDigest.getInstance("MD5") return digest.digest(url.toByteArray()).joinToString("") { "%02x".format(it) } } private fun ensureDiskSpace() { var totalSize = 0L val files = cacheDir.listFiles() ?: return // 파일 목록을 마지막 수정 시간 순으로 정렬 files.sortByDescending { it.lastModified() } for (file in files) { totalSize += file.length() } // 제한 초과 시 오래된 파일 삭제 var i = files.size - 1 while (totalSize > maxDiskSize && i >= 0) { totalSize -= files[i].length() files[i].delete() i-- } } } - 코딩사용자가 스크롤하면 더 불러오는 페이지네이션 리스트를 만드세요.좋은 답변이 다루는 것
- RecyclerView와 Paging 3 라이브러리 사용
- 스크롤 이벤트 감지 (onScrolled, scrollListener)
- 무한 스크롤 임계값 설정 (보통 마지막 아이템 보이기 전)
- 중복 요청 방지 (로딩 중 플래그)
- 새로고침(pull-to-refresh)과 오류 처리
샘플 답변 보기
Android에서는 Paging 3 라이브러리를 권장합니다. PagingSource를 구현하여 데이터 소스(네트워크, DB)를 연결하고, PagingData를 UI에 전달합니다. 직접 구현할 경우 RecyclerView의 OnScrollListener를 추가하여 마지막 보이는 아이템이 전체 아이템 개수 - 임계값(예: 3)에 도달하면 다음 페이지를 요청합니다. 요청 중에는 'isLoading' 플래그를 설정해 중복 요청을 방지합니다. 페이지네이션은 커서 기반(무한 스크롤) 또는 오프셋 기반(페이지 번호)으로 설계할 수 있으며, 커서 기반이 중복 데이터에 강합니다. 다음은 간단한 구현 예시입니다.
참고 코드kotlin import androidx.recyclerview.widget.RecyclerView class PaginationScrollListener( private val layoutManager: androidx.recyclerview.widget.LinearLayoutManager, private val onLoadMore: () -> Unit, private val threshold: Int = 3 ) : RecyclerView.OnScrollListener() { private var isLoading = false override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val visibleItemCount = layoutManager.childCount val totalItemCount = layoutManager.itemCount val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() if (!isLoading && (totalItemCount - visibleItemCount) <= (firstVisibleItemPosition + threshold)) { // 마지막 아이템에 가까워지면 로드 isLoading = true onLoadMore() } } fun setLoading(loading: Boolean) { isLoading = loading } } // 사용 예: recyclerView.addOnScrollListener(PaginationScrollListener(layoutManager) { viewModel.loadMore() }) - 시스템 설계오프라인에서 작동하고 재연결 시 동기화하는 모바일 채팅 앱을 설계하세요.좋은 답변이 다루는 것
- 메시지 로컬 저장소 (Local DB, 예: Room)
- 네트워크 연결 상태 감지와 자동 재연결
- 메시지 전송 큐와 재시도 메커니즘 (지수 백오프)
- 충돌 해결 및 메시지 순서 보장 (타임스탬프, 시퀀스 번호)
- 채팅 UI 업데이트: 낙관적 UI와 동기화 상태 표시
샘플 답변 보기
모바일 채팅 앱의 핵심은 오프라인에서도 메시지를 작성하고 보낼 수 있어야 하며, 재연결 시 자동 동기화되어야 합니다. 클라이언트는 Room과 같은 로컬 DB에 메시지를 저장하고, 각 메시지에 고유 ID와 전송 상태(pending/sent/delivered)를 포함합니다. WebSocket이나 Firebase Realtime Database 같은 실시간 채널을 사용해 메시지를 주고받으며, 연결이 끊어지면 큐에 보낼 메시지를 쌓고 네트워크가 복원되면 순서대로 전송합니다. 충돌 해결은 타임스탬프 또는 서버 생성 시퀀스 번호를 기준으로 병합하고, 동기화 중 중복을 방지하기 위해 클라이언트 메시지 ID의 멱등성을 보장합니다. UI는 낙관적 업데이트를 적용하여 사용자가 보낸 메시지를 즉시 표시하고, 동기화 상태를 아이콘(전송 중, 전송 완료, 읽음)으로 표시합니다. 서버는 메시지를 수신하면 모든 연결된 디바이스에 푸시 알림을 보내고, 오프라인 중이던 사용자는 재연결 시 마지막 읽은 시점 이후 메시지를 배치로 동기화합니다. 데이터 무결성을 위해 각 메시지에는 HMAC이나 서명을 추가할 수 있습니다.
- 행동 면접추적해서 잡아낸 까다로운 크래시나 메모리 버그를 말해 주세요.좋은 답변이 다루는 것
- 메모리 관리 버그: 순환 참조로 인한 메모리 누수
- 멀티스레드 동기화 문제: 경합 조건(Race condition)
- 타이밍 이슈: 생명주기 콜백 내 비동기 작업
- 디바이스 특화 문제: 다양한 화면 크기, API 레벨 차이
- 디버깅 방법: 프로파일러, 로그 분석, 레거시 코드 단서
샘플 답변 보기
과거에 겪었던 까다로운 버그 중 하나는 Fragment에서 내부 클래스로 정의한 Handler가 액티비티가 destroy된 후에도 메시지를 처리하여 발생한 메모리 누수였습니다. Handler는 외부 클래스의 참조를 암시적으로 가지고 있어 GC가 회수하지 못했습니다. 이를 해결하기 위해 Handler를 정적 내부 클래스로 변경하고 WeakReference로 액티비티를 참조했습니다. 또 다른 사례는 RecyclerView에서 이미지 로딩 시 발생한 경합 조건으로, 아이템이 재활용되면서 이전 요청의 콜백이 잘못된 이미지를 설정하는 문제였습니다. 이는 이미지 요청에 태그(아이템 위치나 URL)를 달고 콜백에서 현재 아이템의 태그와 비교하여 일치할 때만 업데이트하도록 수정했습니다. 이러한 버그는 프로파일러(Android Studio Memory Profiler, LeakCanary)와 로그 분석을 통해 발견했으며, 실제 사용자에게 크래시가 발생하기 전에 자체 테스트에서 잡아내는 것이 중요합니다.
- 행동 면접새 기능과 앱 크기·성능을 어떻게 균형 잡나요?좋은 답변이 다루는 것
- 모듈화: 동적 기능 모듈(Dynamic Feature Module) 사용
- 지연 초기화(Lazy loading): 필요한 라이브러리만 로드
- 코드 난독화 및 리소스 축소(ProGuard, shrinkResources)
- 성능 프로파일링: 기능 추가 전후 베이스라인 측정
- 트레이드오프 문서화: 사용자 가치 대비 비용 평가
샘플 답변 보기
새 기능을 추가할 때는 먼저 사용자 가치와 기술적 비용을 평가합니다. 앱 크기가 증가하는 것을 막기 위해 Dynamic Feature Module(Android)이나 On-Demand Resources(iOS)를 활용하여 자주 사용하지 않는 기능은 필요할 때만 다운로드받도록 합니다. 또, 지연 초기화 패턴을 적용하여 앱 시작 시 모든 라이브러리를 초기화하지 않고 실제 사용 시점에 로드합니다. 코드 난독화와 리소스 축소(ProGuard/R8, shrinkResources)로 불필요한 코드와 리소스를 제거합니다. 성능 측면에서는 기능 추가 전후로 프레임 드롭, 메모리 사용량, 배터리 소모를 프로파일링하고, 기준을 넘지 않도록 최적화합니다. 예를 들어, 새로운 애니메이션을 추가할 때는 GPU 오버드로우와 CPU 사용량을 확인하고, 필요하면 렌더링 파이프라인을 조정합니다. 마지막으로, 기능의 중요도에 따라 A/B 테스트를 통해 실제 성능 영향을 측정하고, 만약 성능 저하가 크다면 기능을 축소하거나 대체 방안을 고려합니다.
면접관이 평가하는 것
플랫폼 깊이
iOS(Swift) 또는 Android(Kotlin) 생명주기, 스레딩, 메모리.
UI와 성능
부드러운 리스트, 렌더링, 끊김, 배터리 고려사항.
앱 아키텍처
MVVM/MVI, 내비게이션, 의존성 주입, 모듈성.
오프라인과 동기화
로컬 저장소, 캐싱, 충돌 해결, 연결 손실.
릴리스 엔지니어링
앱 스토어 빌드, 크래시 리포팅, 점진적 롤아웃.
준비 방법
- 생명주기와 메모리로 시작하세요 — 가장 흔한 실패 지점입니다.
- 오프라인과 불안정한 네트워크를 먼저 논의하세요. 모바일 면접관은 이를 기대합니다.
- 측정한다는 것을 보여 주세요: 프로파일링, 크래시율, 프레임 타이밍이 막연한 설명을 이깁니다.
자주 묻는 질문
모바일 면접에 자료 구조 코딩이 포함되나요?
종종 그렇습니다. 더해 캐시나 페이지네이션 리스트 구축 같은 플랫폼 고유 코딩과 앱 아키텍처 논의가 있습니다.
모바일 직무에서 어떤 아키텍처 질문이 나오나요?
MVVM/MVI, 내비게이션, 의존성 주입, 동기화를 갖춘 오프라인 지원 앱 설계가 나옵니다.
모바일 엔지니어링 면접을 어떻게 준비하나요?
플랫폼 생명주기와 메모리를 복습하고, 작은 기능을 처음부터 만들며, 모의 면접에서 아키텍처 결정을 리허설하세요.
모바일 엔지니어 질문을 AI의 즉각적인 피드백으로 연습
Offersly는 당신의 이력서와 목표 직무에 맞춘 모의 면접을 진행하고, 모든 답변을 관련성·깊이·명확성·정확성으로 채점합니다.