tech

Xcode, ‘Point’만 콕 집어 디버깅 하기

폰을 들어 아무 앱이나 열어 보자. 앱이 실행되는 그 짧은 순간에도 수십 줄의 코드가 실행될 것이다. 이때, 만약 앱이 뚝 꺼져 버린다면 ‘언제’ 문제가 발생한 것일까? 상냥한 일부 유저는 개발자에게 “앱을 실행하려고 하면 꺼져요”라고 제보해 준다. 그러면 개발자는 ‘그 수십 줄의 코드 중 정확히 어떤 지점’에서 에러가 났는지를 파악하기 시작한다. 우리는 이를 ‘디버깅Debugging‘이라고 부른다.

그렇다면 개발자는 ‘그 지점’을 어떻게 찾을까? 힌트 삼을 만한 로그log, 일종의 실행 중 메모가 없다면 일단 문제가 발생한 상황을 재현해서 결정적인 순간에 일시정지를 누르면 된다. 컴퓨터를 넘어서는 스피드와 판단력으로 말이다. 와, 쉽다!

고양이라면?
© ideogram.ai

백만분의 일 확률로 얼추 성공한다 쳐도, ‘코드적인 상황’ 즉 당시 어떤 변수에 어떤 잘못된 값이 들어왔는지 따위를 검색해야 한다. 이때를 위해 존재하는 것이 바로 ‘포인트’ 2종 세트, Breakpoint와 Watchpoint다. 지금부터 Xcode에서 둘을 이용하는 방법을 알아보자.

시작하기 전에! 백문이 불여일견.
직접 따라해 봐야 기억에 남는 법이다.

.

.

.

혹시 방금 귀찮다고 생각했는가? 그럴 줄 알고 미리 준비했다. git과 Xcode만 있으면 이 간단한 프로젝트—체험키트—를 바로 다운받을 수 있다! 아래 명령어를 실행해 보자.

📢 제목에서 보이듯 이 글은 iOS 개발자를 대상으로 한다.
그렇지만 Xcode가 궁금한 웹 개발자, 안드로이드 개발자, 그냥 심심한 사람 모두 환영한다!📢

Breakpoint—이하 ‘중단점’— 자체는 이미 대부분 알고 있을 것이다. 에디터 라인 넘버를 클릭하면 활성화되는 그것 말이다. 하지만 중단점은 생각보다 종류가 많다. 네비게이터 바 중단점 탭 하단의 ‘+’를 누르면 확인할 수 있다.

Source File Breakpoint

나도 알고 너도 아는 컨텐츠이니 조금 더 상세히 다뤄보도록 하겠다.

종류

사실 ‘에디터 라인 넘버를 클릭하면 활성화되는 그것’의 정체는 line 중단점이다. 왜냐하면 column 중단점도 있기 때문이다! 라인 넘버만 클릭 할 수 있는 게 아니다. 원하는 가로 지점을 우클릭 하면 Create Column Breakpoint 메뉴가 숨겨져 있다. Xcode 13부터 생긴 기능이다.

코드는 깔끔할수록 좋지만 어쩔 수 없을 때가 있다. 예시코드에서처럼 연이은 고차함수 등으로 인해 가로로 길어진 표현식이 있다면 디버깅을 위해 세로로 풀 시간에 column 중단점을 걸어 보자.

응용

조건 설정하기

특정 조건을 만족할 때나 n번째일 때부터만 멈추도록 설정할 수도 있다. 중단점을 우클릭, Edit Breakpoint를 눌러 보자. Condition란에 조건을, Ignore란에 무시할 횟수를 넣어주면 완성이다. 아래 ‘동작 추가하기’와 엮으면 다양한 활용이 가능하다.

동작 추가하기

Add Action을 누르면 여러 선택지가 나온다. 그 중에서 Debugger Command, Sound가 유용한데, p나 po 커맨드를 입력해 두고 아래의 Automatically continue 옵션을 체크하면 그 구간에 print문을 삽입하고 재빌드 한 것과 같은 효과가 난다. Sound를 선택하면 한창 실행할 동안 다른 일을 보고 있다가 소리가 울릴 때만 확인하는 등 업무 효율을 높이는 데 활용이 가능하다.

위 3가지는 결과적으로 같은 동작을 수행한다.

Swift Error Breakpoint

