Manifest Android Interview 책을 읽고 Practical Questions 에 대한 답변을 작성해보고, 카테고리 내에 특정 개념에 대한 딥다이브 및 스터디 시간에 얘기 나누면 좋을 내용들을 적어보는 글입니다.
답변 정리는 LLM의 도움을 받았습니다.
Q) 49. AppCompat 라이브러리란 무엇인가?
AppCompat은 Android Support Library의 핵심 구성 요소로, 구 버전 Android에서도 최신 UI 기능을 사용할 수 있게 해주는 라이브러리
Q) AppCompat 라이브러리는 구 버전 Android에서 Material Design 지원을 어떻게 가능하게 하며, 이로부터 혜택을 받는 주요 UI 컴포넌트들은 무엇인가?
Material Design 지원 방식:
백포팅(Backporting): 최신 Android의 UI 컴포넌트를 구 버전에서도 동작하도록 구현
테마 시스템: Material Design 테마를 API 14+에서 사용 가능
벡터 드로어블: VectorDrawable을 구 버전에서도 지원
// AppCompatActivity - 최신 기능을 구 버전에서 사용
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ActionBar 대신 Toolbar 사용 (구 버전에서도 Material Design)
setSupportActionBar(findViewById(R.id.toolbar))
}
}
혜택받는 주요 컴포넌트:
AppCompatButton: Material Design 버튼 스타일
AppCompatEditText: Material Design 입력 필드
Toolbar: ActionBar를 대체하는 유연한 툴바
AlertDialog: Material Design 다이얼로그
RecyclerView: 성능 최적화된 리스트뷰
Q) 50. 머티리얼 디자인 컴포넌트(MDC)란 무엇인가?
MDC는 Google의 Material Design 가이드라인을 Android 앱에서 쉽게 구현할 수 있도록 하는 UI 컴포넌트 라이브러리
Q) MDC의 Material Theming은 앱 전반에 걸쳐 디자인 일관성을 유지하는 데 어떻게 도움이 되는가?
Material Theming의 디자인 일관성:
1. 컬러 시스템:
<!-- themes.xml -->
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/primary</item>
<item name="colorOnPrimary">@color/on_primary</item>
<item name="colorSecondary">@color/secondary</item>
<item name="colorSurface">@color/surface</item>
</style>
2. 타이포그래피 시스템:
<style name="TextAppearance.MyApp.Headline1" parent="TextAppearance.Material3.HeadlineLarge">
<item name="fontFamily">@font/custom_font</item>
<item name="android:textSize">32sp</item>
</style>
3. Shape 시스템:
<style name="ShapeAppearance.MyApp.MediumComponent" parent="ShapeAppearance.Material3.Corner.Medium">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
일관성 보장 방법:
모든 컴포넌트가 동일한 테마 속성 참조
중앙집중식 디자인 토큰 관리
다크/라이트 모드 자동 지원
Q) 51. ViewBinding 사용의 장점은 무엇인가?
ViewBinding은 레이아웃 파일의 View들을 type-safe하게 참조할 수 있게 해주는 기능
Q) ViewBinding은 findViewById()에 비해 타입 안전성과 null 안전성을 어떻게 향상시키며, 이러한 접근 방식의 장점은 무엇인가?
타입 안전성과 null 안전성 향상:
findViewById()의 문제점:
// 런타임 에러 가능성
1. 타입 안전성 문제 - 런타임 ClassCastException 가능
val textView = findViewById<Button>(R.id.text_view) // 잘못된 타입 캐스팅 가능
textView.setOnClickListener { } // 런타임에 ClassCastException 발생
2. null 안전성 문제 - 존재하지 않는 ID 참조
val button = findViewById<Button>(R.id.non_existent_view) // null 반환 가능
button.setOnClickListener { } // NullPointerException 발생
ViewBinding의 해결책:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 컴파일 타임에 타입과 존재 여부 검증
binding.textView.text = "Hello" // TextView 타입 보장
binding.button.setOnClickListener { } // Button 타입 보장
}
}
장점:
컴파일 타임 안전성: 존재하지 않는 View 참조 시 컴파일 에러
타입 안전성: 자동으로 올바른 타입 반환
성능: findViewById() 호출 제거로 성능 향상
null 안전성: include된 레이아웃이 없어도 안전하게 처리
Q) 52. DataBinding은 어떻게 작동하는가?
DataBinding은 레이아웃 파일에서 직접 데이터와 UI를 바인딩할 수 있게 해주는 라이브러리
DataBinding의 작동 원리:
// 레이아웃 파일 (activity_main.xml)
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.User" />
</data>
<LinearLayout>
<TextView
android:text="@{user.name}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
컴파일러가 자동 생성하는 코드:
// ActivityMainBinding.java (자동 생성)
public class ActivityMainBinding extends ViewDataBinding {
public final TextView textView;
private User mUser;
// 바인딩 표현식을 실제 코드로 변환
private void executeBindings() {
if (mUser != null) {
textView.setText(mUser.getName());
}
}
}
// 레이아웃의 바인딩 표현식
android:text="@{user.name + ` ` + user.age}"
android:visibility="@{user.isVisible ? View.VISIBLE : View.GONE}"
// 컴파일러가 생성하는 실제 코드
private void executeBindings() {
String textValue = user.getName() + " " + user.getAge();
textView.setText(textValue);
int visibilityValue = user.isVisible() ? View.VISIBLE : View.GONE;
view.setVisibility(visibilityValue);
}
class User : BaseObservable() {
@get:Bindable
var name: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.name) // 자동 생성된 BR 클래스 사용
}
}
// 컴파일러가 생성하는 리스너 등록 코드
binding.addOnPropertyChangedCallback(new OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
if (propertyId == BR.name) {
executeBindings(); // UI 자동 업데이트
}
}
});
// 개발자 코드
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this, R.layout.activity_main
)
binding.user = User("John", 25)
// 내부적으로 실행되는 과정
// 1. 바인딩 객체 생성
// 2. View 참조 설정
// 3. 데이터 변경 리스너 등록
// 4. 초기 바인딩 실행
// 5. Observable 변경 시 자동 재바인딩
// 레이아웃
<EditText android:text="@={user.name}" />
// 컴파일러 생성 코드
editText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
user.setName(s.toString()); // 역방향 바인딩
}
});
핵심 작동 메커니즘:
컴파일 타임: 바인딩 표현식을 Java/Kotlin 코드로 변환
런타임: 생성된 바인딩 클래스가 데이터와 UI를 연결
관찰: Observable 패턴으로 데이터 변경 감지 및 UI 자동 업데이트
최적화: 변경된 필드만 선택적으로 업데이트
Q1) DataBinding과 ViewBinding의 주요 차이점은 무엇이며, 어떤 시나리오에서 둘 중 하나를 선택하겠는가?
ViewBinding:
View 참조만 제공
컴파일 타임 안전성
오버헤드 최소
단순한 View 접근에 적합
DataBinding:
양방향 데이터 바인딩
레이아웃에서 표현식 사용 가능
Observable 데이터 지원
더 많은 기능, 더 큰 오버헤드
선택 기준:
// ViewBinding 적합: 단순한 View 조작
binding.textView.text = viewModel.userName
binding.button.setOnClickListener { viewModel.onButtonClick() }
// DataBinding 적합: 복잡한 데이터 바인딩
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
<variable name="viewModel" type="com.example.UserViewModel"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:text="@{user.name}"
android:visibility="@{user.isVisible ? View.VISIBLE : View.GONE}"/>
<Button android:onClick="@{() -> viewModel.onButtonClick()}"
android:enabled="@{user.isValid}"/>
</LinearLayout>
</layout>
Q2) DataBinding은 MVVM 아키텍처에서 어떤 역할을 하며, Android 개발에서 UI 로직과 비즈니스 로직을 분리하는 데 어떻게 도움이 되는가?
DataBinding은 MVVM 아키텍처에서 View와 ViewModel을 연결하는 다리 역할을 수행
// ViewModel
class UserViewModel : ViewModel() {
val userName = MutableLiveData<String>()
val isLoading = MutableLiveData<Boolean>()
fun updateUser() {
isLoading.value = true
// 비즈니스 로직 수행
}
}
// Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = userViewModel
binding.lifecycleOwner = this // LiveData 관찰을 위해 필요
}
}
UI와 비즈니스 로직 분리:
View는 데이터 표시에만 집중
ViewModel은 비즈니스 로직에만 집중
DataBinding이 둘 사이의 동기화 자동 처리
Q) 53. LiveData란 무엇인가?
LiveData는 생명주기를 인식하는 Observable 데이터 홀더 클래스
Q1) LiveData는 어떻게 생명주기 인식을 보장하며, RxJava나 EventBus와 같은 전통적인 옵저버블에 비해 어떤 장점을 제공하는가?
class UserRepository {
private val _userData = MutableLiveData<User>()
val userData: LiveData<User> = _userData
fun fetchUser() {
// 데이터 로드
_userData.value = loadedUser
}
}
// Activity/Fragment에서 관찰
viewModel.userData.observe(this) { user ->
// 활성 상태(STARTED 또는 RESUMED)일 때만 호출됨
updateUI(user)
}
LiveData의 생명주기 인식 보장 메커니즘:
// LiveData.observe() 내부 구현
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
return; // 이미 파괴된 컴포넌트는 관찰하지 않음
}
// LifecycleBoundObserver 래퍼 생성
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
mObservers.putIfAbsent(observer, wrapper);
}
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
@NonNull final LifecycleOwner mOwner;
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver); // 자동으로 옵저버 제거
return;
}
// 생명주기 상태 변경에 따른 활성/비활성 상태 결정
previouslyActive = shouldBeActive();
activeStateChanged(shouldBeActive());
}
@Override
boolean shouldBeActive() {
// STARTED 또는 RESUMED 상태일 때만 활성화
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
}
// LiveData 내부 setValue/postValue 처리
private void dispatchingValue(@Nullable ObserverWrapper initiator) {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
ObserverWrapper observer = iterator.next().getValue();
if (!observer.shouldBeActive()) {
continue; // 비활성 상태면 콜백 호출하지 않음
}
observer.mObserver.onChanged((T) mData); // 활성 상태일 때만 콜백 실행
}
}
전통적 옵저버블과의 차이점:
RxJava 문제점:
// 메모리 누수 위험
disposable = observable
.subscribe { data ->
updateUI(data) // Activity가 destroy되어도 호출될 수 있음
}
// 수동으로 해제 필요
override fun onDestroy() {
super.onDestroy()
disposable.dispose()
}
EventBus 문제점:
// EventBus - 생명주기 무관하게 이벤트 수신
@Subscribe
fun onUserEvent(user: User) {
updateUI(user) // Fragment가 백스택에 있거나 paused 상태여도 호출됨
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this) // 수동 등록
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this) // 수동 해제 (깜빡하면 메모리 누수)
}
// 문제점:
// 1. 생명주기 무시 - 비활성 상태에서도 이벤트 수신
// 2. 수동 관리 - register/unregister 깜빡하면 크래시
// 3. 타입 안전성 부족 - 런타임에 이벤트 타입 매칭
LiveData 장점:
자동 생명주기 관리 - STARTED/RESUMED 상태에서만 콜백 실행
자동 메모리 해제 - DESTROYED 시 옵저버 자동 제거
UI 크래시 방지 - 비활성 상태에서 UI 업데이트 차단
개발자 실수 방지 - dispose/unregister 깜빡해도 안전
Q2) LiveData에서 setValue()와 postValue()의 차이점은 무엇이며, 각각을 언제 사용하는가?
// setValue() - 메인 스레드 전용
@MainThread
protected void setValue(T value) {
assertMainThread("setValue"); // 메인 스레드 아니면 에러
dispatchingValue(null); // 즉시 실행
}
// postValue() - 모든 스레드에서 호출 가능
protected void postValue(T value) {
synchronized (mDataLock) {
mPendingData = value; // 값만 저장
}
postToMainThread(mPostValueRunnable); // 메인 스레드로 위임
}
// setValue() - 즉시 동기 실행
_data.setValue("값1") // 즉시 콜백
_data.setValue("값2") // 즉시 콜백
_data.setValue("값3") // 즉시 콜백
// 결과: 3번 콜백
// postValue() - 비동기 실행, 값 병합
_data.postValue("값1")
_data.postValue("값2")
_data.postValue("값3")
// 결과: "값3"만 1번 콜백
setValue() 사용:
UI 이벤트 처리 (메인 스레드)
모든 상태 변화를 순차적으로 전달하고 싶을 때
postValue() 사용:
백그라운드 스레드에서 업데이트
빈번한 업데이트가 있을 때 (성능 최적화)
// setValue 예시
fun onButtonClick() {
_uiState.setValue(UiState.Loading) // 메인 스레드
}
// postValue 예시
viewModelScope.launch(Dispatchers.IO) {
_data.postValue(result) // 백그라운드 스레드
}
핵심: postValue()는 setValue()의 스레드 안전 래퍼로, 내부적으로 메인 스레드에서 setValue() 호출
Q3) LiveData의 한계점은 무엇이며, 구성 변경 시 재트리거되지 않도록 하면서 여러 UI 이벤트(내비게이션이나 토스트 메시지 표시 등)를 관찰해야 하는 경우를 어떻게 처리하겠는가?
한계점:
일회성 이벤트 처리 어려움 (configuration change 시 재트리거)
UI 이벤트(네비게이션, 토스트)가 중복 실행될 수 있음
해결책 - SingleLiveEvent:
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
}
Event Wrapper 패턴:
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
}
LiveData는 상태 홀더로서 마지막 값을 보관하여 재트리거하지만, Flow는 스트림으로서 구독 시점부터 새로 시작
왜 이런 차이가 있을까?
LiveData/StateFlow 설계 목적:
상태 보관: UI 상태를 안전하게 보관하고 복원
구성변경 대응: 화면 회전 등에서 데이터 손실 방지
Flow 설계 목적:
스트림 처리: 데이터의 흐름을 처리
Cold Stream: 구독할 때마다 새로 시작
Q) 54. Jetpack ViewModel이란 무엇인가?
ViewModel은 UI 관련 데이터를 생명주기를 고려하여 저장하고 관리하는 클래스
Q1) ViewModel은 구성 변경 시 데이터를 어떻게 유지하며, onSaveInstanceState()를 사용한 상태 저장과 어떻게 다른가?
ViewModel의 생존 원리:
class UserViewModel : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
init {
loadUsers() // 구성 변경 시에도 재호출되지 않음
}
override fun onCleared() {
super.onCleared()
// ViewModel이 완전히 소멸될 때만 호출
}
}
onSaveInstanceState()와의 차이:
onSaveInstanceState():
Bundle에 원시 데이터만 저장 가능
크기 제한 (약 1MB)
시스템에 의해 프로세스가 죽었을 때 복원
ViewModel:
모든 타입의 객체 저장 가능
크기 제한 없음
프로세스가 살아있는 동안만 유지
구성 변경에만 대응
Q2) ViewModelStoreOwner의 목적은 무엇이며, 동일한 Activity 내의 여러 Fragment에서 ViewModel을 어떻게 공유할 수 있는가?
ViewModelStoreOwner 목적:
ViewModel의 생명주기를 관리하는 인터페이스
Fragment 간 ViewModel 공유:
// Activity 범위의 ViewModel
class SharedViewModel : ViewModel() {
val sharedData = MutableLiveData<String>()
}
// Fragment A
class FragmentA : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
private fun updateSharedData() {
sharedViewModel.sharedData.value = "Fragment A에서 업데이트"
}
}
// Fragment B
class FragmentB : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels() // 같은 인스턴스
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sharedViewModel.sharedData.observe(viewLifecycleOwner) { data ->
// Fragment A의 변경사항을 받음
}
}
}
Q3) UI 상태 관리를 위해 ViewModel 내에서 StateFlow나 LiveData를 사용하는 것의 장점과 잠재적 단점은 무엇인가?
StateFlow 장점:
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 항상 현재 상태를 가지고 있음
// 코루틴과 완벽한 통합
// 백프레셔 지원
}
LiveData 장점:
class UserViewModel : ViewModel() {
private val _userData = MutableLiveData<User>()
val userData: LiveData<User> = _userData
// Android 생명주기와 완벽한 통합
// 자동 구독 해제
// 더 간단한 API
}
잠재적 단점:
StateFlow: 생명주기 인식 부족 (별도 처리 필요)
LiveData: 코루틴 통합 복잡, 백프레셔 미지원
백프레셔란?
데이터 생산자가 소비자보다 빠르게 데이터를 생성할 때, 소비자가 처리할 수 없는 데이터를 어떻게 처리할지에 대한 전략
Q) 55. Jetpack Navigation 라이브러리란 무엇인가?
Navigation 라이브러리는 앱 내 화면 간 이동을 관리하는 라이브러리
Q1) Jetpack Navigation 라이브러리는 백스택을 어떻게 처리하며, NavController를 사용하여 프로그래밍 방식으로 백스택을 어떻게 조작할 수 있는가?
백스택 자동 관리:
<!-- navigation_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:startDestination="@id/homeFragment">
<fragment android:id="@+id/homeFragment"
android:name="com.example.HomeFragment">
<action android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="false"/>
</fragment>
<fragment android:id="@+id/detailFragment"
android:name="com.example.DetailFragment"/>
</navigation>
프로그래밍 방식 조작:
class HomeFragment : Fragment() {
private fun navigateToDetail() {
// 일반 네비게이션
findNavController().navigate(R.id.action_home_to_detail)
// 백스택 조작
findNavController().navigate(
R.id.detailFragment,
null,
NavOptions.Builder()
.setPopUpTo(R.id.homeFragment, true) // homeFragment까지 pop, inclusive
.setLaunchSingleTop(true) // 중복 방지
.build()
)
}
private fun popBackStack() {
findNavController().popBackStack()
// 특정 destination까지 pop
findNavController().popBackStack(R.id.homeFragment, false)
}
}
Q2) Safe Args는 무엇이며, Jetpack Navigation Component에서 목적지 간에 데이터를 전달할 때 타입 안전성을 어떻게 향상시키는가?
Safe Args는 타입 안전한 데이터 전달을 위한 코드 생성 플러그인
기존 방식의 문제:
// 타입 안전하지 않음
val bundle = Bundle().apply {
putString("user_id", userId)
putInt("age", age)
}
findNavController().navigate(R.id.detailFragment, bundle)
// 받는 쪽에서 키 이름 오타 가능
val userId = arguments?.getString("user_id") // null 가능성
Safe Args 사용:
<!-- navigation_graph.xml -->
<fragment android:id="@+id/detailFragment"
android:name="com.example.DetailFragment">
<argument android:name="userId"
app:argType="string"/>
<argument android:name="age"
app:argType="integer"
app:defaultValue="0"/>
</fragment>
// 보내는 쪽 - 타입 안전
val action = HomeFragmentDirections.actionHomeToDetail(
userId = "12345",
age = 25
)
findNavController().navigate(action)
// 받는 쪽 - 타입 안전
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val userId: String = args.userId // null-safe
val age: Int = args.age
}
}
Q) 56. Dagger 2와 Hilt란 무엇인가?
Q1) Hilt는 Dagger 2에 비해 의존성 주입을 어떻게 단순화하며, Android 애플리케이션에서 Hilt를 사용하는 주요 장점은 무엇인가?
Dagger 2의 복잡성:
// Dagger 2 - 많은 보일러플레이트 코드
@Component(modules = [NetworkModule::class, DatabaseModule::class])
@Singleton
interface AppComponent {
fun inject(activity: MainActivity)
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
}
// Application에서 수동 초기화
class MyApplication : Application() {
val appComponent: AppComponent by lazy {
DaggerAppComponent.builder()
.application(this)
.build()
}
}
Hilt의 단순화:
// Hilt - 간단한 어노테이션으로 해결
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository
// 자동 주입
}
Hilt 장점:
보일러플레이트 제거: Component 정의 불필요
Android 생명주기 통합: Activity, Fragment, Service 등 자동 지원
사전 정의된 스코프: @Singleton, @ActivityScoped 등 제공
컴파일 타임 검증: 의존성 그래프 오류를 컴파일 시점에 발견
Q2) Dagger와 Hilt에서 @Provides와 @Binds의 차이점은 무엇이며, 각각을 언제 사용하겠는가?
@Provides - 인스턴스 생성:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.build()
}
}
@Binds - 인터페이스 바인딩:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
// 추상 메서드로 정의, 구현체를 인터페이스에 바인딩
}
사용 기준:
@Provides
: 복잡한 객체 생성, 외부 라이브러리 객체, 설정이 필요한 경우
@Binds
: 인터페이스 구현체 바인딩, 더 효율적 (추상 메서드)
@Provides와 @Binds의 차이점과 생성 코드 분석:
@Provides 생성 코드
// 개발자 작성 코드
@Provides
fun provideUserRepository(api: UserApi): UserRepository {
return UserRepositoryImpl(api)
}
// Dagger가 생성하는 코드
public final class NetworkModule_ProvideUserRepositoryFactory
implements Factory<UserRepository> {
private final Provider<UserApi> apiProvider;
@Override
public UserRepository get() {
return NetworkModule.INSTANCE.provideUserRepository(apiProvider.get());
// 메서드 호출 오버헤드 발생
}
}
@Binds 생성 코드
// 개발자 작성 코드
@Binds
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
// Dagger가 생성하는 코드
public final class RepositoryModule_BindUserRepositoryFactory
implements Factory<UserRepository> {
private final Provider<UserRepositoryImpl> implProvider;
@Override
public UserRepository get() {
return implProvider.get(); // 직접 반환, 메서드 호출 없음
}
}
@Provides의 오버헤드
// 런타임 시 실제 실행되는 과정
@Provides
fun provideDatabase(): Database {
return Room.databaseBuilder(...).build() // 매번 이 메서드 실행
}
// 생성된 바이트코드
// 1. Module 클래스 로드
// 2. 메서드 호출 스택 생성
// 3. provideDatabase() 메서드 실행
// 4. 반환값 처리
@Binds의 효율성
// 컴파일 타임에 결정됨
@Binds
abstract fun bindRepository(impl: RepositoryImpl): Repository
// 생성된 바이트코드
// 1. 구현체 Provider에서 직접 get() 호출
// 2. 타입 캐스팅 (필요한 경우)
// 3. 즉시 반환
@Provides - 런타임 의존성
@Module
object DatabaseModule {
@Provides
fun provideDao(database: Database): UserDao {
return database.userDao() // 런타임에 메서드 호출
}
}
// 문제점:
// - 메서드 호출 오버헤드
// - 스택 프레임 생성
// - 가비지 컬렉션 압박
@Binds - 컴파일 타임 바인딩
@Module
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}
// 장점:
// - 컴파일 타임에 바인딩 결정
// - 직접적인 타입 매핑
// - 런타임 오버헤드 최소화
@Binds 사용 조건과 이점
// ✅ @Binds 사용 가능한 경우 (권장)
interface UserRepository
class UserRepositoryImpl @Inject constructor(
private val api: UserApi
) : UserRepository
@Binds
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
// 조건:
// 1. 구현체에 @Inject 생성자 있음
// 2. 단순한 인터페이스-구현체 바인딩
// 3. 추가 로직 불필요
@Provides를 써야 하는 경우
// ❌ @Binds 사용 불가능한 경우
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.build() // 복잡한 빌더 패턴
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL) // 런타임 값 사용
.client(okHttpClient)
.build()
}
바이트코드 크기 비교
// @Provides 생성 코드 (약 15-20줄)
public final class Module_ProvideRepoFactory implements Factory<Repository> {
private final Module module;
private final Provider<Api> apiProvider;
public Module_ProvideRepoFactory(Module module, Provider<Api> apiProvider) {
this.module = module;
this.apiProvider = apiProvider;
}
@Override
public Repository get() {
return module.provideRepository(apiProvider.get());
}
public static Factory<Repository> create(Module module, Provider<Api> apiProvider) {
return new Module_ProvideRepoFactory(module, apiProvider);
}
}
//@Binds 생성 코드 (약 8-10줄)
public final class Module_BindRepoFactory implements Factory<Repository> {
private final Provider<RepositoryImpl> implProvider;
public Module_BindRepoFactory(Provider<RepositoryImpl> implProvider) {
this.implProvider = implProvider;
}
@Override
public Repository get() {
return implProvider.get(); // 직접 반환
}
}
@Provides의 메모리 오버헤드
// 메모리에 유지되는 객체들
// 1. Module 인스턴스
// 2. Factory 인스턴스
// 3. Provider 체인
// 4. 메서드 참조 정보
@Binds의 메모리 효율성
// 메모리에 유지되는 객체들
// 1. Factory 인스턴스 (더 작음)
// 2. Provider 체인 (단순함)
// 3. 직접 바인딩 정보
결론
@Binds 사용이 유리한 이유:
성능: 메서드 호출 오버헤드 제거
메모리: 더 적은 바이트코드와 런타임 객체
컴파일 타임: 더 나은 최적화 가능
가독성: 인터페이스-구현체 관계가 명확
핵심 원칙: 단순한 인터페이스 바인딩이 가능한 상황에서는 항상 @Binds를 사용하고, 복잡한 객체 생성이나 설정이 필요한 경우에만 @Provides를 사용
Q3) Hilt에서 @Singleton, @ActivityScoped, @ViewModelScoped를 사용한 스코핑이 어떻게 작동하는지 설명하고, 이러한 스코프들이 애플리케이션 내에서 의존성 생명주기에 어떤 영향을 미치는지 설명하라.
// Singleton - 앱 전체에서 하나의 인스턴스
@Singleton
class AppDatabase @Inject constructor() { }
// ActivityScoped - Activity 생명주기와 연결
@ActivityScoped
class ActivityScopedRepository @Inject constructor() { }
// ViewModelScoped - ViewModel 생명주기와 연결
@ViewModelScoped
class ViewModelScopedUseCase @Inject constructor() { }
스코프별 생명주기:
@Singleton
: Application 생성 ~ Application 소멸
@ActivityScoped
: Activity 생성 ~ Activity 소멸 (구성 변경 시 재생성)
@ViewModelScoped
: ViewModel 생성 ~ ViewModel 소멸 (구성 변경 시 유지)
@FragmentScoped
: Fragment 생성 ~ Fragment 소멸
의존성 생명주기 영향:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var singleton: SingletonRepository // 앱 전체 공유
@Inject lateinit var activityScoped: ActivityRepository // 이 Activity에서만 유지
}
Q) 57. Jetpack Paging 라이브러리란 무엇인가?
Paging 라이브러리는 대용량 데이터셋을 효율적으로 로드하고 표시하는 라이브러리
Q) Paging 라이브러리는 데이터 로딩 중 발생하는 오류를 어떻게 처리하며, 페이징된 데이터 흐름에서 오류 처리 및 재시도 메커니즘을 구현하기 위한 권장 전략은 무엇인가?
오류 처리와 재시도 메커니즘:
// PagingSource에서 오류 처리
class UserPagingSource(
private val apiService: ApiService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val page = params.key ?: 1
val response = apiService.getUsers(page, params.loadSize)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.users.isEmpty()) null else page + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
UI에서 오류 처리:
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
// LoadState를 통한 오류 상태 처리
}
// Fragment에서 LoadState 관찰
lifecycleScope.launch {
adapter.loadStateFlow.collect { loadState ->
when (val refreshState = loadState.refresh) {
is LoadState.Loading -> {
showLoading()
}
is LoadState.Error -> {
showError(refreshState.error)
}
is LoadState.NotLoading -> {
hideLoading()
}
}
}
}
재시도 메커니즘:
// Adapter에서 재시도
class LoadStateAdapter(
private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState, retry)
}
}
// ViewHolder에서 재시도 버튼 처리
class LoadStateViewHolder(
private val binding: LoadStateItemBinding,
private val retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(loadState: LoadState, retry: () -> Unit) {
when (loadState) {
is LoadState.Error -> {
binding.errorMsg.text = loadState.error.localizedMessage
binding.retryButton.setOnClickListener { retry() }
}
}
}
}
// 전체 데이터 재시도
adapter.retry()
// 새로고침
adapter.refresh()
권장 전략:
네트워크 오류: 자동 재시도 + 사용자 수동 재시도 옵션
서버 오류: 에러 메시지 표시 + 재시도 버튼
오프라인: 캐시된 데이터 표시 + 연결 상태 모니터링
Q) 58. Baseline Profile이란 무엇인가?
Baseline Profile은 앱의 중요한 코드 경로를 미리 컴파일하여 성능을 향상시키는 기술
Q) Android Runtime (ART)은 앱 성능 향상을 위해 Baseline Profiles를 어떻게 활용하며, 이 접근 방식이 전통적인 Just-In-Time (JIT) 컴파일에 비해 갖는 주요 장점은 무엇인가?
ART의 활용 방식:
전통적인 JIT 컴파일 문제:
앱 실행 중에 코드를 해석하거나 컴파일
첫 실행 시 성능 저하 (cold start)
자주 사용되는 코드도 매번 컴파일
Baseline Profile의 해결책:
// 프로파일 생성을 위한 테스트
@Test
fun generateBaselineProfile() {
// 중요한 사용자 여정 시뮬레이션
device.pressHome()
val intent = Intent("com.example.myapp.MAIN")
device.context.startActivity(intent)
// 핵심 기능 실행
device.findObject(UiSelector().text("중요한 기능")).click()
device.waitForIdle()
}
// build.gradle
android {
buildTypes {
release {
// Baseline Profile 포함
profileable true
}
}
}
dependencies {
implementation "androidx.profileinstaller:profileinstaller:1.3.0"
}
AOT (Ahead-Of-Time) 컴파일 적용:
ART는 Baseline Profile 정보를 사용하여 설치 시점에 중요한 코드를 미리 네이티브 코드로 컴파일
JIT(런타임 컴파일) 대신 AOT(사전 컴파일) 로 핵심 경로 최적화
프로파일에 명시된 메서드들은 앱 실행 전에 이미 최적화된 상태
JIT 대비 주요 장점:
1. 빠른 앱 시작:
중요한 코드가 이미 컴파일된 상태
Cold start 시간 최대 30% 단축
첫 실행부터 최적화된 성능
사용자 경험 일관성 향상
런타임 컴파일 오버헤드 감소
CPU 사용량 감소
컴파일된 코드는 여러 프로세스 간 공유 가능
JIT 컴파일러 메모리 사용량 감소
실제 적용 과정:
이를 통해 사용자는 첫 실행부터 빠른 앱 성능을 경험할 수 있으며, JIT 컴파일의 "워밍업" 시간 없이 즉시 최적화된 성능을 제공받을 수 있음
Q) 53 에서 언급
ViewModel 구성변경 시 인스턴스 유지 메커니즘 정리:
핵심 질문)
Activity가 구성변경으로 파괴될 때와 finish()로 파괴될 때를 ViewModel은 어떻게 구분하는가?
전체 흐름도
ActivityThread → Activity → ComponentActivity → ViewModelStore → ViewModel
↓ ↓ ↓ ↓ ↓
mChangingConfig → isChangingConfigurations() → clear() 호출 여부 결정 → onCleared()
// 1. ViewModel.clear() (final 함수)
final void clear() {
onCleared(); // 사용자가 오버라이드할 수 있는 함수
}
// 2. ViewModelStore.clear()
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear(); // 모든 ViewModel의 clear() 호출
}
mMap.clear();
}
// 3. ComponentActivity에서 호출
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(...) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) { // 핵심 조건!
getViewModelStore().clear();
}
}
}
});
// Activity.java
public boolean isChangingConfigurations() {
return mChangingConfigurations; // 멤버변수 반환
}
// ActivityThread.java - 구성변경 시 호출
public void activityLocalRelaunch(ActivityClientRecord r) {
r.activity.mChangingConfigurations = true; // 여기서 true로 설정!
}
조건 분기
// 구성변경으로 인한 파괴
if (isChangingConfigurations()) {
// ViewModelStore.clear() 호출 안함 → ViewModel 유지
}
// 일반적인 파괴 (finish() 등)
if (!isChangingConfigurations()) {
getViewModelStore().clear(); // ViewModel.onCleared() 호출
}
// ComponentActivity.java
public Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();
ViewModelStore viewModelStore = mViewModelStore;
return new NonConfigurationInstance(custom, viewModelStore); // ViewModelStore 보존
}
// Activity 재생성 시
ViewModelStore viewModelStore = null;
NonConfigurationInstance nc = (NonConfigurationInstance) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore; // 이전 ViewModelStore 복원
}
if (viewModelStore == null) {
viewModelStore = new ViewModelStore(); // 새로 생성
}
mViewModelStore = viewModelStore;
// ActivityThread - 시스템에서 구성변경 감지
public void activityLocalRelaunch(ActivityClientRecord r) {
// 1. 구성변경 플래그 설정
r.activity.mChangingConfigurations = true;
// 2. NonConfigurationInstance 보존
r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
// 3. Activity 재생성 시 전달
activity.attach(..., r.lastNonConfigurationInstances, ...);
}
ViewModelStore의 역할:
ViewModel들을 HashMap으로 관리
구성변경 시에도 인스턴스 보존
완전한 파괴 시에만 clear() 호출
ComponentActivity의 책임:
ViewModelStoreOwner 인터페이스 구현
구성변경과 일반 파괴 구분
NonConfigurationInstance를 통한 상태 보존
결론)
ViewModel이 구성변경을 감지하는 방법:
이를 통해 ViewModel은 구성변경 시에도 데이터를 유지하면서, 실제 앱 종료 시에만 정리 작업을 수행할 수 있습니다.
Jetpack Navigation 2 (기존)
// NavController를 통한 간접적 백스택 조작
findNavController().navigate(R.id.action_home_to_detail)
findNavController().popBackStack(R.id.homeFragment, false)
// 백스택 상태는 간접적으로만 관찰 가능
navController.currentBackStackEntry // 현재 엔트리만 접근
Navigation 3 (새로운 방식)
// 직접적인 백스택 조작 - MutableList 사용
val backStack = remember { mutableStateListOf<Any>(Home) }
// 네비게이션: 백스택에 직접 추가
backStack.add(Product("123"))
// 뒤로가기: 백스택에서 직접 제거
backStack.removeLastOrNull()
// 백스택 상태를 직접 관찰 가능
backStack.size // 백스택 크기
backStack.last() // 현재 화면
Navigation 2 - XML 기반 + 간접 조작
<!-- navigation_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android">
<fragment android:id="@+id/homeFragment">
<action android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:popUpTo="@id/homeFragment"/>
</fragment>
</navigation>
// NavHost 사용 - 단일 화면만 표시
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("detail") { DetailScreen() }
}
Navigation 3 - 완전 선언적 + 직접 조작
// 라우트를 데이터 클래스로 정의
data object Home
data class Product(val id: String)
// NavDisplay 사용 - 다중 화면 지원 가능
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = { route ->
when (route) {
is Home -> NavEntry(route) { HomeScreen() }
is Product -> NavEntry(route) { ProductScreen(route.id) }
}
}
)
Navigation 2 - Safe Args 플러그인 필요
// Safe Args 코드 생성 필요
val action = HomeFragmentDirections.actionHomeToDetail(
userId = "12345",
age = 25
)
findNavController().navigate(action)
// 받는 쪽
private val args: DetailFragmentArgs by navArgs()
val userId: String = args.userId
Navigation 3 - 네이티브 타입 안전성
// 데이터 클래스 자체가 타입 안전
data class Product(val id: String, val category: String)
// 네비게이션 시 컴파일 타임 타입 체크
backStack.add(Product(id = "123", category = "electronics"))
// 받는 쪽에서도 타입 안전
entryProvider = { route ->
when (route) {
is Product -> NavEntry(route) {
ProductScreen(
id = route.id, // String 타입 보장
category = route.category // String 타입 보장
)
}
}
}
Navigation 2의 한계
// NavHost는 단일 대상만 표시
NavHost(navController, "home") {
composable("home") { HomeScreen() }
composable("detail") { DetailScreen() } // 하나씩만 표시 가능
}
// 태블릿에서 마스터-디테일 레이아웃 구현 복잡
Navigation 3의 유연성
// 다중 화면 동시 표시 가능
if (isTablet) {
Row {
// 마스터 화면
NavDisplay(
backStack = masterBackStack,
entryProvider = { route -> /* 목록 화면 */ }
)
// 디테일 화면
NavDisplay(
backStack = detailBackStack,
entryProvider = { route -> /* 상세 화면 */ }
)
}
} else {
// 폰에서는 단일 화면
NavDisplay(backStack = singleBackStack, ...)
}
Navigation 2 - 두 개의 진실 공급원(two sources of truth)
// NavController 상태와 UI 상태가 분리될 수 있음
var uiState by remember { mutableStateOf(UiState.Loading) }
val navController = rememberNavController()
// 불일치 가능성
navController.navigate("detail") // NavController 상태 변경
uiState = UiState.Detail // UI 상태는 별도 관리
Navigation 3 - 단일 진실 공급원(signle source of truth)
// 백스택 자체가 UI 상태
val backStack = remember { mutableStateListOf<Any>(Home) }
// 백스택 변경 = UI 상태 변경
backStack.add(Detail) // 상태와 네비게이션이 일치
Navigation 2
XML 네비게이션 그래프 학습 필요
Safe Args 플러그인 설정
Fragment/Activity 생명주기 이해
NavController API 숙지
Navigation 3
Compose 상태 관리 이해면 충분
기존 Kotlin 지식 활용 가능
더 직관적인 API
하지만 현재 알파 버전으로 API 변경 가능성
결론
Navigation 3의 주요 개선점:
직접적인 백스택 조작 - 더 예측 가능하고 제어 가능
네이티브 타입 안전성 - 별도 플러그인 불필요
적응형 레이아웃 지원 - 다중 화면 동시 표시
단일 진실 공급원 - 상태 불일치 해결
Compose 친화적 - 선언적 패러다임 완전 수용
현재 상황: Navigation 3는 아직 알파 단계(Compose Mulitplatform 미지원)이므로, 프로덕션에서는 Navigation 2를 계속 사용하되, Navigation 3 존버
레퍼런스)
LiveData 관련:
[안드로이드 공식문서 파헤치기] LiveData의 모든 것! - 1편(Observer Pattern, MutableLiveData)
[안드로이드 공식문서 파헤치기] LiveData의 모든 것! - 2편(lifecycle AAC)
https://github.com/woowacourse/android-shopping-cart/pull/121
ViewModel 관련:
ViewModel이 구성변경에도 인스턴스를 유지하는 이유
Navigation3 관련:
Announcing Jetpack Navigation 3
Everything you need to know about NEW Navigation 3
Jetpack Navigation 3 is Here! 🚀 Pass Data & Custom Objects in Compose Like a Pro