12월 21, 2025
53 분 읽기

고급 안드로이드 모바일 개발자 면접 질문: 완벽 가이드

interview
career-advice
job-search
고급 안드로이드 모바일 개발자 면접 질문: 완벽 가이드
MB

Milad Bonakdar

작성자

고급 안드로이드 개발 마스터를 위한 필수 면접 질문들을 소개합니다. 아키텍처 패턴, 성능 최적화, 의존성 주입, 테스팅, 보안 및 시스템 설계 등 고급 개발자를 위한 내용을 다룹니다.


소개

Android 시니어 개발자는 높은 성능과 코드 품질을 보장하면서 확장 가능하고 유지 관리 가능한 애플리케이션을 설계할 수 있어야 합니다. 이 역할은 Android 프레임워크, 아키텍처 패턴, 의존성 주입, 테스팅 전략에 대한 깊은 전문 지식과 정보에 입각한 기술적 결정을 내릴 수 있는 능력을 요구합니다.

이 종합 가이드는 고급 Kotlin 개념, 아키텍처 패턴, 성능 최적화, 의존성 주입, 테스팅, 시스템 설계에 걸쳐 시니어 Android 개발자를 위한 필수 면접 질문을 다룹니다. 각 질문에는 자세한 답변, 희소성 평가, 난이도 등급이 포함되어 있습니다.


고급 Kotlin & 언어 기능 (5 문제)

1. Kotlin 코루틴과 스레드에 비해 장점을 설명하세요.

답변: 코루틴은 비동기 코드를 순차적으로 작성할 수 있게 해주는 경량 동시성 기본 요소입니다.

  • 스레드에 비해 장점:
    • 경량: 성능 문제 없이 수천 개의 코루틴을 생성할 수 있습니다.
    • 구조화된 동시성: 부모-자식 관계는 적절한 정리를 보장합니다.
    • 취소 지원: 내장된 취소 전파
    • 예외 처리: 구조화된 예외 처리
  • 주요 구성 요소:
    • CoroutineScope: 라이프사이클을 정의합니다.
    • Dispatchers: 실행 컨텍스트를 제어합니다 (Main, IO, Default).
    • suspend functions: 일시 중지 및 재개될 수 있습니다.
class UserRepository(private val api: ApiService) {
    suspend fun getUser(id: Int): Result<User> = withContext(Dispatchers.IO) {
        try {
            val user = api.fetchUser(id)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = repository.getUser(id)) {
                is Result.Success -> _user.value = result.data
                is Result.Error -> handleError(result.exception)
            }
        }
    }
}

희소성: 매우 흔함 난이도: 어려움


2. Sealed 클래스란 무엇이며 언제 사용해야 할까요?

답변: Sealed 클래스는 모든 하위 클래스가 컴파일 시간에 알려진 제한된 클래스 계층 구조를 나타냅니다.

  • 장점:
    • 완전한 when 표현식
    • 타입 안전 상태 관리
    • 복잡한 데이터에 대한 열거형보다 나음
  • 사용 사례: 상태, 결과, 탐색 이벤트를 나타냅니다.
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser()
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// UI에서
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // 컴파일러는 모든 경우를 처리하는지 확인합니다.
}

희소성: 흔함 난이도: 중간


3. Kotlin Flow를 설명하고 LiveData와 어떻게 다른지 설명하세요.

답변: Flow는 값을 순차적으로 방출하는 Kotlin의 콜드 비동기 스트림입니다.

  • Flow vs LiveData:
    • Flow: 콜드 스트림, 연산자 지원, 라이프사이클 인식 아님, 더 유연함
    • LiveData: 핫 스트림, 라이프사이클 인식, Android 특정, UI에 더 간단함
  • Flow 유형:
    • Flow: 콜드 스트림 (수집 시 시작)
    • StateFlow: 현재 상태가 있는 핫 스트림
    • SharedFlow: 이벤트를 위한 핫 스트림
class UserRepository {
    // 콜드 Flow - 수집 시 시작
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - 핫, 상태 유지
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - 핫, 이벤트를 위해
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()
}

class UserViewModel : ViewModel() {
    val users = repository.getUsers()
        .map { it.filter { user -> user.isActive } }
        .catch { emit(emptyList()) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// Activity/Fragment에서 수집
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.users.collect { users ->
            updateUI(users)
        }
    }
}

