Skip to main content

테스트, 디버깅, 품질 개선

Views 10약 10분생성형 AI 활용
Summary

기능 구현 다음에 해야 할 일

앱이 화면에서 정상적으로 움직인다고 해서 곧바로 출시 가능한 것은 아닙니다. 실제 사용자는 우리가 예상하지 못한 순서로 버튼을 누르고, 네트워크가 끊긴 상태에서 앱을 열고, 오래된 기기에서 느린 화면을 경험합니다. 개발 중에는 잘 보이지 않던 문제가 출시 후에 크래시, 리뷰, 이탈로 드러납니다.

그래서 앱 개발의 후반부에는 기능을 더 넣는 일만큼 검증이 중요합니다. 디버깅은 이미 발생한 문제의 원인을 찾는 과정이고, 테스트는 같은 문제가 다시 생기지 않도록 막는 안전망입니다. 품질 개선은 크래시, 메모리, 성능, 사용성 문제를 계속 줄여 가는 작업입니다.

이 장에서는 세 가지 흐름을 함께 다룹니다.

  • 문제가 생겼을 때 Android Studio로 원인을 추적하는 방법

  • 단위 테스트와 UI 테스트로 동작을 반복 검증하는 방법

  • 크래시와 느린 화면을 재현하고 측정해 품질을 개선하는 방법

핵심은 간단합니다. 감으로 고치지 말고, 재현하고, 관찰하고, 수정하고, 다시 검증해야 합니다.

테스트, 디버깅, 품질 개선AI 생성 이미지

디버깅은 재현에서 시작한다

디버깅은 코드를 바라보며 추측하는 일이 아닙니다. 먼저 문제를 같은 방식으로 다시 만들 수 있어야 합니다.

예를 들어 학습 관리 앱에서 완료 버튼을 눌렀는데 진행률이 바뀌지 않는 문제가 있다고 해 봅시다. 이때 바로 ViewModel 코드를 고치기보다 다음 정보를 먼저 정리합니다.

  • 어떤 화면에서 발생했는가?

  • 어떤 순서로 조작했는가?

  • 항상 발생하는가, 가끔 발생하는가?

  • 앱을 재실행해도 같은가?

  • 네트워크, 로그인, 데이터 상태와 관련이 있는가?

재현 절차가 정리되면 Android Studio의 디버그 모드로 앱을 실행합니다. 디버그 모드는 앱 실행 중 특정 코드 줄에서 멈추고, 변수 값과 호출 흐름을 확인할 수 있게 해 줍니다. 이미 실행 중인 앱에 디버거를 붙이는 Attach debugger 기능도 사용할 수 있습니다1. 앱을 처음부터 다시 실행하기 어려운 상황에서 유용합니다.

브레이크포인트로 흐름을 멈춰 보기

브레이크포인트는 코드 실행을 잠시 멈추는 표시입니다. 완료 버튼 문제라면 버튼 클릭 이벤트가 ViewModel로 전달되는 줄, Repository를 호출하는 줄, 상태를 갱신하는 줄에 브레이크포인트를 걸 수 있습니다.

실행이 멈추면 다음을 확인합니다.

  • 현재 함수에 들어온 인자 값

  • ViewModel의 현재 UI 상태

  • Repository가 반환한 결과

  • 조건문이 어느 방향으로 실행되는지

  • 호출 스택에서 어떤 함수들이 차례로 호출됐는지

호출 스택은 현재 코드에 도달하기까지 거친 함수 호출의 목록입니다. 버튼 클릭에서 시작해 Composable, ViewModel, UseCase, Repository로 이어지는 흐름을 확인하면 문제가 어느 계층에서 생겼는지 좁힐 수 있습니다.

디버거의 Step Over, Step Into, Step Out도 자주 사용합니다. Step Over는 현재 줄을 실행하고 같은 함수 안에서 다음 줄로 이동합니다. Step Into는 현재 줄에서 호출하는 함수 내부로 들어갑니다. Step Out은 현재 함수를 끝까지 실행한 뒤 호출한 쪽으로 돌아갑니다2. 처음에는 천천히 한 줄씩 따라가며 앱의 실제 실행 순서를 익히는 것이 좋습니다.

조건부 브레이크포인트도 유용합니다. 예를 들어 특정 학습 항목의 id가 10일 때만 멈추고 싶다면 조건을 걸 수 있습니다. 목록이 길거나 같은 함수가 여러 번 호출될 때 모든 호출에서 멈추지 않아도 되므로 시간을 줄일 수 있습니다.

Logcat과 디버거를 함께 사용하기

디버거는 특정 순간의 값을 자세히 볼 때 강합니다. Logcat은 앱이 실행되는 전체 흐름을 시간순으로 볼 때 좋습니다. 둘 중 하나만 쓰기보다 함께 써야 원인을 더 빨리 찾을 수 있습니다.

