본문 바로가기
Project/Fill Sketch

실시간 드로잉 렌더링 개선 과정 및 성능 분석

by oldogz 2025. 4. 14.

Fill Sketch는 스케치 위에 색깔을 칠해 그림 그리는 앱입니다.

 

https://github.com/p-chanmin/Fill-Sketch-Module

 

GitHub - p-chanmin/Fill-Sketch-Module: 🖌 스케치 위에 색칠하는 드로잉앱

🖌 스케치 위에 색칠하는 드로잉앱. Contribute to p-chanmin/Fill-Sketch-Module development by creating an account on GitHub.

github.com

 

드래그를 통해 그림에 색을 칠하는 기능을 개발하면서 발생했던 렌더링 문제를 개선한 과정에 대해 정리하였습니다.

 

스케치 구현

Bitmap에 그림을 그리기 위해서 터치 및 드래그의 이벤트가 발생할 경우,

path를 추가하여 Bitmap을 draw 하고 UI에 표시하는 방식으로 구현하였습니다.

// DrawingViewModel.kt

private val _redoPathList = mutableStateListOf<PathWrapper>()
private val _undoPathList = mutableStateListOf<PathWrapper>()
private val pathList: SnapshotStateList<PathWrapper> = _undoPathList
data class PathWrapper(
    var points: MutableList<Offset>,
    val strokeWidth: Float,
    val strokeColor: Color,
    val actionType: ActionType,
)

 

하나의 PathWrapper에 여러 point가 존재하고, 이 point는 드래그 할 때 추가되어 사용자가 드래그한 위치에 점 혹은 선을 표현할 수 있습니다.

 

 

이러한 PathWrapper가 pathList에 여러 개 있으면서, 점과 선의 정보가 쌓이고 그림으로 그려질 수 있습니다.

 

드래그를 예시로 들면,

1. 첫 입력에서 PathWrapper 생성 및 초기 point 추가

2. 이어지는 입력에서 PathWrapper에 point 추가

3. 드래그 종료

순서로 점이나 선에 대한 데이터가 입력될 것입니다.

 

드로잉 초기 구현

사용자가 드래그를 하는 과정에서 point가 입력되고, point가 입력될 때마다 실시간으로 사용자에게 그림이 그려지고 있음을 보여주어야 했습니다.

 

pathList의 정보를 통해 currentMaskBitmap을 만들고, 스케치인 outlineBitmap과 결합시켜 최종적으로 resultBitmap을 만들어 UI에 표현할 수 있도록 계획하였습니다.

 

새로운 point가 입력되면 새롭게 currentMaskBitmap을 그려 사용자에게 보여줄 수 있도록 가장 간단한 구현 방법을 선택하였습니다.

 

하지만, 이러한 방식을 통해 구현하고 테스트하는 과정에서 문제점을 발견할 수 있었습니다.

 

point가 입력될 때마다 처음부터 currentMaskBitmap을 다시 그려야 하기 때문pathList의 PathWrapper 정보가 많아지면, 처리해야할 데이터가 많아져 렌더링까지 시간이 오래 걸리며, 프레임이 드랍되고, 앱이 느려지는 것처럼 보이는 문제가 발생했습니다.

 

 

렌더링 성능을 분석하기 위해 HWUI 렌더링 프로파일(GPU 렌더링 프로파일)을 사용하였습니다.

 

 

공식 문서를 참고하면, 녹색 가로선은 16.67ms을 나타냅니다.

초당 60 프레임을 달성하려면, 이 막대가 녹색선 아래 머물러야 합니다.

 

테스트를 위해 pathList에 미리 PathWrapper를 10,000개 추가하여 이후 그려지는 point에 대해서 테스트를 진행하였습니다.

스케치 좌상단의 빨간색 대각선의 선이 10,000개의 선을 겹쳐놓은 테스트용 데이터입니다.

 

눈으로 보기에도 실시간으로 드로잉 되는 선이 마우스를 따라지 못하는 것을 확인할 수 있었고,

프로파일을 통해서 보이는 막대그래프에서도 렌더링 성능을 확인할 수 있었습니다.

 

특히, 입력 처리 및 애니메이션, 기타 시간/VSync 지연 부분의 세그먼트 수치가 초당 60 프레임을 달성하기 위한 기준을 훨씬 넘어 가장 높게 나오는 것을 확인할 수 있었습니다.

