디버깅 소개

소프트웨어를 사용한 경험이 있는 사용자는 버그를 경험했을 가능성이 높습니다. 버그란 의도치 않은 동작(예: 앱 비정상 종료, 기능이 예상대로 작동하지 않음)을 발생시키는 소프트웨어 오류입니다. 환경과 무관하게 모든 개발자는 코드를 작성할 때 버그를 마주하며, Android 개발자에게 가장 중요한 기술 중 하나는 바로 버그를 식별하고 수정하는 것입니다. 버그 수정만을 위한 앱 버전이 출시되는 것은 드물지 않습니다. 예를 들어 아래의 Google 지도 버전 세부정보를 참조하세요.

9d5ec1958683e173.png

버그 수정 프로세스를 디버깅이라고 합니다. 저명한 컴퓨터 공학자 브라이언 커니핸은 "가장 효과적인 디버깅 도구는 신중하게 배치된 print 문과 함께 주의 깊게 생각하는 것입니다"라는 말을 남겼습니다. 물론 이것이 사실이지만 더욱 정교한 디버깅 도구를 사용하면 버그를 더 쉽고 빠르게 찾을 수 있습니다. 프로그래밍과 마찬가지로 디버깅도 시간이 지남에 따라 발전하는 기술이지만 Android 스튜디오에 빌드된 디버깅 도구를 익히는 것은 결코 늦지 않습니다. 이 과정에서는 Android 스튜디오의 통합 디버거를 살펴보고, 스택 트레이스를 읽는 방법과 중단점을 사용하여 코드를 단계별로 실행하는 방법을 알아봅니다.

기본 요건

  • Android 스튜디오에서 프로젝트를 탐색하는 방법을 알고 있습니다.

학습할 내용

  • 실행 중인 앱에 디버거를 연결하는 방법
  • 스택 트레이스에서 유의미한 정보를 찾는 방법
  • 중단점을 사용하여 실행 중인 앱을 일시중지하고 코드를 한 번에 한 줄씩 검사

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터

복잡한 대규모 앱을 디버그하는 대신 빈 프로젝트로 시작하여 Android 스튜디오의 디버깅 도구를 설명하기 위해 버그 코드를 의도적으로 도입할 것입니다.

다음과 같이 새 Android 스튜디오 프로젝트를 만듭니다.

  1. Select a Project Template 화면에서 Blank Activity를 선택합니다.

a949156bcfbf8a56.png

  1. 앱의 이름을 Debugging으로 지정하고 언어가 Kotlin으로 설정되어 있으며 다른 모든 부분이 변경되지 않았는지 확인합니다.

9863157e10628a87.png

  1. MainActivity.kt라는 파일을 표시하는 새 Android 스튜디오 프로젝트가 표시됩니다.

e3ab4a557c50b9b0.png

버그 작성하기

빈 프로젝트에서는 디버깅할 것이 많지 않습니다. 이 앱에 비정상 종료를 유발하는 코드를 추가해 보겠습니다.

수학 수업에서 숫자를 0으로 나눌 수 없다는 것을 배웠을 때를 기억하시나요? 코드에서 0으로 나누려고 하면 어떻게 되는지 살펴보겠습니다.

  1. MainActivity.kt를 열고 아래 함수를 추가합니다. 이 코드는 숫자 두 개로 시작하고 repeat를 사용하여 분자를 분모로 다섯 번 나눈 결과를 출력합니다. repeat 블록의 코드가 실행될 때마다 denominator 값이 1씩 감소합니다. 다섯 번째이자 마지막 반복에서는 앱이 0으로 나누기를 시도합니다.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        println(numerator / denominator)
        denominator--
    }
}
  1. onCreate()에서 division() 함수를 호출합니다. 새 onCreate() 함수는 다음과 같습니다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    division()
}
  1. 앱을 실행합니다. 첫 번째 앱이 표시되는 즉시 onCreate()가 실행되므로 앱이 시작 즉시 비정상 종료됩니다. 앱에서 비정상 종료가 발생하면 오류의 위치를 찾는 데 유용한 정보가 제공됩니다.
  2. Android 스튜디오 하단에서 Logcat 탭을 엽니다(탭이 처음 열리는 경우 시간이 오래 걸릴 수 있음).

e4d025b0363eaa63.png

  1. Logcat 창에는 많은 출력이 표시되므로 스크롤을 이용하여 정보를 찾아야 할 수도 있습니다. 앱의 출력만 표시되도록 하려면 왼쪽 상단의 드롭다운을 에뮬레이터(또는 실제 기기) 이름으로 설정하고 프로세스를 앱(com.example.debugging)으로 설정합니다.