예를 들어 다음과 같은 로그를 남길 수 있습니다.

  • 화면 진입 시점

  • 버튼 클릭 시점

  • Repository 호출 전후

  • 서버 응답 코드

  • Room 저장 성공 여부

  • 예외 발생 메시지

하지만 로그를 아무 곳에나 많이 남기면 오히려 읽기 어려워집니다. 로그는 질문에 답하도록 남겨야 합니다. 지금 확인하고 싶은 것이 입력값인지, 서버 응답인지, 상태 갱신인지 먼저 정하고 그 지점에만 남깁니다.

크래시가 발생하면 Logcat에서 스택 트레이스를 확인합니다. 특히 다음 세 가지를 먼저 봅니다.

  1. 예외 종류: NullPointerException, IllegalArgumentException, SQLiteException 등

  2. Caused by: 실제 원인이 이어서 표시되는 부분

  3. 파일명과 줄 번호: 앱 코드에서 문제가 발생한 위치

스택 트레이스의 맨 위만 보고 판단하면 안 됩니다. 라이브러리 내부에서 터진 것처럼 보여도 실제 원인은 우리가 잘못 넘긴 값일 수 있습니다. Caused by와 앱 패키지명으로 시작하는 줄을 함께 봐야 합니다.

수정 후에는 같은 재현 절차를 다시 실행합니다. 로그가 예상대로 바뀌었는지, 디버거에서 상태가 올바르게 변하는지 확인합니다. 버그 수정은 코드를 바꾸는 순간 끝나는 것이 아니라, 같은 조건에서 문제가 사라졌음을 확인할 때 끝납니다.

단위 테스트로 빠르게 검증하기

단위 테스트는 작은 코드 조각이 의도대로 동작하는지 확인하는 테스트입니다. 안드로이드 프로젝트에서는 보통 app/src/test/java에 작성하며, JVM에서 실행됩니다. 기기나 에뮬레이터 없이 빠르게 돌릴 수 있으므로 ViewModel, UseCase, 입력 검증, 계산 로직을 검증하기에 좋습니다. 로컬 단위 테스트는 Android Studio에서 테스트 클래스를 실행하거나 ./gradlew test 명령으로 실행할 수 있습니다3.

학습 관리 앱의 예를 들어 보겠습니다. 다음 로직은 단위 테스트에 적합합니다.

  • 제목이 비어 있으면 저장할 수 없다.

  • 완료한 항목 수와 전체 항목 수로 진행률을 계산한다.

  • 목표 날짜가 지난 항목은 지연 상태로 표시한다.

  • 서버 오류가 발생하면 UI 상태를 오류로 바꾼다.

테스트하기 좋은 코드는 입력과 출력이 분명합니다. 예를 들어 진행률 계산을 Composable 안에 직접 넣으면 테스트하기 어렵습니다. 반대로 calculateProgress(completed, total) 같은 함수로 분리하면 여러 조건을 빠르게 확인할 수 있습니다.

단위 테스트는 복잡할 필요가 없습니다. 중요한 것은 정상 상황만 확인하지 않는 것입니다.

  • 전체 10개 중 3개 완료면 30%인가?

  • 전체가 0개면 0%로 처리하는가?

  • 완료 수가 전체 수보다 클 수 없는가?

  • 잘못된 입력이 들어왔을 때 앱이 죽지 않는가?

이런 테스트는 작지만 강력합니다. 나중에 진행률 계산 방식을 바꾸더라도 기존 테스트가 깨지면 영향을 즉시 알 수 있습니다.

ViewModel 테스트의 핵심은 상태 변화다

Compose 앱에서 ViewModel은 화면 상태를 만드는 중심입니다. 따라서 ViewModel 테스트는 함수 호출 후 UI 상태가 어떻게 바뀌는지 확인하는 방식으로 작성합니다.

예를 들어 HomeViewModel이 학습 항목을 불러온다고 해 봅시다. 테스트할 시나리오는 다음과 같습니다.

  • Repository가 목록을 반환하면 Success 상태가 된다.

  • Repository가 빈 목록을 반환하면 빈 화면 상태가 된다.

  • Repository에서 예외가 발생하면 오류 상태가 된다.

  • 완료 버튼을 누르면 해당 항목의 완료 상태가 바뀐다.

여기서 실제 Room 데이터베이스나 실제 서버를 사용하면 테스트가 느리고 불안정해집니다. 네트워크 상태, 서버 데이터, 기기 환경에 따라 결과가 달라질 수 있기 때문입니다. 대신 가짜 Repository를 사용합니다.

가짜 Repository는 실제 데이터베이스나 서버에 접근하지 않고, 테스트가 정한 값을 반환하는 객체입니다. 성공 목록을 반환하도록 만들 수도 있고, 예외를 던지도록 만들 수도 있습니다. 이렇게 하면 성공, 실패, 빈 데이터 같은 상황을 안정적으로 반복할 수 있습니다.