희소성: 매우 흔함 난이도: 어려움


4. Inline 함수란 무엇이며 언제 사용해야 할까요?

답변: Inline 함수는 함수 호출 오버헤드를 피하기 위해 함수 본문을 호출 사이트로 복사합니다.

  • 장점:
    • 람다 할당 오버헤드 제거
    • 람다에서 비지역 반환 허용
    • 고차 함수에 대한 더 나은 성능
  • 사용 사례: 람다 매개변수가 있는 고차 함수
  • Trade-off: 코드 크기 증가
// Inline 없이 - 람다 객체 생성
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// Inline으로 - 람다 객체 생성되지 않음
inline fun measureTimeInline(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// noinline - 특정 매개변수가 인라인되는 것을 방지
inline fun performOperation(
    inline operation: () -> Unit,
    noinline logger: () -> Unit
) {
    operation()
    saveLogger(logger)  // noinline 람다 저장 가능
}

// crossinline - 인라인을 허용하지만 비지역 반환을 방지
inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()  // 외부 함수에서 반환 불가
    }
}

희소성: 중간 난이도: 어려움


5. Kotlin에서 Delegation을 설명하세요.

답변: Delegation을 사용하면 객체가 일부 책임을 다른 객체에 위임할 수 있습니다.

  • 클래스 Delegation: by 키워드
  • 속성 Delegation: Lazy, observable, delegates
  • 장점: 코드 재사용, 상속보다 구성
// 클래스 delegation
interface Repository {
    fun getData(): String
}

class RemoteRepository : Repository {
    override fun getData() = "Remote data"
}

class CachedRepository(
    private val remote: Repository
) : Repository by remote {
    private var cache: String? = null
    
    override fun getData(): String {
        return cache ?: remote.getData().also { cache = it }
    }
}

// 속성 delegation
class User {
    // Lazy 초기화
    val database by lazy {
        Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
    }
    
    // Observable 속성
    var name: String by Delegates.observable("Initial") { prop, old, new ->
        println("$old -> $new")
    }
    
    // 사용자 지정 delegate
    var token: String by PreferenceDelegate("auth_token", "")
}

class PreferenceDelegate(
    private val key: String,
    private val defaultValue: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return sharedPreferences.getString(key, defaultValue) ?: defaultValue
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        sharedPreferences.edit().putString(key, value).apply()
    }
}

희소성: 중간 난이도: 중간


아키텍처 패턴 (6 문제)

6. MVVM 아키텍처와 장점을 설명하세요.

답변: MVVM (Model-View-ViewModel)은 UI 로직과 비즈니스 로직을 분리합니다.

Loading diagram...
  • Model: 데이터 레이어 (repositories, data sources)
  • View: UI 레이어 (Activities, Fragments, Composables)
  • ViewModel: 프레젠테이션 로직, 구성 변경에서 살아남음
  • 장점: 테스트 가능, 관심사 분리, 라이프사이클 인식
// Model
data class User(val id: Int, val name: String, val email: String)

// Repository
class UserRepository(
    private val api: ApiService,
    private val dao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return dao.getUser(id) ?: api.fetchUser(id).also {
            dao.insertUser(it)
        }
    }
}

// ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

// View (Fragment)
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UiState.Loading -> showLoading()
                    is UiState.Success -> showUser(state.data)
                    is UiState.Error -> showError(state.message)
                }
            }
        }
    }
}

희소성: 매우 흔함 난이도: 중간


7. Clean Architecture란 무엇이며 Android에서 어떻게 구현하나요?

답변: Clean Architecture는 코드를 명확한 종속성이 있는 레이어로 분리합니다.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • 종속성 규칙: 내부 레이어는 외부 레이어에 대해 알지 못합니다.
// Domain Layer - Entities
data class User(val id: Int, val name: String, val email: String)

// Domain Layer - Repository Interface
interface UserRepository {
    suspend fun getUser(id: Int): Result<User>
}

// Domain Layer - Use Case
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: Int): Result<User> {
        return repository.getUser(id)
    }
}

// Data Layer - Repository Implementation
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    override suspend fun getUser(id: Int): Result<User> {
        return try {
            val localUser = localDataSource.getUser(id)
            if (localUser != null) {
                Result.success(localUser)
            } else {
                val remoteUser = remoteDataSource.fetchUser(id)
                localDataSource.saveUser(remoteUser)
                Result.success(remoteUser)
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Presentation Layer - ViewModel
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = getUserUseCase(id)) {
                is Result.Success -> handleSuccess(result.data)
                is Result.Failure -> handleError(result.exception)
            }
        }
    }
}