5c008135b1804091.png

  1. 검색창에 "RuntimeException"을 입력하고 Enter를 눌러 오류 메시지를 검색합니다(Windows에서는 Ctrl+F, Mac에서는 Command+F).

9468226e5f4d5729.png

스택 트레이스의 구성

예외를 설명하는 텍스트 블록을 스택 트레이스라고 합니다. 스택 트레이스는 가장 최근에 호출된 것부터 시작해 예외까지 호출된 모든 함수를 표시합니다. 전체 출력은 다음과 같습니다.

Process: com.example.debugging, PID: 23296
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:17)
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:10)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

텍스트가 너무 많습니다. 다행히도 일반적으로 정확한 오류의 범위를 좁히는 데에는 몇 가지 도구만 필요합니다. 맨 위부터 시작하겠습니다.

  1. java.lang.RuntimeException:
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero

첫 번째 줄은 앱이 활동을 시작할 수 없었다고 설명하며, 이로 인해 앱이 비정상 종료되었습니다. 다음 줄은 정보를 조금 더 제공합니다. 구체적으로 활동을 시작할 수 없는 이유는 ArithmeticException 때문입니다. 자세히 살펴보면 ArithmeticException의 유형은 "divide by zero"였습니다.

  1. Caused by:
Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:17)

"Caused by" 줄까지 아래로 스크롤하면 "divide by zero" 오류가 다시 표시됩니다. 이번에는 오류가 발생한 정확한 함수(division())와 정확한 줄 번호(17)도 표시됩니다.

이 버그는 의도적으로 도입되었기 때문에 놀라울 일은 없습니다. 하지만 알 수 없는 오류의 원인을 파악해야 하는 경우 정확한 예외 유형, 함수 이름, 줄 번호를 알고 있으면 매우 유용한 정보를 얻을 수 있습니다.

이전 예에서는 스택 트레이스가 오류에 대한 구체적인 정보(예: 함수와 발생한 코드 줄)를 어떻게 제공했는지 확인했습니다. 실제 버그를 처리하는 경우 이것만으로 문제를 해결하기에는 충분하지 않을 수 있습니다. 예를 들어 코드에서 버그가 발생한 위치(줄 17)와 정확히 어떤 오류가 발생했는지(0으로 나누기) 알지만 코드가 숫자를 0으로 나누는 이유를 알지 못할 수 있습니다. 이 경우 division() 함수에서 나눗셈이 발생하기 전에 println() 문을 추가하여 분모로 사용되는 denominator 값을 출력할 수 있습니다.

fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        println(denominator)
        println(numerator / denominator)
        denominator--
    }
}

물론 문제가 더 복잡한 것으로 확인되면 유용한 정보를 찾을 때까지 계속 println() 문을 추가하고 앱을 다시 실행해야 합니다. 코드가 더 복잡해지면 추적하기 어려울 수 있습니다.

이 상황에서 중단점이 사용됩니다. 잘 배치된 print 문의 가치에 있어서는 브라이언 커닝핸의 말이 맞았을 수도 있습니다. 중단점은 비슷한 용도로 사용되지만 실행 중인 앱에서 일시중지 버튼을 누르는 것과 비슷합니다. 거의 모든 코드 줄에서 중단점을 설정할 수 있습니다. 중단점에 도달하면 모든 실행이 중지되고, 한 번에 한 줄만 실행하여 변수 값을 검사하거나 코드를 단계별로 실행할 수도 있습니다. 중단점을 사용하려면 디버거를 사용하여 앱을 실행해야 합니다.

디버거 연결하기

내부적으로 Android 스튜디오는 Android 디버그 브리지(ADB)라는 도구를 사용합니다. Android 스튜디오에 통합되어 있고 중단점과 같은 디버깅 기능을 실행 중인 앱에 제공하는 명령줄 도구입니다. 디버깅 도구를 디버거라고도 합니다.

디버거를 앱에 사용(또는 연결)하기 위해 이전과 같이 단순하게 Run > Run으로 앱을 실행하면 안 됩니다. 대신 Run > Debug 'app'을 사용하여 앱을 실행합니다.

21d706a854ebe710.png

프로젝트에 중단점 추가

작동 중인 중단점을 보려면 다음 단계를 수행합니다.

  1. 일시중지할 줄 번호 옆의 빈 공간을 클릭하여 중단점을 추가합니다. 줄 번호 옆에 점이 표시되고 줄이 강조표시됩니다.

6b6c2cd97bdc08ba.png

  1. Run > Debug 'app' 또는 툴바의 f6a141c7f2a4e444.png 아이콘을 사용하여 연결된 디버거를 통해 앱을 실행합니다. 앱이 실행되면 다음과 같은 화면이 표시됩니다.