테스트 더블이라는 표현도 자주 나옵니다. 외부 의존성을 대신하는 객체를 넓게 부르는 말입니다.

  • Fake: 실제처럼 동작하지만 단순한 구현을 가진 객체

  • Stub: 미리 정한 값을 반환하는 객체

  • Mock: 특정 함수가 호출됐는지 검증하는 객체

초반에는 Fake Repository만 잘 사용해도 충분합니다. 중요한 것은 ViewModel이 실제 Retrofit이나 Room을 직접 알지 않게 만드는 것입니다. Repository 인터페이스에 의존하면 테스트에서 가짜 구현으로 쉽게 바꿀 수 있습니다.

계측 테스트와 Compose UI 테스트

모든 코드를 JVM 단위 테스트로 확인할 수는 없습니다. Android Context, 실제 Activity, 리소스, 기기 화면, Compose UI와 상호작용이 필요한 동작은 Android 환경에서 실행해야 합니다. 이런 테스트는 보통 app/src/androidTest/java에 작성하며, 에뮬레이터나 실제 기기에서 실행됩니다. Gradle에서는 connectedCheck 같은 작업으로 연결된 기기 대상 테스트를 실행할 수 있습니다4.

Compose UI 테스트는 사용자의 행동에 가까운 방식으로 화면을 검증합니다.

  • 특정 텍스트가 표시되는가?

  • 버튼을 누르면 화면 상태가 바뀌는가?

  • 입력창에 값을 넣고 저장하면 목록에 추가되는가?

  • 로딩 상태에서 진행 표시가 보이는가?

  • 오류가 발생하면 재시도 버튼이 나타나는가?

UI 테스트를 작성할 때는 화면의 구현 세부사항보다 사용자가 보는 결과에 집중하는 것이 좋습니다. 예를 들어 내부 Composable 함수 이름을 검증하는 것은 의미가 없습니다. 사용자는 함수 이름을 보지 않습니다. 대신 '저장' 버튼이 보이고, 버튼을 누른 뒤 '학습 항목이 추가되었습니다'가 표시되는지를 확인합니다.

Compose에서는 테스트 태그를 붙여 특정 UI 요소를 찾을 수 있습니다. 다만 모든 요소에 무분별하게 태그를 붙이기보다, 자동화 테스트에서 반드시 찾아야 하는 입력창, 버튼, 목록 항목 등에 제한적으로 사용하는 것이 좋습니다.

테스트할수록 구조가 좋아진다

테스트는 품질을 확인하는 도구이면서 설계를 개선하는 도구입니다. 테스트가 지나치게 어렵다면 코드 구조가 너무 많이 얽혀 있다는 신호일 수 있습니다.

예를 들어 ViewModel 테스트를 하려는데 실제 서버 주소, 실제 데이터베이스, 실제 알림 권한, 실제 시간까지 모두 필요하다면 ViewModel이 너무 많은 것을 직접 알고 있을 가능성이 큽니다. 이때는 역할을 나눠야 합니다.

  • 현재 시간은 Clock 같은 인터페이스로 분리한다.

  • 데이터 접근은 Repository로 숨긴다.

  • 알림 예약은 별도 Manager로 분리한다.

  • 복잡한 규칙은 UseCase로 옮긴다.

이렇게 분리하면 테스트에서 원하는 상황을 쉽게 만들 수 있습니다. 오늘 날짜가 목표일을 지난 상황, 서버가 실패하는 상황, 알림 권한이 없는 상황을 실제 환경 없이 재현할 수 있습니다.

좋은 테스트는 빠르고, 반복 가능하며, 실패 이유가 분명해야 합니다. 한 번은 통과하고 한 번은 실패하는 테스트는 신뢰하기 어렵습니다. 외부 환경에 흔들리는 테스트보다 작은 단위의 안정적인 테스트를 많이 쌓는 편이 낫습니다.

크래시를 줄이는 기본 습관

크래시는 사용자의 작업을 끊고 앱 신뢰를 떨어뜨립니다. 크래시를 완전히 없앨 수는 없지만, 자주 발생하는 원인은 미리 줄일 수 있습니다.

안드로이드 앱에서 흔한 크래시 원인은 다음과 같습니다.

  • nullable 값을 안전하게 처리하지 않음

  • 빈 리스트에서 첫 번째 값을 무조건 꺼냄

  • 잘못된 화면 인자를 그대로 사용함

  • 권한이 없는데 카메라, 위치, 파일에 접근함

  • 네트워크 실패를 예외로만 처리하고 UI 상태로 바꾸지 않음

  • Activity나 Context를 오래 보관해 메모리 누수를 만듦