Swift Error 중단점은 말 그대로 Swift Error가 발생했을 때 중단하는 포인트다. Swift Error란 Swift로 정의된 Error로서, 시스템 프레임워크의 기본 에러뿐 아니라 개발자가 커스텀 정의한 에러도 포함한다. 물론 에러를 던지고 캐치하는 곳에 간단하게 line 중단점을 만들어 볼 수 있다. 하지만 에러를 던지고 받는 사슬이 길 때 처음 에러가 발생한 지점을 알고 싶다든지, 그 지점에서 뭐가 잘못된 건지 찍어보고 싶다든지 하는 상황에서는 Swift Error 중단점이 제격이다. 물론 원하는 에러의 종류를 지정해줄 수도 있다!

예시 프로젝트에서는 JSON을 디코딩할 때의 에러를 감지하는 케이스를 제시했다. 처음부터 JSON 문자열이 잘못된 줄 알고 디코딩 메서드함수에 line 중단점을 찍어 볼 수도 있지만, 그렇지 않을 때가 많다. 서버로부터 데이터를 받아왔을 때부터 문제가 있었는지(네트워크 에러가 있었는지), 문자열을 가져올 json파일의 경로가 잘못되었던 건지, 예시코드처럼 오탈자가 있었던 건지 모르는 상황에서 Swift Error 중단점은 처음 에러가 발생한 시점에 알아서 멈춰 주는 고마운 기능이다.

Exception Breakpoint

Exception 중단점은 Swift Error 중단점의 짝꿍이라고 할 수 있다. Objective-C의 Exception로직을 정상적으로 처리할 수 없음이 발생 또는 감지되면 멈추는 중단점이다. 요즘은 대부분 Swift를 사용하지만 그럼에도 NSException이 발생할 가능성은 무궁무진하다. 대표적으로 예시로 Array의 index out of range범위 외 위치의 요소를 요구함가 있다.

아쉽게도 Swift에서는 NSException을 핸들링 할 수 없기에 크래시 발생 직전에 멈추려면, 그러니까 제대로 활용하려면 Objective-C 코드가 필요하다. 그러나 적어도 발생 지점에서 멈추어 줌으로써 상세한 디버깅을 가능케 해 준다.

아래는 Exception 중단점을 사용할 때와 아닐 때의 비교다. 중단점 없이는 Exception 발생 시 곧장 AppDelegate에서 빨간 에러메시지를 마주한다. 디버그 네비게이터에서 스레드 콜스택을 거슬러 올라가 발생 지점을 찾을 수는 있지만, 그 범위에 들어가 디버깅 할 수는 없다. 반면 중단점이 설정되어 있다면 정말 크래시가 나기 직전 해당 지점에서 먼저 멈춘다. 고로 원인이 뭔지 디버깅 해볼 수 있다.

중단점 없을 시
중단점 있을 시

TIP!
게다가 po $arg1으로 좀 더 상세한 에러메시지를 얻을 수 있다. Add Action으로 출력해 보자.

Symbolic Breakpoint

대망의 Symbolic 중단점이다. 몰랐다가 알게 되면 가장 유용하게 많이 쓸 포인트라고 할 수 있다.

어떤 이름을 가진 함수가 호출될실행될 때 로그를 찍거나 멈추려면 꼭 해당 함수가 정의된 곳에 line 중단점을 걸어줘야만 할까? 하지만 우리에겐 다형성이라는 게 있다. 같은 네이밍이 한둘이 아닌데 해당하는 코드에 대해 모두 적용하고 싶다면? 그 중에서도 특정 모듈이나 클래스 내에서 정의된 것만 따로 모아 중단점을 부여하고 싶다면?

특정 심볼, 즉 같은 이름을 가진 것들이 호출될 때마다 중단해주는 기능이 있다. 바로 Symbolic 중단점이다.

예시 프로젝트에서 아래 Symbol란을 ViewController.someMethod에서 someMethod로 수정해 주면 하위 중단점 목록에 SomeStruct.someMethod가 추가되는 것을 볼 수 있다. ViewController의 someMethod라는 조건이 사라지니 모든 someMethod들에 중단점이 걸렸다. 추가로, 정해진 실행파일이나 라이브러리에 정의된 것만으로 한정하고 싶다면 아래 Module란에 모듈명을 넣어주면 된다.

한편, 흔한 네이밍일 경우 주의해야 한다. 예를 들어 viewDidLoad가 있다. 특정 프레임워크로 한정하지 않으면 모든 viewDidLoad들, 즉 UIKit의 UIViewController를 상속한 모든 자식클래스들의 viewDidLoad에 대하여 중단점이 걸린다. 어쩌면 너무 많아서 로딩이 걸릴지도 모른다. 이는 반대로, Module에 HiveUI라고 작성할 경우 HiveUI라는 프레임워크 내 어떤 UIViewController 자식클래스들이 오버라이드재정의한 viewDidLoad들만으로 특정할 수 있다는 뜻이다. 그 소스코드에 접근할 수 없어도 말이다!