3bd9cbe69d5a0d0e.png

앱이 실행되면 중단점이 활성화되면서 강조표시됩니다.

a4860e59534f216a.png

이전에 Logcat 창을 표시했던 화면 하단에 새로운 Debug 탭이 열렸습니다.

ce37d2791db7302.png

왼쪽에는 함수 목록(스택 트레이스에 표시되는 목록과 동일)이 있고 오른쪽에는 개별 변수의 값을 확인할 수 있는 창이 있습니다. 상단에는 일시중지 상태에서 프로그램을 탐색할 수 있는 버튼이 있습니다. 가장 많이 사용하는 것은 강조표시된 단일 코드를 실행하는 Step Over입니다.

a6c07c89e81abdc5.png

코드를 디버그하려면 다음 단계를 따르세요.

  1. 중단점에 도달한 후 줄 14(numerator 변수 선언)가 강조표시되지만 아직 실행되지 않았습니다. Step Over 1d02d8134802ee64.png 버튼을 사용하여 줄 14를 실행합니다. 이제 줄 15가 강조표시됩니다.

58f4bb135d5b756e.png

  1. 줄 17에 중단점을 설정합니다. 나눗셈이 발생했으며 스택 트레이스에서 예외를 보고한 줄입니다.

88d7d810a29965aa.png

  1. Debug 창 왼쪽에 있는 Resume Program 8119afebc5492126.png 버튼을 사용하여 다음 중단점으로 이동하고 나머지 division() 함수를 실행합니다.

433d1c2a610b7945.png

  1. 실행 전에 줄 17에서 실행이 중지됩니다.

1f6aedcf2a48c492.png

  1. 각 변수의 값(numeratordenominator)이 선언 옆에 표시됩니다. 변수 값이 Variables 탭의 디버그 창에도 표시되는지 확인합니다.

ebac20924bafbea5.png

  1. 디버그 창 왼쪽에 있는 Resume Program 버튼을 4번 더 누르고 매번 일시중지하여 numeratordenominator의 값을 관찰합니다. 마지막 반복에서 numerator60이고 denominator0이어야 합니다. 60을 0으로 나눌 수는 없습니다.

246dd310b7fb54fe.png

이제 버그를 유발하는 정확한 코드 줄을 알 수 있을 뿐만 아니라 정확한 이유를 알고 있습니다. 다섯 번째 반복에서 denominator 값은 0입니다. denominator0.과 같지 않은 경우에만 나눗셈을 실행할 if 문을 추가하여 이 오류를 해결할 수 있습니다.

fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        if (denominator != 0) {
            println(numerator / denominator)
        }
        denominator--
    }
}

코드를 반복할 횟수를 5에서 4로 변경할 수도 있습니다.

fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        println(numerator / denominator)
        denominator--
    }
}

이러한 방법을 사용하면 런타임 예외를 발생시키지 않고 코드를 실행할 수 있습니다. 한 번 시도해 보세요.

이전 과정에서는 Kotlin의 println() 문을 사용하여 텍스트 출력을 생성했습니다. Android 앱에서 출력 로깅 권장사항은 로그를 사용하는 것입니다. 출력을 로깅하는 여러 함수에서 Log.e() 또는 Log.d() 양식과 두 가지 매개변수를 가져옵니다. 첫 번째는 로그 메시지의 소스를 식별하는 문자열인 '태그'입니다(텍스트를 로그하는 클래스의 이름 등). 두 번째는 실제 로그 메시지입니다.

서로 다른 문자로 명명된 다른 로그 함수가 존재하는 이유는 다른 로그 수준에 상응하기 때문입니다. 출력하려는 정보의 유형에 따라 Logcat 출력에서 필터링하는 데 도움이 되는 다른 로그 수준을 사용합니다. 정기적으로 사용하는 5가지 기본 로그 수준이 있습니다.

로그 수준

사용 사례

ERROR

ERROR 로그는 앱이 비정상 종료된 이유 등 심각한 문제가 발생했다고 보고합니다.

Log.e(TAG, "The cake was left in the oven for too long and burned.").

WARN

WARN 로그는 오류보다 덜 심각하지만 더 심각한 오류를 방지하기 위해 수정해야 하는 사항을 보고합니다. 지원 중단된 함수를 호출하는 경우를 예로 들 수 있습니다. 즉, 최신 함수로 대체하여 사용하는 것이 좋습니다.

Log.w(TAG, "This oven does not heat evenly. You may want to turn the cake around halfway through to promote even browning.")

INFO