VSync란?
VSync(Vertical Synchronization)는 화면의 수직 주사율(일반적으로 60Hz)에 맞춰 프레임 렌더링 타이밍을 조절하는 기술로, 안드로이드에서는 1초에 60번 (16.67ms마다 한 번) 화면을 갱신하려고 합니다. 이 주기를 기준으로 앱의 UI나 애니메이션, 드로잉을 맞춰야 부드럽고 안정적으로 보여줄 수 있습니다. VSync 지연은 앱이 프레임을 제시간(16.67ms 이내)에 준비하지 못해서, 다음 VSync 타이밍까지 기다리게 되는 현상을 말합니다.

 

데이터가 많아졌을 경우, 사용자의 입력인 point를 처리하고, currentMaskBitmap을 새롭게 그리는 과정에서 소요되는 시간길어지면서 렌더링이 느려지는 것으로 판단하고, 이를 개선할 방법을 모색하였습니다.

 

첫 번째 개선. 기존 상태를 유지하며 마지막 point를 그리기

기존에는 새로운 point가 입력될 경우, 처음부터 모든 path를 다시 그리면서 currentMaskBitmap을 생성하였습니다.

 

렌더링 문제를 해결하고, 더 효율적으로 처리하기 위해 기존에 그려진 currentMaskBitmap을 유지하는 아이디어를 사용하기로 하였습니다.

 

새로운 point가 입력되면, 기존에 그려진 Bitmap위에 마지막 point의 위치만 새롭게 그리는 방법으로 개선하였습니다.

 

이렇게 구현했을 경우, path 데이터가 많아져도 마지막 point만 새롭게 그리기 때문에 렌더링 성능이 개선될 것으로 예상하였습니다.

 

적용한 결과, 초기 구현보다 렌더링 성능은 눈에 띄게 개선되었지만 마지막 point만 새롭게 그려지기 때문에 매끄럽게 선을 표현할 수 없었습니다.

 

선을 매끄럽게 그리기 위해서는 point들의 정보를 바탕으로 그려야 하지만, 마지막 point의 점 한 개 만으로는 실시간으로 그려지는 선을 제대로 그릴 수 없었고, 드래그가 조금만 빨라지면 선이 끊어지는 현상이 발생하였습니다.

 

드래그가 끝나서야 모든 path를 새롭게 그리면서 제대로 그려지는 것을 확인할 수 있었습니다.

 

두 번째 개선. 기존 상태를 유지하며 마지막 선을 그리기

새롭게 발생한 문제를 해결하기 위해서 선을 매끄럽게 그릴 수 있는 방법을 모색하였습니다.

 

기존에 그려진 Bitmap위에 마지막 point를 새롭게 그리는 것이 아닌 마지막 선을 그리는 방법으로 개선해 보았습니다.

 

선의 데이터를 포함하고 있는 PathWrapper를 통해 point가 새롭게 입력될 때마다 해당 선을 기존에 그려진 Bitmap에 그리도록 구현해 보았습니다.

 

마지막 point만을 사용해서 그렸을 때보다 선이 매끄럽게 그려졌고, PathWrapper 데이터가 많아져도 렌더링 성능이 초기 구현보다 훨씬 나아진 것을 확인할 수 있었습니다.

 

이 방법을 통해 개선된 것처럼 보였지만, alpha값이 조정된 색을 그릴 경우 투명도가 제대로 반영되지 않는다는 문제가 추가적으로 발생하였습니다.

 

드래그가 끝나고 다시 Bitmap을 새롭게 그릴 때 투명도가 반영되어 그려지며, 실시간으로 드래그되는 상황에서 투명도를 제대로 표현할 수 없었습니다.

 

 

왜 실시간으로 드로잉 될 때 투명도 있는 색을 제대로 표현할 수 없는지 분석해 보았습니다.

 

이번 개선 방법은 기존에 그려진 Bitmap에 마지막 선을 갱신해 가면서 그리는 방법이기 때문에 새로운 point가 입력되면, 그려지고 있는 선을 새롭게 Bitmap에 그리게 됩니다.

 

이때, 이미 그려진 Bitmap위에 추가적으로 선을 그리게 됩니다.

 

위 사진처럼 4번에 걸쳐서 4개의 point가 입력되었을 경우

 

1번 point를 먼저 draw 하고,