희소성: 흔함 난이도: 어려움


8. Dependency Injection과 Dagger/Hilt를 설명하세요.

답변: Dependency Injection은 클래스 내부에서 종속성을 생성하는 대신 클래스에 종속성을 제공합니다.

  • 장점: 테스트 가능성, 느슨한 결합, 재사용성
  • Dagger: 컴파일 시간 DI 프레임워크
  • Hilt: Android용 간소화된 Dagger
// Hilt로 종속성 정의
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
            .build()
    }
    
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// 주입된 종속성이 있는 Repository
@Singleton
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return userDao.getUser(id) ?: apiService.fetchUser(id).also {
            userDao.insertUser(it)
        }
    }
}

// 주입된 repository가 있는 ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ViewModel 로직
}

// Activity/Fragment
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
}

희소성: 매우 흔함 난이도: 어려움


9. Repository 패턴이란 무엇이며 왜 사용해야 할까요?

답변: Repository 패턴은 데이터 소스를 추상화하여 데이터 액세스를 위한 깔끔한 API를 제공합니다.

  • 장점:
    • 단일 소스 진실
    • 중앙 집중식 데이터 로직
    • 데이터 소스 전환 용이
    • 테스트 가능
  • 구현: 여러 데이터 소스 간 조정
interface UserRepository {
    suspend fun getUsers(): Flow<List<User>>
    suspend fun getUser(id: Int): User?
    suspend fun saveUser(user: User)
    suspend fun deleteUser(id: Int)
}

class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource,
    private val cacheDataSource: UserCacheDataSource
) : UserRepository {
    
    override suspend fun getUsers(): Flow<List<User>> = flow {
        // 캐시된 데이터를 먼저 내보냅니다.
        val cachedUsers = cacheDataSource.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // 로컬 데이터베이스에서 가져옵니다.
        val localUsers = localDataSource.getUsers()
        if (localUsers.isNotEmpty()) {
            emit(localUsers)
            cacheDataSource.saveUsers(localUsers)
        }
        
        // 원격에서 가져옵니다.
        try {
            val remoteUsers = remoteDataSource.fetchUsers()
            localDataSource.saveUsers(remoteUsers)
            cacheDataSource.saveUsers(remoteUsers)
            emit(remoteUsers)
        } catch (e: Exception) {
            // 원격이 실패하면 이미 캐시/로컬 데이터를 내보냈습니다.
        }
    }
    
    override suspend fun getUser(id: Int): User? {
        return cacheDataSource.getUser(id)
            ?: localDataSource.getUser(id)
            ?: remoteDataSource.fetchUser(id)?.also {
                localDataSource.saveUser(it)
                cacheDataSource.saveUser(it)
            }
    }
}

희소성: 매우 흔함 난이도: 중간


10. Single Activity 아키텍처를 설명하세요.

답변: Single Activity 아키텍처는 Navigation Component에서 관리하는 여러 Fragments가 있는 하나의 Activity를 사용합니다.

  • 장점:
    • 간소화된 탐색
    • 프래그먼트 간 공유 ViewModels
    • 더 나은 애니메이션
    • 더 쉬운 딥 링크
  • Navigation Component: 프래그먼트 트랜잭션, 백 스택, 인수를 처리합니다.
// 탐색 그래프 (nav_graph.xml)
<navigation>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.app.HomeFragment">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.app.DetailFragment">
        <argument
            android:name="userId"
            app:argType="integer" />
    </fragment>
</navigation>

// MainActivity - 단일 activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navController = findNavController(R.id.nav_host_fragment)
        setupActionBarWithNavController(navController)
    }
}

// HomeFragment - 세부 정보로 탐색
class HomeFragment : Fragment() {
    fun navigateToDetail(userId: Int) {
        val action = HomeFragmentDirections.actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
}

// DetailFragment - 인수 수신
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userId = args.userId
        viewModel.loadUser(userId)
    }
}

희소성: 흔함 난이도: 중간


11. MVI (Model-View-Intent) 아키텍처란 무엇인가요?