INFO 로그는 성공적으로 완료되는 작업과 같은 유용한 정보를 제공합니다. println() 문의 로그 수준입니다.

Log.i(TAG, "The cake is ready to be served.").println("The cake has cooled.")

DEBUG

DEBUG 로그에는 문제를 조사할 때 유용한 정보가 포함됩니다.

Log.d(TAG, "Cake was removed from the oven after 55 minutes. Recipe calls for the cake to be removed after 50 - 60 minutes.")

VERBOSE

이름에서 알 수 있듯이 디버그 로그로 간주되는 상세 로깅은 약간 주관적입니다. 일반적으로 상세 로깅은 기능이 구현된 후 삭제될 수 있는 반면 디버그 로그는 디버깅에도 유용할 수 있습니다.

Log.v(TAG, "Put the mixing bowl on the counter.")Log.v(TAG, "Grabbed the eggs from the refrigerator.")Log.v(TAG, "Plugged in the stand mixer.")

각 유형의 로그 수준을 사용해야 하는 경우, 특히 INFO, DEBUG, VERBOSE를 사용하는 경우에 대한 기본 규칙은 없습니다. 소프트웨어 개발팀은 각 로그 수준을 사용해야 하는 경우에 대한 자체 가이드라인을 만들거나 VERBOSE와 같은 특정 로그 수준을 아예 사용하지 않기로 결정할 수 있습니다.

Logcat에서 이와 같은 다양한 로그 수준의 모습을 살펴보겠습니다.

  1. MainActivity.kt에서 클래스 선언 앞에 TAG라는 상수를 추가하고 값을 클래스 이름인 MainActivity로 설정합니다.
private const val TAG = "MainActivity"
  1. 다음과 같이 MainActivity 클래스에 logging()이라는 새 함수를 추가합니다.
fun logging() {
    Log.e(TAG, "ERROR: a serious error like an app crash")
    Log.w(TAG, "WARN: warns about the potential for serious errors")
    Log.i(TAG, "INFO: reporting technical information, such as an operation succeeding")
    Log.d(TAG, "DEBUG: reporting technical information useful for debugging")
    Log.v(TAG, "VERBOSE: more verbose than DEBUG logs")
}
  1. onCreate()division() 호출(이전 예)을 logging() 호출로 바꿉니다. 새 onCreate() 메서드는 다음과 같이 표시됩니다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. 앱을 실행하고 Logcat의 출력을 관찰합니다. 필요한 경우 출력을 필터링하여 com.example.debugging 프로세스의 로그만 표시합니다. 출력을 필터링하여 "MainActivity" 태그가 있는 로그만 표시할 수도 있습니다. 이렇게 하려면 Logcat 창의 오른쪽 상단에 있는 드롭다운 메뉴에서 "Edit Filter Configuration"을 선택하세요.

5fa189e6b18a966a.png

  1. 그런 다음 Log Tag에 "MainActivity"를 입력하고 다음과 같이 필터 이름을 만듭니다.

6dbba17eb5df15eb.png

  1. 이제 "MainActivity" 태그가 있는 로그 메시지만 표시됩니다.

4061ca006b1d278c.png

클래스 이름 앞에 로그 수준에 해당하는 문자(예: W/MainActivity)가 있는지 확인합니다. 또한 WARN 로그는 파란색으로 표시되지만 ERROR 로그는 빨간색으로 표시되며, 이전 예시의 치명적인 오류와 동일합니다.

  1. 프로세스별로 디버그 출력을 필터링하는 방법과 마찬가지로 로그 수준별로 출력을 필터링할 수도 있습니다. 기본적으로 Verbose로 설정되어 있으며, VERBOSE 로그 및 상위 로그 수준을 표시합니다. 드롭다운 메뉴에서 Warn을 선택하면 WARNERROR 수준의 로그만 표시됩니다.

c4aa479a8dd9d4ca.png

  1. 다시 드롭다운을 Assert로 변경하면 표시되는 로그가 없는 것을 관찰할 수 있습니다. 이렇게 하면 ERROR 수준 이하의 모든 항목이 필터링됩니다.

169a0bc232f77734.png

이렇게 하면 println() 문이 너무 심각한 것처럼 보일 수 있지만 더 큰 앱을 빌드할수록 더 많은 Logcat 출력이 발생하며, 다양한 로그 수준을 사용하면 가장 유용하고 관련성 높은 정보를 선택할 수 있습니다. 로그를 사용하는 것이 권장사항으로 여겨지며 Android 개발에서는 println()보다 선호됩니다. 올바른 로그 수준을 선택하면 개발 팀에 있는 다른 개발자처럼 코드만큼 익숙하지 않은 경우 도움이 되며, 버그를 쉽게 파악하고 해결할 수 있습니다.