2번 point가 입력되면 1~2번 point로 선을 draw,

3번 point가 입력되면 1~3번 point로 선을 draw,

4번 point가 입력되면 1~4번 point로 선을 draw 되기 때문에

 

겹쳐지는 부분의 색이 여러 번 덧칠되면서 투명도의 역할을 제대로 할 수 없다는 문제를 확인하였습니다.

 

브러쉬 크기를 키우고, 천천히 드래그 했을 때 색깔이 서서히 원래 색을 찾아가며 덧칠되는 것을 확인할 수 있었습니다.

 

실시간 드로잉의 렌더링을 최적화하면서, 투명도 있는 색까지 표현할 수 있는 새로운 방법을 모색해야 했습니다.

 

세 번째 개선. 실시간 드로잉 레이어 분리

 

아이디어는 기존에 그려진 Bitmap을 재사용하는 것에서 시작하여, 실시간으로 그려지는 선을 다른 레이어로 분리하는 방법을 생각하였습니다.

 

기존에 그려진 Bitmap인 currentMaskBitmap 위에 liveDrawingMaskBitmap 레이어를 추가하여 실시간으로 그려지고 있는 선의 레이어를 별도로 분리하여 point가 들어올 때마다 해당하는 PathWrapper 전체를 새롭게 그리도록 처리하였습니다.

 

그려지는 순서를 정리하면

  1. 실시간으로 그려지고 있는 선은 point가 입력될 때마다 새로운 liveDrawingMaskBitmap에 모든 point를 처음부터 그립니다.
  2. 기존에 그려진 currentMaskBitmap부터 liveDrawingMaskBitmap, outlineBitmap을 순서대로 겹치며 resultBitmap을 갱신합니다.
  3. 드래그가 종료되면 liveDrawingMaskBitmap를 그리는 데 사용되었던 데이터를 포함하여 currentMaskBitmap을 갱신합니다.

 

 

이를 통해 구현한 결과에서도 PathWrapper 데이터가 많아졌을 때 렌더링 성능이 초기 구현보다 훨씬 나아졌으며, 사용자는 실시간으로 그려지는 레이어가 분리되어 있는지 모르고 하나의 그림에 그려지는 것처럼 UI에 표현할 수 있었습니다. 

 

또한 투명도가 있는 색을 그렸을 때도, 자연스럽게 제대로 투명도가 반영된 색으로 실시간 드로잉을 표시할 수 있었고, 드래그 중인 경우에 중복되는 위치를 지나가도 덧그려지는 현상이 발생하지 않아 한 개의 선으로 투명도 있는 색을 섬세하게 그릴 수 있게 되었습니다.

 

개선 결과 정리 및 비교

 

초기 구현과 비교했을 때 3번에 이어진 개선을 거치면서  HWUI 렌더링 프로파일로 확인할 수 있는 입력 처리 및 애니메이션, 기타 시간/VSync 지연 부분의 세그먼트 수치가 확연히 줄어든 것버벅거림 현상, 드래그보다 늦게 그려지는 현상이 눈에 띄게 줄어든 것을 확인할 수 있었습니다.

 

또한 선이 매끄럽게 그려지고, 투명도 있는 색을 제대로 표현할 수 있으며 무엇보다 사용자가 그림을 그리는 것에 위화감이 없도록 개선할 수 있었습니다.

 

CPU 프로파일링

개선 효과를 객관적으로 입증하기 위해 추가적으로 Android Studio의 CPU 프로파일링 도구를 활용해 보았습니다.

 

측정은 Occurrences 수를 최대한 동일하게 맞추어 비교하는 방법으로 공정하게 이루어질 수 있도록 하였습니다.

Occurrences는 Thread State의 상태 변화 횟수로 앱 내에서 동작의 반복 빈도와 관련이 깊습니다.

이 횟수가 크게 다르면, 측정 구간에서 수행한 작업량이 달라져서 비교의 신뢰도가 낮아질 수 있기 때문에 개선 전과 후 모두 약 2000회 수준의 상태 변화가 일어난 구간을 기준으로 7초~30초가량의 데이터를 수집했습니다.

