12월 21, 2025
53 분 읽기

시니어 Android 개발자 면접 질문: Kotlin, Compose, 아키텍처

interview
career-advice
job-search
시니어 Android 개발자 면접 질문: Kotlin, Compose, 아키텍처
Milad Bonakdar

Milad Bonakdar

작성자

Kotlin 코루틴, Jetpack Compose 상태, 앱 아키텍처, 성능, 테스트, 오프라인 우선 데이터, 보안을 중심으로 시니어 Android 면접을 준비하세요.


소개

시니어 Android 개발자 면접에서는 아키텍처 트레이드오프, 라이프사이클에 안전한 동시성, Compose 상태 소유권, 오프라인 우선 데이터 흐름, 성능 진단, 테스트 전략, 보안을 설명할 수 있어야 합니다. 좋은 답변은 어떤 API를 쓰는지에서 끝나지 않고, 제품 제약에 왜 맞는지와 프로덕션에서 어떻게 디버깅할지도 보여줍니다.

이 가이드는 실전 체크리스트로 활용하세요. Kotlin 코루틴과 Flow, ViewModel과 상태 관리, Room 기반 repository, Compose와 레거시 UI 선택, 시작 시간과 메모리, 그리고 본인의 앱에서 설명할 수 있는 사례를 먼저 준비하는 것이 좋습니다.

이 질문들을 활용하는 방법

  • 각 답변을 짧은 의사결정으로 연습하세요: 맥락, 트레이드오프, 구현, 실패 모드.
  • 기술 선택을 더 빠른 시작, 안정적인 오프라인 읽기, 안전한 토큰 저장, 라이프사이클 버그 감소 같은 사용자 영향과 연결하세요.
  • 아키텍처, 성능, 테스트, 보안에 대해 실제 프로젝트 사례를 하나씩 준비하세요.

고급 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

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

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

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

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

더 나은 이력서 만들기

이 게시물 공유

6초를 최대한 활용하세요

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