기본적으로 프로젝트를 만드는 데 사용한 Blank Activity 템플릿은 화면 중앙에 TextView가 있는 단일 활동을 추가합니다. 앞서 학습한 것과 같이 Layout Editor에서 ID를 설정하고 findViewByID()로 뷰에 액세스하여 코드에서 뷰를 참조할 수 있습니다. 다른 버그를 설명하기 위해 뷰에 액세스해 보겠습니다.

  1. activity_main.xml을 열고 Hello, world! TextView를 선택한 다음 ID를 hello_world로 설정합니다.

8a5dede436e2718e.png

  1. 다시 onCreate()ActivityMain.kt에서 코드를 추가하여 TextView를 가져오고 텍스트를 "Hello, debugging!"으로 변경한 다음setContentView()를 호출합니다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val helloTextView: TextView = findViewById(R.id.hello_world)
    helloTextView.text = "Hello, debugging!"
    setContentView(R.layout.activity_main)
    division()
}
  1. 앱을 다시 실행하고 실행 즉시 다시 비정상 종료되는지 확인합니다. com.example.debugging으로 로그를 필터링합니다.

cdb335255d798a0a.png

예외는 Logcat에서 마지막에 표시되는 것 중 하나입니다(표시되지 않는 경우 RuntimeException 검색 가능). 출력은 다음과 같을 것입니다.

com.example.debugging E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.debugging, PID: 5516
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.IllegalStateException: findViewById(R.id.hello_world) must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.IllegalStateException: findViewById(R.id.hello_world) must not be null
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:10)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

이전과 마찬가지로 상단에 "Unable to start activity"가 표시됩니다. 이는 MainActivity를 실행하기도 전에 앱이 비정상 종료되었기 때문입니다. 다음 줄은 오류에 대해 조금 더 자세히 설명합니다.

java.lang.IllegalStateException: findViewById(R.id.hello_world) must not be null

스택 트레이스를 살펴보면 정확한 함수 호출 및 줄 번호가 표시되는 줄도 표시됩니다.

Caused by: java.lang.IllegalStateException: findViewById(R.id.hello_world) must not be null
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:10)

이 오류의 정확한 의미는 무엇이며 "null" 값은 정확히 무엇인가요? 이는 잘못된 예시이고 앱이 비정상 종료된 이유를 이미 알고 있을 수도 있겠지만 이전에는 본 적이 없는 오류 메시지를 분명히 마주할 것입니다. 이 경우 오류를 처음 경험한 사용자가 아닐 수 있으며, 가장 숙련된 개발자조차도 다른 사용자가 문제를 어떻게 해결했는지 확인하기 위해 Google에 오류 메시지를 검색합니다. 이 오류를 검색하면 개발자가 질문을 하고 버그가 있는 코드 또는 보다 일반적인 프로그래밍 주제에 대한 답변을 제공할 수 있는 사이트인 StackOverflow에서 여러 결과를 확인할 수 있습니다.

efa074b344d1704c.png

몇 가지 답변을 읽으면 오류의 원인이 여러 가지인 것을 알 수 있습니다. 하지만 예를 단원 1의 코드와 비교하면 setContentView()를 호출하기 전에 의도적으로 뷰에 액세스하려고 시도했음을 알 수 있습니다. 뷰가 존재하기 전에 뷰에 액세스하려고 하면서 런타임 예외가 발생했습니다.

코드를 업데이트하여 오류를 수정합니다.

  1. findViewById()setContentView() 호출 아래의 helloTextView 텍스트를 설정하는 줄로 호출을 이동합니다. 새 onCreate() 메서드는 다음과 같습니다.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   val helloTextView: TextView = findViewById(R.id.hello_world)
   helloTextView.text = "Hello, debugging!"
   division()
}
  1. 그런 다음 앱을 다시 실행하고 코드가 더 이상 비정상 종료되지 않고 텍스트가 예상대로 업데이트되는지 확인합니다.

e52adf1c7bf6f792.png

요약하면 다음과 같습니다.

  • 디버깅은 코드의 버그 문제를 해결하는 프로세스입니다.
  • 스택 트레이스는 예외를 발생시킨 정확한 함수와 예외가 발생한 줄 번호와 같이 예외에 대한 정보를 제공합니다.
  • 중단점을 설정하여 앱 실행을 일시중지할 수 있습니다.
  • 실행이 일시중지된 경우 코드를 한 줄만 실행하도록 "Step Over"를 사용할 수 있습니다.

자세히 알아보기