이 문제들은 대부분 방어적인 상태 설계로 줄일 수 있습니다. 데이터가 없으면 빈 화면 상태를 보여주고, 권한이 없으면 권한 안내 화면을 보여주고, 네트워크가 실패하면 재시도 가능한 오류 상태를 보여줍니다. 예외를 사용자에게 보이는 상태로 바꾸는 것이 중요합니다.

상세 화면에서 lessonId가 잘못 들어온 경우도 마찬가지입니다. 앱이 죽는 대신 '항목을 찾을 수 없습니다'라는 상태를 보여주고 뒤로 가기 버튼을 제공하는 편이 낫습니다.

메모리와 성능 문제는 측정해야 한다

성능 문제는 느낌만으로 판단하면 위험합니다. 어떤 개발자는 이미지가 문제라고 생각하지만 실제 원인은 메인 스레드에서 실행한 데이터 정렬일 수 있습니다. 또 어떤 화면은 네트워크가 느린 것이 아니라 불필요한 recomposition이 많아서 느릴 수 있습니다.

먼저 현상을 구체적으로 정리합니다.

  • 앱 시작이 느린가?

  • 특정 화면 전환이 느린가?

  • 목록 스크롤이 끊기는가?

  • 오래 사용하면 앱이 무거워지는가?

  • 특정 기능 후 메모리가 계속 증가하는가?

그다음 Android Studio Profiler, Logcat, 디버거를 함께 사용해 원인을 좁힙니다. 시간이 오래 걸리는 함수가 있다면 시작과 끝 로그를 남겨 대략적인 시간을 볼 수 있습니다. 더 자세한 분석이 필요하면 프로파일링 도구로 CPU, 메모리, 네트워크 사용량을 확인합니다. 디버거 자료에서도 메서드 실행 시간과 오래 걸리는 지점을 추적하는 방식이 소개됩니다5.

메모리 누수는 화면을 떠났는데도 Activity, Context, 리스너, 코루틴 작업이 계속 남아 있을 때 자주 발생합니다. ViewModel에서 Activity Context를 보관하지 말고, 센서나 위치 리스너는 생명주기에 맞춰 해제해야 합니다. 코루틴도 화면 수명과 맞는 scope에서 실행해야 합니다.

버그 수정 후에는 테스트를 남긴다

좋은 개발 습관은 버그를 고친 뒤 관련 테스트를 추가하는 것입니다. 이미 한 번 발생한 문제는 다시 발생할 가능성이 높습니다. 특히 다음 유형은 테스트로 남겨 두는 것이 좋습니다.

  • 빈 데이터에서 크래시가 났던 문제

  • 잘못된 입력값을 허용했던 문제

  • 네트워크 실패 시 로딩이 끝나지 않았던 문제

  • 완료 상태 변경 후 진행률이 틀렸던 문제

  • 화면 회전이나 재진입 후 상태가 사라졌던 문제

테스트 이름은 문서처럼 읽히게 작성합니다. 예를 들어 emptyTaskList_showsEmptyState처럼 조건과 기대 결과가 드러나면 좋습니다. 나중에 테스트가 실패했을 때 무엇이 깨졌는지 바로 알 수 있습니다.

출시 가능한 품질을 위한 점검표

출시 전에는 다음 항목을 최소한으로 점검합니다.

  • 주요 화면에서 정상, 로딩, 빈 데이터, 오류 상태가 모두 처리되는가?

  • ViewModel과 핵심 비즈니스 로직에 단위 테스트가 있는가?

  • 중요한 사용자 흐름에 Compose UI 테스트가 있는가?

  • Logcat에 반복되는 예외나 경고가 없는가?

  • 네트워크 실패, 권한 거부, 잘못된 화면 인자를 앱이 안전하게 처리하는가?

  • 긴 목록, 이미지, 데이터 로딩에서 눈에 띄는 버벅임이 없는가?

  • 버그를 수정한 뒤 같은 문제가 재발하지 않도록 테스트를 추가했는가?

테스트와 디버깅은 앱 개발의 마지막에 잠깐 하는 작업이 아닙니다. 새 기능을 만들 때마다 함께 따라가야 하는 개발 습관입니다. 작은 테스트가 쌓이면 코드를 바꾸는 일이 덜 두려워집니다. 로그와 디버거를 차분히 보면 복잡해 보이던 문제도 한 줄씩 좁혀집니다.

출시 가능한 앱은 기능이 많은 앱이 아니라, 예외 상황에서도 무너지지 않고 계속 개선할 수 있는 앱입니다.

참고

1The Android Studio debugger

2The Android Studio debugger

3Developing Android unit and instrumentation tests - Tutorial

4Developing Android unit and instrumentation tests - Tutorial

5The Android Studio debugger

책 목록

처음부터 출시까지 배우는 안드로이드 앱 개발

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기