View 생명주기
View 생명주기
모든 안드로이드 View는 생성부터 소멸까지 일련의 단계를 거칩니다. View가 윈도우에 연결되고, 측정(measure)되고, 배치(layout)되고, 그려진(draw) 뒤 최종적으로 분리(detach)되는 과정에서 시스템은 각 시점에 맞는 특정 메서드를 호출합니다. 이러한 단계를 정확히 이해하는 것은 커스텀 뷰를 구현하거나, View의 가시성에 연결된 리소스를 관리하거나, 측정 및 그리기 관련 버그를 방지하는 데 필수적입니다. 면접에서도 View 생명주기와 렌더링 파이프라인은 단골 주제이므로, 단순히 콜백 이름만 아는 수준을 넘어서 내부 동작 원리까지 숙지하고 계시면 좋은 인상을 남기실 수 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
View의 인스턴스 생성부터 분리(detach)까지의 전체 생명주기를 추적할 수 있습니다.- 측정(measure), 배치(layout), 그리기(draw) 파이프라인의 흐름과 각 메서드의 호출 시점을 설명할 수 있습니다.
onAttachedToWindow()와onDetachedFromWindow()가 리소스 관리에서 수행하는 역할을 이해할 수 있습니다.invalidate()와requestLayout()이 파이프라인의 어떤 단계를 트리거하는지 식별할 수 있습니다.- 커스텀 뷰에서 생명주기를 올바르게 활용하여 메모리 누수와 불필요한 작업을 방지하는 방법을 적용할 수 있습니다.
윈도우 연결 (Attachment to the Window)
View는 프로그래밍 방식으로 직접 생성하거나, XML을 파싱하는 레이아웃 인플레이터(layout inflater)를 통해 인스턴스화됩니다. 이 시점에서 View는 Kotlin/Java 객체로서만 존재하며, 아직 어떤 윈도우나 가시적 계층 구조에도 속해 있지 않습니다. 측정이나 그리기도 수행되지 않은 상태입니다.
커스텀 뷰는 일반적으로 여러 생성자를 구현합니다. 프로그래밍 방식 생성을 위한 단일 인자 생성자(Context), XML 인플레이션을 위한 두 인자 생성자(Context, AttributeSet), 그리고 테마 커스터마이징을 위한 세 인자 생성자(Context, AttributeSet, defStyleAttr)가 있습니다. XML 인플레이터는 두 인자 형태를 사용하며, 선언된 속성을 전달하여 파싱합니다. 실무에서 커스텀 뷰를 작성할 때 이 세 가지 생성자를 빠짐없이 구현하지 않으면 XML에서 사용할 때 런타임 크래시가 발생하는 경우가 있으니 유의하셔야 합니다.
View가 윈도우에 연결된 ViewGroup에 추가되면 시스템이 onAttachedToWindow()를 호출합니다. 이 시점이 View가 디스플레이 서피스(display surface)에 처음으로 접근할 수 있고, 시스템 콜백을 등록할 수 있는 최초의 지점입니다. 애니메이션 시작, 리스너 등록, View가 실제 계층 구조에 포함되어야만 획득할 수 있는 리소스 확보 등의 작업을 이곳에서 수행하는 것이 올바른 패턴입니다.
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// 센서 리스너 등록 (View가 윈도우에 연결된 시점)
sensorManager.registerListener(
this, accelerometer, SensorManager.SENSOR_DELAY_UI
)
}
하나의 View는 호스팅하는 Activity나 Fragment의 수명 동안 여러 번 연결(attach)과 분리(detach)를 반복할 수 있습니다. 부모에 View를 추가하면 onAttachedToWindow()가 호출되고, 제거하면 onDetachedFromWindow()가 호출됩니다. 가령 RecyclerView는 아이템 뷰가 뷰포트 밖으로 스크롤될 때 분리하고, 다시 뷰포트 안으로 스크롤되면 재연결합니다. 이처럼 attach/detach는 일회성 이벤트가 아니라 반복적으로 발생할 수 있다는 점을 명심하셔야 합니다.
측정과 배치 (Measure and Layout)
윈도우 연결 이후, 시스템은 측정(measure) 패스와 배치(layout) 패스를 실행합니다. 부모 ViewGroup이 각 자식에 대해 measure()를 호출하면, 이 메서드는 내부적으로 onMeasure()에 위임합니다. 자식 View는 부모가 제공하는 MeasureSpec 제약 조건을 기반으로 원하는 너비와 높이를 계산하고, setMeasuredDimension()을 호출하여 결과를 보고합니다.
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = calculateContentWidth()
val desiredHeight = calculateContentHeight()
// MeasureSpec 제약 조건에 따라 최종 크기 결정
val width = resolveSize(desiredWidth, widthMeasureSpec)
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
}