개발공방

[Swift] print()와 같은 디버깅 코드들이 앱에 어떤 영향을 미칠까? 본문

Swift

[Swift] print()와 같은 디버깅 코드들이 앱에 어떤 영향을 미칠까?

chemi_ 2023. 4. 27. 17:00

코드리뷰를 겪으면서 print() 구문이나, dump() 를 남겨뒀을 때 지워달란 리뷰를 받은적이 있다. 난 해당 값들을 확인하면서 개발하는게 편해서 남겨뒀다가 미처 지우지 못했던 것이다.

그러다 문득 의문이 들었다. 디버깅 코드들이 과연 얼마나 앱에 영향을 미칠까??

이에 대해 알아보았다.

명확한 공식문서나 WWDC영상은 찾을 수 없었으나, stackoverflow를 통해 어느정도 해답을 얻을 수 있었다.

 

print(), dump() 와 같이 개발하면서 디버깅시에 도움을 주는 코드들이 있다.

하지만 이 코드들은 프로덕션(릴리즈) 상태에서는 해당 코드가 실행은 되지만, 눈으로 확인할 수 없다. 말 그대로 디버깅 코드이기 때문이다.

위와 같은 디버깅 코드들이 프로덕션 코드에 남아있을 때, 생길 수 있는 단점이 분명 있다.

  1. 앱 성능이 저하된다.
    공식문서나 WWDC영상에서 언급된 성능이 저하된다는 얘기는 찾아볼 수 없었지만, 앱 성능이 저하된다는건 사실이다. print() 는 I/O 작업이기 때문에 main스레드에 영향을 미친다고 한다. https://stackoverflow.com/questions/39235738/deleting-print-lines-from-code
  2. 악의적인 용도로 사용될 가능성이 있다.
    세부 정보들이 다 표시되기 때문에, 누군가 악의적인 마음으로 공격한다면 굉장히 위험하다.

그래서 디버깅 코드들을 따로 처리를 해줄 필요가 있다.

 

디버깅을 하는 상황은 너무 다양하지만, 예시로 네트워크 통신을 예시로 들어보겠다. (실제로 자주 확인해보곤 한다.)

 

우선 예시 코드를 확인해보자

private func requestWaitRoomInfo(completionHandler: @escaping ((Result<Room, NetworkError>) -> Void)) {
    Task {
        do {
            let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
            if let roomInfo = data {
                self.roomInformation = roomInfo
                completionHandler(.success(roomInfo))
            }
        } catch NetworkError.serverError {
            completionHandler(.failure(.serverError))
        } catch NetworkError.clientError(let message) {
            completionHandler(.failure(.clientError(message: message)))
        }
    }
}

코드는 간단하다.

if let roomInfo = data 즉, 네트워크 통신이 성공적으로 이루어지면(디코딩된 데이터가 있으면) if let 내부로 들어가서 successscompletionHandler 로 넘겨주고, 실패한다면 catch 구문에서 failure 를 넘겨준다.

 

위의 경우에서 roomInfo에 어떤 데이터가 들어왔는지 확인하고 싶다고 가정해보자.

일반적인 방법으론 print(roomInfo) 를 생각할 수 있다.

우리가 받을 Http Response의 형식은 아래 사진과 같다.

 

제대로 응답이 오는지 확인하기 위해서 값을 출력해보자.

 

print()

먼저 가장 일반적인 print() 를 찍어보자.

print(roomInfo) 를 했을 때 어떻게 찍히는지 보자.

print(roomInfo)

 

dump()

원하는 정보를 확인할 순 있지만, 가독성이 조금 떨어진다. 그래서 난 print() 보단 dump() 를 더 선호한다.

객체형식을 출력할 땐 print() 보단 dump() 가 더 읽기 수월하다

dump(roomInfo) 를 하면

dump(roomInfo)

앞선 print(roomInfo) 보다 훨씬 읽기 편하다.

하지만 이 방식의 단점이 있다면, 코드를 실제로 남겨야 한다는 점이다.

더 나은 방법으로 추천하는게

BreakPoint -po

수많은 코드들 속에 디버깅 코드를 적지 않아도 되는 방법이 BreakPoint를 이용한 방법이다.

117번째 줄에 BreakPoint를 걸어서 확인할 수 있다.

이 상태로 해당 코드가 실행되는 View로 이동하면

위와 같이 BreakPoint가 걸리고 앱이 중단된다.

❗️현재 상태에선 roomInfo에 아직 data가 할당되지 않았기 때문에 po roomInfo 를 할 순 없다.

건너뛰는듯한 버튼(다음줄로 이동)을 눌러서 아랫줄로 이동시키면

위와 같이 roomInfo 를 확인해볼 수 있다.

po roomInfo 를 확인해보면 dump() 와 비슷한 형식으로 확인할 수 있다.