Runtime Breakpoint

Runtime 중단점의 키워드는 ‘체크’다. 지정한 타입에 대응하는 Diagnostic에 체크해줘야 한다. 무슨 말이냐면, Edit Scheme의 Run-Diagnostics 탭에서 원하는 종류의 체크박스를 활성화 해줘야 한다는 뜻이다. 어디서 설정하는지는 Edit Breakpoint 말풍선의 바로가기 버튼을 클릭하면 쉽게 알 수 있다. 이번에는 Main Thread Checker와 Thread Sanitizer 타입만 짚고 넘어가겠다.

Main Thread Checker와 Thread Performance Checker는 기본적으로 체크 되어 있다.

두 중단점은 직관적이게도 이름 그대로 ‘스레드’에 관한 문제를 진단한다. 예시 프로젝트는 둘의 대표적인 발동 케이스로서 각각 메인 스레드, 동시성 관련 문제를 유도할 예정이다. 프로젝트를 다운 받았다면 타입은 All로, Diagnostics도 Main Thread Checker와 Thread Sanitizer 활성화로 알맞게 세팅되어 있을 것이다.

실행해 보면, 예시에서는 UI로직을 메인이 아닌 백그라운드 스레드에서 돌려서, 한 데이터에 여러 스레드가 동시에 접근해서 문제를 발생시킨다. 단순화된 케이스라 그렇지 큰 프로젝트에서는 좀처럼 눈에 띄지 않을 수 있다. 따라서 이어서 설명할 Constraint Error 중단점과 함께 미리미리 활용하여 점검하는 편이 좋다. 해당 Diagnostics가 켜져 있지 않으면 보라색 경고마크도 뜨지 않기에 알게 모르게 산재해 있을 수 있기 때문이다.

Constraint Error Breakpoint

AutoLayout 설정 시 제약constraint끼리 충돌하면 멈춰주는 중단점이다.

원래대로라도 런타임에 문제가 생기지는 않지만, 다음과 같은 로그가 자동으로 뜬다. 길고 지저분할 뿐더러 정확히 어떤 UI 요소끼리 충돌한 건지 직관적으로 알기도 어렵다.

로그 메시지 말대로 symbolic 중단점을 만드는 것도 좋은 방법이지만, Constraint Error 중단점만 뚝딱 켜주면 손쉽게 확인할 수 있다. 아래는 Constraint Error 중단점을 만들어 주었을 때 충돌 발생 지점에서 실행이 일시정지된 장면이다.

Test Failure Breakpoint

Test Failure라는 이름대로 테스트가 실패하면 멈추는 중단점이다. 사실 잘 작성한 유닛 테스트의 경우 테스트 실패 알림으로 충분할 것이다. 대부분 의도한 실패거나 간단명료하게 되어 있을 테니까 말이다. 하지만 통합 테스트 등 불가피하게 복잡한 테스트의 경우, 특히 거기서 의도치 않은 실패가 나온 경우 간혹 필요할 수 있다. 또는 테스트도 실패도 많아 각 실패 케이스당 살펴볼 생각이라면 보탬이 될 기능이다.

Test Failure 중단점은 Constraint Error 중단점에서와 같이 실패로그가 뜨기 전에 멈춘다. 한 마디로 테스트가 실패한 순간 테스트 함수 내에서 멈추므로 해당 케이스에 대한 디버깅이 가능하다는 소리다.

Test Failure 중단점으로 탄탄한 테스트 고수가 되어 보자.

지금까지는 물가에 앉은 시간능력자가 되어 스쳐 지나가는 물고기들을 건드려 보았다. 이제부터는 원하는 물고기 허리에 끈을 묶고 물살을 따라가는 방법을 알아보자. 일명 Watchpoint—이하 ‘감시점’—에는 4종류가 있다. read, write, read-write, modified가 그것이다. 이 글에서는 기본값인 ‘감시할 변수값이 수정되었을 때 멈추는’ 즉 writeXcode 16 미만에서의 기본값 또는 modifiedXcode 16 이상에서 추가, 기본값 타입의 감시점을 다루도록 하겠다. (expression, variable로도 나눌 수 있지만 이 또한 사용 빈도상 후자만 다룬다.)

Watchpoint 만들기

만드는 방법은 중단점과 마찬가지로 2가지다. 클릭하여 만들기, lldb 명령어로 만들기 모두 가능하다. Xcode 16부터 후자로 생성해도 인스펙터 바에 표시되는 중단점과 달리 감시점은 그렇지 않아 좀 더 마이너하지만, 그 미만 버전에서 전자로 생성 시 실패하는 경우가 있어 후자도 서술하려 한다.

