안드로이드에서 커스텀 뷰를 만들다 보면 invalidate()와 postInvalidate()를 자주 사용하게 된다. 하지만 이 메서드들이 정확히 어떻게 동작하는지, 언제 어떤 것을 사용해야 하는지 명확히 이해하지 못한 채 사용하는 경우가 많다. 이 글에서는 안드로이드 View 시스템의 무효화 메커니즘을 깊이 있게 살펴보고자 한다.
무효화(Invalidation)는 "이 뷰가 변경되었으니 다시 그려야 한다"고 안드로이드 시스템에 알리는 메커니즘이다. 안드로이드는 성능 최적화를 위해 뷰를 매 프레임마다 다시 그리지 않는다. 한번 그려진 뷰는 화면에 그대로 유지되며, 변경사항이 생겼을 때만 다시 그린다.
예를 들어 프로그레스 바의 진행률이 50%에서 60%로 변경되었다면, 이 변경사항을 화면에 반영하기 위해 무효화가 필요하다.
class CustomProgressView : View {
private var progress = 0f
fun setProgress(value: Float) {
progress = value
invalidate() // "다시 그려주세요!" 요청
}
override fun onDraw(canvas: Canvas) {
canvas.drawArc(rect, -90f, progress * 3.6f, false, paint)
}
}
커스텀 뷰의 색상이 바뀌었을 때, 사용자의 터치로 뷰의 상태가 변경되었을 때, 애니메이션으로 뷰의 위치가 움직일 때 모두 무효화가 필요하다.
안드로이드는 다양한 무효화 메서드를 제공한다. 가장 기본적인 invalidate()는 뷰 전체를 다시 그리도록 요청한다. 특정 영역만 변경되었다면 invalidate(left, top, right, bottom)을 사용하여 해당 영역만 무효화할 수 있다. 이는 큰 뷰에서 작은 부분만 변경되었을 때 성능 최적화에 유용하다.
class GameView : View {
private val player = Rect(100, 100, 150, 150)
fun movePlayer(dx: Int, dy: Int) {
invalidate(player) // 이전 위치 무효화
player.offset(dx, dy)
invalidate(player) // 새 위치 무효화
}
}
백그라운드 스레드에서 무효화가 필요하다면 postInvalidate()를 사용해야 한다. 이 메서드는 내부적으로 메인 스레드로 메시지를 전달하여 안전하게 무효화를 처리한다.
class RealtimeChartView : View {
fun updateDataFromNetwork() {
thread {
val newData = fetchData()
dataPoints.add(newData)
postInvalidate() // 백그라운드에서 안전하게 호출
}
}
}
postInvalidateDelayed()는 지정된 시간 후에 무효화를 예약하며, 간단한 깜빡임 효과나 지연된 업데이트가 필요할 때 사용할 수 있다. 애니메이션을 구현할 때는 postInvalidateOnAnimation()이 유용하다. 이 메서드는 다음 애니메이션 프레임에 무효화를 예약하여 부드러운 애니메이션을 구현하는 데 최적화되어 있다.
뷰의 크기나 위치가 변경되었을 때는 invalidate()가 아닌 requestLayout()을 호출해야 한다.
// 외관만 변경 - invalidate() 사용
fun changeColor(color: Int) {
paint.color = color
invalidate()
}
// 크기 변경 - requestLayout() 필요
fun changeSize(width: Int, height: Int) {
layoutParams.width = width
layoutParams.height = height
requestLayout()
}
requestLayout()은 measure, layout, draw의 전체 과정을 다시 실행하여 뷰 계층 구조를 재계산한다.
invalidate()는 생각보다 단순한 메서드다. 이 메서드를 호출하면 즉시 화면이 다시 그려지는 것이 아니라, 단지 뷰에 "더티 플래그"를 설정할 뿐이다. 실제 그리기 작업은 안드로이드의 렌더링 파이프라인에 의해 다음 프레임에 처리된다.
내부 동작 과정을 살펴보면 다음과 같다. 먼저 뷰가 이미 무효화되었는지 확인한다. 아직 무효화되지 않았다면 더티 플래그를 설정하고, 부모 뷰에게 자식이 무효화되었음을 알린다. 이 무효화 요청은 뷰 트리를 따라 부모에서 부모로 전파되며, 최상위 ViewRootImpl에 도달한다. ViewRootImpl은 다음 VSYNC 신호가 올 때 그리기 작업을 예약하고, 다음 프레임에서 실제로 onDraw()가 호출된다.
fun setProgress(value: Float) {
Log.d("TAG", "setProgress 호출")
progress = value
invalidate() // 더티 플래그만 설정
Log.d("TAG", "invalidate 완료 (아직 그려지지 않음)")
}
override fun onDraw(canvas: Canvas) {
Log.d("TAG", "onDraw 호출됨 (다음 프레임)")
canvas.drawArc(rect, -90f, progress * 3.6f, false, paint)
}
// 출력:
// setProgress 호출
// invalidate 완료 (아직 그려지지 않음)
// (약 16ms 후)
// onDraw 호출됨 (다음 프레임)
이러한 구조 덕분에 같은 프레임 내에서 invalidate()를 여러 번 호출해도 실제로는 한 번만 그려진다. 안드로이드가 중복된 무효화 요청을 자동으로 병합하여 성능을 최적화하는 것이다. 예를 들어 어떤 함수에서 invalidate()를 세 번 연속 호출해도, 뷰는 이미 더티 상태이므로 두 번째와 세 번째 호출은 무시되고, 다음 프레임에 onDraw()가 단 한 번만 호출된다.
postInvalidate()는 invalidate()와 근본적으로 다른 방식으로 동작한다. 이 메서드는 Handler를 통해 메인 스레드에 무효화 메시지를 전달하는 방식이다.
백그라운드 스레드에서 postInvalidate()를 호출하면, 내부적으로 메인 스레드의 Handler에 메시지를 큐에 추가한다. 메인 스레드가 이 메시지를 처리할 때 invalidate()를 호출하며, 이후는 일반적인 invalidate()와 동일한 과정을 거친다.
이러한 차이는 스레드 안전성에서 중요한 의미를 갖는다. invalidate()는 반드시 메인 스레드에서만 호출해야 하며, 백그라운드 스레드에서 호출하면 CalledFromWrongThreadException이 발생한다. 반면 postInvalidate()는 어떤 스레드에서든 안전하게 호출할 수 있다.
invalidate()와 postInvalidate()의 가장 큰 차이는 스레드 안전성이다. 메인 스레드에서 뷰를 업데이트할 때는 invalidate()를 사용하고, 백그라운드 스레드에서는 postInvalidate()를 사용해야 한다.
class DataView : View {
// 메인 스레드에서 호출 - OK
fun updateFromMainThread() {
invalidate()
}
// 백그라운드에서 호출 - 에러 발생!
fun updateFromBackgroundWrong() {
thread {
invalidate() // CalledFromWrongThreadException!
}
}
// 백그라운드에서 호출 - OK
fun updateFromBackgroundCorrect() {
thread {
postInvalidate() // Handler를 통해 안전하게 전달
}
}
}
성능 측면에서도 차이가 있다. invalidate()는 직접 더티 플래그를 설정하므로 오버헤드가 거의 없다. 반면 postInvalidate()는 Handler를 거쳐야 하므로 약간의 지연과 오버헤드가 발생한다. 실제 측정 결과 postInvalidate()가 약 5배 정도 느린 것으로 나타났지만, 이는 나노초 단위의 차이이므로 실제 사용에서는 체감하기 어렵다.
실행 시점도 다르다. invalidate()는 호출 즉시 더티 플래그를 설정하지만, postInvalidate()는 메시지 큐에 추가된 후 메인 스레드가 처리할 때 비로소 무효화가 이루어진다.
메인 스레드에서 작업할 때는 항상 invalidate()를 사용하는 것이 좋다. 터치 이벤트 처리, UI 이벤트 콜백, ValueAnimator의 업데이트 리스너 등은 모두 메인 스레드에서 실행되므로 invalidate()를 사용하면 된다.
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isPressed = true
invalidate() // 메인 스레드 - invalidate() 사용
}
}
return true
}
백그라운드 스레드에서 작업할 때는 postInvalidate()를 사용해야 한다. 네트워크에서 데이터를 가져와 뷰를 업데이트하거나, 센서 데이터를 실시간으로 그래프에 표시하는 경우가 여기에 해당한다.
코루틴을 사용할 때는 두 가지 방법이 있다.
// 방법 1: Dispatcher 전환
fun updateWithDispatcher() {
viewModelScope.launch(Dispatchers.IO) {
val data = fetchData()
withContext(Dispatchers.Main) {
dataPoints.add(data)
invalidate()
}
}
}
// 방법 2: postInvalidate() 사용
fun updateWithPost() {
viewModelScope.launch(Dispatchers.IO) {
val data = fetchData()
dataPoints.add(data)
postInvalidate()
}
}
전자가 코드의 의도를 더 명확하게 표현하지만, 후자도 충분히 안전하고 실용적이다.
무효화를 효율적으로 사용하기 위한 몇 가지 모범 사례가 있다.
첫째, 값이 실제로 변경되었을 때만 무효화해야 한다.
// 나쁜 예
fun setProgress(value: Float) {
progress = value
invalidate() // 값이 같아도 항상 호출
}
// 좋은 예
fun setProgress(value: Float) {
if (progress != value) {
progress = value
invalidate() // 값이 변경되었을 때만 호출
}
}
둘째, 가능하면 부분 무효화를 활용해야 한다. 뷰의 작은 부분만 변경되었다면 전체를 다시 그리는 것보다 해당 영역만 무효화하는 것이 효율적이다.
셋째, onDraw() 메서드 안에서는 객체 생성을 피해야 한다.
// 나쁜 예
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 매번 생성 - 메모리 낭비
canvas.drawCircle(50f, 50f, 20f, paint)
}
// 좋은 예
private val paint = Paint() // 한 번만 생성
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(50f, 50f, 20f, paint)
}
onDraw()는 매우 자주 호출될 수 있으므로, Paint 같은 객체는 멤버 변수로 미리 생성해두고 재사용해야 한다.
무효화를 사용할 때 절대 하면 안 되는 것이 있다. onDraw() 메서드 안에서 invalidate()를 호출하면 안 된다.
// 절대 하면 안 됨!
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(...)
invalidate() // 무한 루프!
}
이렇게 하면 무한 루프가 발생한다. onDraw()가 호출되면 invalidate()를 호출하고, 다시 onDraw()가 호출되는 식으로 계속 반복되기 때문이다.
개발자 옵션의 "GPU 렌더링 프로필 표시" 기능을 켜면 무효화가 얼마나 효율적으로 이루어지는지 실시간으로 확인할 수 있다. 녹색 막대가 기준선 아래에 있으면 60fps를 유지하는 것이고, 넘어가면 프레임 드롭이 발생하는 것이다.
Systrace를 사용하면 더 상세한 성능 분석이 가능하다. Trace.beginSection()과 Trace.endSection()으로 코드를 감싸면 해당 구간의 실행 시간을 측정할 수 있다.
invalidate()와 postInvalidate()는 단순해 보이지만, 그 내부 동작을 이해하면 더 효율적인 커스텀 뷰를 만들 수 있다. 핵심은 "꼭 필요한 만큼만 다시 그리기"다. 전체 뷰가 아닌 변경된 부분만 무효화하고, 값이 실제로 변경되었을 때만 무효화하며, 적절한 스레드에서 적절한 메서드를 사용하는 것이 중요하다.
무효화는 안드로이드 뷰 시스템의 핵심 메커니즘이다. 이를 제대로 이해하고 활용하면 부드럽고 효율적인 UI를 만들 수 있으며, 불필요한 성능 저하를 방지할 수 있다. 커스텀 뷰를 만들거나 복잡한 애니메이션을 구현할 때, 이러한 원리를 염두에 두고 코드를 작성한다면 더 나은 사용자 경험을 제공할 수 있을 것이다.