답변: MVI는 Redux에서 영감을 받은 단방향 데이터 흐름 아키텍처입니다.

  • 구성 요소:
    • Model: UI 상태를 나타냅니다.
    • View: 상태를 렌더링하고 intents를 내보냅니다.
    • Intent: 사용자 작업/이벤트
  • 장점: 예측 가능한 상태, 쉬운 디버깅, 시간 여행 디버깅
// State
data class UserScreenState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

// Intent (사용자 작업)
sealed class UserIntent {
    data class LoadUser(val id: Int) : UserIntent()
    object RetryLoading : UserIntent()
}

// ViewModel
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow(UserScreenState())
    val state: StateFlow<UserScreenState> = _state.asStateFlow()
    
    fun processIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.id)
            is UserIntent.RetryLoading -> retryLoading()
        }
    }
    
    private fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true, error = null)
            try {
                val user = repository.getUser(id)
                _state.value = _state.value.copy(
                    isLoading = false,
                    user = user
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

// View
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // Intent 보내기
        viewModel.processIntent(UserIntent.LoadUser(123))
        
        // 상태 관찰
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.state.collect { state ->
                render(state)
            }
        }
    }
    
    private fun render(state: UserScreenState) {
        when {
            state.isLoading -> showLoading()
            state.error != null -> showError(state.error)
            state.user != null -> showUser(state.user)
        }
    }
}

희소성: 중간 난이도: 어려움


성능 및 최적화 (5 문제)

12. RecyclerView 성능을 어떻게 최적화하나요?

답변: 여러 전략이 RecyclerView 스크롤 성능을 향상시킵니다.

  1. ViewHolder 패턴: 뷰 재사용 (내장)
  2. DiffUtil: 효율적인 목록 업데이트
  3. Stable IDs: getItemId()를 재정의하고 setHasStableIds(true)를 설정합니다.
  4. Prefetching: 프리페치 거리 증가
  5. 이미지 로딩: 적절한 크기 조정을 통해 Glide/Coil과 같은 라이브러리 사용
  6. 과도한 작업 방지: onBindViewHolder에서 비용이 많이 드는 계산을 수행하지 마십시오.
  7. 중첩된 RecyclerViews: setRecycledViewPool()setHasFixedSize(true)를 설정합니다.
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    override fun getItemId(position: Int): Long {
        return getItem(position).id.toLong()
    }
}

class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }
    
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
    }
}

// Fragment/Activity에서
recyclerView.apply {
    setHasFixedSize(true)
    adapter = userAdapter
    // 프리페치 증가
    (layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}

// 목록을 효율적으로 업데이트
userAdapter.submitList(newUsers)

희소성: 매우 흔함 난이도: 중간


13. Android에서 메모리 누수를 감지하고 수정하는 방법은 무엇인가요?

답변: 메모리 누수는 객체가 필요한 것보다 오래 메모리에 보관될 때 발생합니다.

  • 일반적인 원인:
    • 컨텍스트 누수 (Activity/Fragment 참조)
    • 정적 참조
    • 익명 내부 클래스
    • 등록되지 않은 리스너
    • 취소되지 않은 코루틴
  • 감지 도구:
    • LeakCanary 라이브러리
    • Android Studio Memory Profiler
    • 힙 덤프
// 나쁨 - 메모리 누수
class MyActivity : AppCompatActivity() {
    companion object {
        var instance: MyActivity? = null  // 정적 참조 누수
    }
    
    private val handler = Handler()  // Activity에 대한 암시적 참조
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        
        // 익명 클래스는 Activity 참조를 보유합니다.
        handler.postDelayed({
            // Activity가 파괴될 수 있습니다.
        }, 10000)
    }
}

// 좋음 - 메모리 누수 없음
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 필요한 경우 약한 참조 사용
        val weakRef = WeakReference(this)
        handler.postDelayed({
            weakRef.get()?.let { activity ->
                // activity를 안전하게 사용할 수 있습니다.
            }
        }, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)  // 정리
    }
}

// ViewModel - 구성 변경에서 살아남음
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()  // 코루틴 취소
    }
}

희소성: 매우 흔함 난이도: 중간


14. 앱 시작 시간을 어떻게 최적화하나요?