디버깅시에 BreakPoint를 활용하는 것은 아주 좋은 방법인 것 같다. 실제로 사용하면 너무 편하기 때문이다.

 

print() , dump() 같은 코드는 적고 다시 실행을 시켜야하지만, BreakPoint 는 실행중에도 해당 위치에 Break를 추가하고 계속 앱을 진행시켜도 적용이 되는 장점이 있다.

 

위의 print(), dump() BreakPoint는 순간순간 확인이 가능하지만, 항상 출력을 찍어 놓고 싶을때가 있다.

실제로 난 네트워크 통신에 대한 Response 값은 항상 출력을 시켜서 잘 되는지 확인하고 싶었다.

그럴때 유용한 방법을 소개하겠다.

 

#if DEBUG

이름에서도 알 수 있듯이, Debug 상태에서만 실행되는 코드이다. 프로덕션(릴리즈) 코드로 넘어가면 해당 코드는 실행되지 않는 장점이 있다.

사용방법은 매우 간단하다

그냥 위 아래로 #if DEBUG  #endif 를 감싸주면 OK다.

 

위 상황들을 디버깅 상황에서 해당 객체들을 출력해서 확인하고 싶은 용도였다.

 

이제 ERROR 상황에 각각의 로그들을 출력하고 싶다면 아주 좋은 방법이 있다.

애플에서 권장하는 로깅 방식을 소개하겠다.

 

os_log

https://developer.apple.com/documentation/oslog

 

OSLog | Apple Developer Documentation

A unified logging system for the reading of historical data.

developer.apple.com

위에 설명했던 print() dump() 와는 방식이 다른게, 로깅의 개념이다.

두 함수는 해당 변수의 내용들을 출력해보기 위해서 사용했지만, 로깅은 에러가 생겼을 때의 상황을 로그해두면 좋은 방식이다.

 

사용방법은 크게 어렵지 않다.

  1. 라이브러리를 import 해준다
import OSLog

2. 로그를 남기고 싶은 위치에 os_log(”에러 메시지 작성”) 을 하면 끝이다.

Task {
    do {
        let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
        if let roomInfo = data {
            self.roomInformation = roomInfo
            completionHandler(.success(roomInfo))
        }
    } catch NetworkError.serverError {
        os_log("방 정보 불러오기 서버 오류") // <<<<<< 👍
        completionHandler(.failure(.serverError))
    } catch NetworkError.clientError(let message) {
        completionHandler(.failure(.clientError(message: message)))
    }
}

(잘 되는지 확인을 위해 os_log 의 위치를 조금 옮겨보겠다.)

Task {
    do {
        let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
        if let roomInfo = data {
						os_log("테스트 해보겠습니다.") // <<<<<< 👍
            self.roomInformation = roomInfo
            completionHandler(.success(roomInfo))
        }
    } catch NetworkError.serverError {
        completionHandler(.failure(.serverError))
    } catch NetworkError.clientError(let message) {
        completionHandler(.failure(.clientError(message: message)))
    }
}

3. 콘솔 앱을 연다

 

4. 해당 기기를 선택하고 시작 을 누른다.

5. 앱에서 해당 코드를 적은 부분을 지나가게끔 실행시켜주면 된다.

6. 검색에 해당 메시지를 검색하면 잘 나오는 걸 확인할 수 있다.

여기까지 아주 간단하게 os_log 를 활용하는 방법을 알아보았다.

 

조금 더 편하게 os_log를 사용하는 법

추가적으로 더 편하게 사용하려면 extension 을 활용하면 편하다.

extension OSLog {
    private static let subsystem = Bundle.main.bundleIdentifier! // 1.
    
    static let network = OSLog(subsystem: subsystem, category: "Network") // 2.
		static let ui = OSLog(subsystem: subsystem, category: "UI")
}
  1. 해당 앱의 bundleID를 통해 콘솔에서 식별하는게 일반적이다.
  2. 각각의 에러 케이스들을 정의해두면 편하다. network에러나, UI에러 등등 입맛에 맞게끔 추가해두자.

그리고 로그가 필요한 곳 에서

os_log("네트워크 에러", log: .network)

위와 같이 사용하면 아주 편리하다.

 

물론 Xcode내에도 출력된다.

 

 

os_log는 애플에서 권장하는 로깅 방식이기 때문에 적극 사용하면 좋을 것 같다.

 

 

참고자료

https://developer.apple.com/documentation/oslog

 

OSLog | Apple Developer Documentation

A unified logging system for the reading of historical data.

developer.apple.com

https://stackoverflow.com/questions/39235738/deleting-print-lines-from-code

 

Deleting print lines from code

I have weirdest question I came up with today since I am going to release application soon. My question is, is it necessary to remove all lines from code which are print(""). Does it somehow affect...

stackoverflow.com

 

Comments