Thread State 설명
Running CPU가 해당 스레드를 실행 중인 상태. 즉, 코드가 실제로 실행되고 있는 시간을 의미함. 이 값이 높을수록 CPU 점유율이 높고 자원 사용이 많음.
Runnable 실행 가능한 상태이나, 아직 CPU를 배정받지 못한 상태. 많을수록 스레드가 경쟁 중이거나, 지연이 발생할 수 있음.
Sleeping 스레드가 일시적으로 대기 상태에 들어간 경우. 주로 Thread.sleep, postDelayed, 프레임 대기 등에 해당함. 이 비율이 높다는 건 CPU 리소스를 아끼고 있다는 의미.

 

 

 

측정 결과 분석

Running 시간 감소

  • 30.16s → 2.48s, 약 91.8% 감소
  • CPU가 지속적으로 바쁘게 실행 중이던 시간이 크게 줄어들었음을 확인할 수 있었습니다.

Runnable 상태 약간 증가

  • 렌더링 알고리즘 개선 → 전체 연산량 감소
  • 하지만 작업을 나누거나 스레드 경쟁 구조를 바꾼 건 아니기 때문에 → Runnable 수치에는 거의 영향이 없습니다.
  • 즉, 병목을 해결한 것이 아니라 병목이 생기지 않을 구조이므로  Runnable 상태는 개선 전후 큰 변화가 없으며, 이는 스레드 경쟁이 아닌 렌더링 방식의 비효율이 주원인이었음을 의미합니다.

Sleeping 시간 증가

  • 6.41% → 64.8%, 약 10배 증가
  • CPU가 유휴 상태로 휴식하는 비율이 크게 증가함을 확인할 수 있었습니다.
항목 개선 전 개선 후 변화율
CPU 점유율 (Running %) 93.25% 33.48% -64.1%
CPU 유휴율 (Sleeping %) 6.41% 64.8% +911%

 

 

기대 효과

  • 배터리 효율 향상: CPU 사용률이 크게 줄어 배터리 소모 감소.
  • 발열 최소화: 실시간 렌더링에서 자원 낭비가 줄어 발열 완화.
  • 성능 안정성 증가: 동시에 실행 중인 시스템 서비스나 백그라운드 앱에 영향을 줄 가능성 완화.
  • 프레임 드롭 감소 및 사용자 경험 향상: 프레임 수가 최적화되어 사용자가 느끼는 반응 속도나 부드러움이 눈에 띄게 개선되어 사용자에게 부드러운 드로잉 경험과 앱이 더 "가볍고 빠르다"는 인상 제공.

 

마무리

이번 최적화를 통해 단순히 화면에 그리는 기능만 개선한 것이 아니라, 앱 전체의 시스템 자원 사용 효율을 향상시킬 수 있었습니다.

 

사용자 경험 향상뿐 아니라 배터리 효율, 발열 감소, 시스템 성능 안정성까지 긍정적인 영향을 줄 수 있다는 점에서, 이번 개선은 매우 의미 있는 작업이었다고 생각합니다.

 

또한 다양한 프로파일링 도구를 적극 활용하면, 다양한 성능 문제도 수치로 파악하고 해결할 수 있다는 것을 느낄 수 있었습니다.

 

처음부터 최적의 방법을 생각해 내 바로 구현할 수 있으면 더욱 좋겠지만, "헤맨 만큼 내 땅이다."라는 말처럼 이번 개선 과정에서 시행착오를 겪고 원인을 파악해 가는 과정 자체가 결국 개발 역량을 넓혀주는 시간이 되었다고 생각합니다.



참고자료

https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering?hl=ko

 

GPU 렌더링 속도 및 오버드로 검사  |  App quality  |  Android Developers

앱에서 문제가 발생할 수 있는 위치를 시각화하는 데 도움이 되는 온디바이스 개발자 옵션을 알아보세요.

developer.android.com

https://brunch.co.kr/@oemilk/44

 

안드로이드 개발자 옵션 렌더링 #02

프로필 GPU 렌더링, 렌더링 비용 줄이기 | 1. 프로필 GPU 렌더링 프로필 GPU 렌더링 툴은 앱이 느려질 경우 어디서 문제가 생겼나 보여주는, 개발자 옵션에 포함된 기능입니다. 프로필 GPU 렌더링을

brunch.co.kr

https://developer.android.com/studio/profile?hl=ko

 

앱 성능 프로파일링  |  Android Studio  |  Android Developers

Android 스튜디오에서 앱을 프로파일링하는 방법을 알아봅니다.

developer.android.com