디버그 영역에서 우클릭하여 생성
디버그 콘솔에서 lldb로 생성

첫째, 클릭하여 만들기다. 먼저 해당 요소가 범위 내에 들어오는 첫 구간에 중단점을 설정한다. 제공한 예시에서는 viewDidLoad에 두었다. 런타임실행 중에 중단점이 발동하면 좌하단 뷰에 해당 요소가 뜰 것이다. 여기에 대고 우클릭, ‘Watch (요소 이름)’을 누르면 된다. 이는 좌측 네비게이터 바에서도 중단점 목록과 함게 확인할 수 있다.

둘째, 명령어로 만들기다. 첫째 방법과 마찬가지로 중단점을 설정하고 시작하면 편리하다. 중단점이 발동하면 lldb란에 아래 명령어를 입력한다.

그러면 콘솔에서 감시점이 생성되었다며 메모리 주소 등을 알려준다. 이 시점부터는 해당 요소에 새 값이 들어올 때마다 감시점이 발동하며 헌값·새값을 자동으로 찍어줄 것이다. didSet/willSet이 따로 필요 없다.

Watchpoint 활용하기

한 UIViewController가 있다. 이걸 모달뷰로 present하고자 한다—편의상 ModalVC라고 부르겠다. ModalVC의 요소가 ModalVC가 dismiss될 때 메모리에서 해제되는지 알고 싶다면 어떻게 할까? deinit은 ModalVC가 메모리에서 해제된 이후가 아니라 직전 시점이다. deinit에서 전부 nil할당을 해줄 수도 있지만 여의치 않을 때, 확실히 nil이 되었는지 알고자 한다면?

이럴 때 필요한 게 감시점이다. 특정 요소에 대해 감시점을 설정해 보자. 그리고 deinit에서의 nil할당 없이 ModalVC를 dismiss해 보자.

titleLabel이라는 공간에 UILabel 객체 주소가 들어갔다가 다시 nil이 할당된 것을 확인할 수 있다. 이렇듯, 특정 시점에 설정하는 중단점과 다르게 감시점은 특정 요소를 추적할 수 있다. 여기에는 위와 같은 UI요소 뿐만 아니라 클로저(핸들러) 등도 포함된다.

이 글을 읽은 당신, 오늘부터 print문들과 무한 재컴파일과 git revert로부터 안녕이다. 문제가 없더라도 프로젝트를 개선할 생각에, 혹은 이슈가 발생해도 개발자답게 우아하게 디버깅 할 생각에 두근두근 할 수도 있다!

분량상 담지 못한 내용이 참 많다. 혹시 호기심이 발동했다면 아래 ‘더 알아보기’가 도움이 될지도 모른다. 모쪼록 잘 활용해 주길 바란다. 마지막으로 긴 글을 읽어준 독자분들께 감사의 말을 전하며 이만 글을 마친다.

더 알아보기

  • 애플의 공식 튜토리얼을 원한다면?
    • 따끈따끈한 WWDC2024 영상을 추천한다. (🔗링크)
  • lldb 명령어가 궁금하다면?
    • Xcode 16부터는 p 명령어가 강화되어 깊이 팔 필요가 없어졌지만, 그래도 궁금한 참된 개발자는 이 페이지를 참고하면 좋다.(🔗링크)
  • 위 페이지 없이, 중단점과 감시점에 대한 명령어만 알고 싶다면?
    • lldb란에 apropos breakpointapropos watchpoint를 입력해 보자.
  • 감시점도 중단점처럼 커스텀하고 싶다면?
    • Xcode 16 beta 기준으로 우클릭하여 생성한 경우에도 edit하려면 명령어를 통해야 한다. lldb란에 help watchpoint ignorehelp watchpoint command add를 입력해 보자.
  • 중단점을 팀원들과 공유할 수 있을까?
    • 그렇다. 인스펙터 바에서 원하는 중단점을 우클릭, ‘Share Breakpoint’를 누르자. git status를 보면 해당 중단점이 작업트리에 반영되어 있는 것을 볼 수 있을 것이다.
이주은 기자

가진 지식을 실현하고 나누길 좋아해서 개발자가 되었습니다. 팀 막내로서 평소 관심 가졌던 주제를 사보라는 창구로 공유할 수 있게 되어 영광이고 뿌듯합니다. 글을 읽어주신 분께 작게나마 도움이 되었다면 너무나 기쁠 것 같습니다.


TOP