답변: 더 빠른 시작은 사용자 경험을 향상시킵니다.

  1. Lazy Initialization: 필요한 경우에만 객체를 초기화합니다.
  2. Application.onCreate()에서 과도한 작업 방지:
    • 백그라운드 스레드로 이동
    • 중요하지 않은 초기화 연기
  3. Content Providers: 최소화하거나 lazy-load합니다.
  4. 종속성 감소: 라이브러리가 적을수록 시작 속도가 빨라집니다.
  5. App Startup Library: 구조화된 초기화
  6. Baseline Profiles: AOT (Ahead-of-time) 컴파일 힌트
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 중요한 초기화만
        initCrashReporting()
        
        // 중요하지 않은 작업 연기
        lifecycleScope.launch {
            delay(100)
            initAnalytics()
            initImageLoader()
        }
        
        // 백그라운드 초기화
        CoroutineScope(Dispatchers.IO).launch {
            preloadData()
        }
    }
}

// App Startup library
class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.getInstance(context).apply {
            initialize()
        }
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}

// Lazy 초기화
class MyActivity : AppCompatActivity() {
    private val database by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
    }
    
    // 액세스할 때만 생성됨
    private val heavyObject by lazy {
        createHeavyObject()
    }
}

희소성: 흔함 난이도: 중간


15. 비트맵 로딩 및 캐싱을 효율적으로 처리하는 방법은 무엇인가요?

답변: 효율적인 이미지 처리는 성능에 매우 중요합니다.

  • 라이브러리: Glide, Coil (자동으로 캐싱 처리)
  • 수동 최적화:
    • 다운샘플링 (더 작은 이미지 로드)
    • 메모리 캐시 (LruCache)
    • 디스크 캐시
    • 비트맵 풀링
// Glide 사용
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .override(width, height)  // 크기 조정
    .into(imageView)

// LruCache를 사용한 수동 구현
class ImageCache {
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8
    
    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount / 1024
        }
    }
    
    fun getBitmap(key: String): Bitmap? {
        return memoryCache.get(key)
    }
    
    fun putBitmap(key: String, bitmap: Bitmap) {
        if (getBitmap(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }
}

// 큰 이미지 다운샘플링
fun decodeSampledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(file.path, this)
        
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        
        BitmapFactory.decodeFile(file.path, this)
    }
}

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    
    return inSampleSize
}

희소성: 흔함 난이도: 어려움


16. ANR이란 무엇이며 어떻게 방지하나요?

답변: ANR (Application Not Responding)은 기본 스레드가 너무 오랫동안 차단될 때 발생합니다.

  • 원인:
    • 기본 스레드에서 과도한 계산
    • 기본 스레드에서 네트워크 호출
    • 기본 스레드에서 데이터베이스 작업
    • 데드락
  • 예방:
    • 과도한 작업을 백그라운드 스레드로 이동
    • 적절한 디스패처로 코루틴 사용
    • 기본 스레드에서 동기화된 블록 방지
    • 백그라운드 작업에 WorkManager 사용
// 나쁨 - 기본 스레드 차단
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ANR 위험!
        val data = fetchDataFromNetwork()  // UI 스레드 차단
        processLargeFile()  // UI 스레드 차단
    }
}

// 좋음 - 백그라운드 처리
class GoodActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.loadData()  // 코루틴으로 ViewModel에서 처리
    }
}

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // 자동으로 Main 디스패처에서
            showLoading()
            
            // 네트워크에 대한 IO 디스패처로 전환
            val data = withContext(Dispatchers.IO) {
                fetchDataFromNetwork()
            }
            
            // 과도한 계산을 위해 Default 디스패처로 전환
            val processed = withContext(Dispatchers.Default) {
                processData(data)
            }
            
            // UI 업데이트를 위해 Main으로 돌아갑니다.
            updateUI(processed)
        }
Newsletter subscription

실제로 효과가 있는 주간 커리어 팁

최신 인사이트를 받은 편지함으로 직접 받아보세요

Decorative doodle

채용률을 60% 높이는 이력서 만들기

몇 분 만에 면접을 6배 더 많이 받는 것으로 입증된 맞춤형 ATS 친화적 이력서를 만드세요.

더 나은 이력서 만들기

이 게시물 공유

6초를 최대한 활용하세요

채용 담당자는 평균적으로 6~7초만 이력서를 훑어봅니다. 우리의 검증된 템플릿은 즉시 주목을 끌고 계속 읽게 하도록 설계되었습니다.