[Swift] Logging에 대한 고찰

유경박·2023년 10월 31일
1

들어가기에 앞서

개발을 진행하다보면 콘솔창에 많은 Warning Logs 때문에 원인 분석을 하기가 힘들다. 또한 네트워크와 관련된 에러 로그, 앱의 규모가 커짐에 따라 스레드의 사용 극대화를 위한 동시성 및 병렬성과 같은 기술적인 스레드 사용 기법을 통해 발생되는 코드 및 구조의 전반적인 통합적인 Log의 관리가 필요하다.

(이는 전체적인 OS의 운영정책과도 관련이 있으며 안정적인 앱운영을 위해 경고메시지와 관련된 메시지를 통합적으로 관리하는 기능이 필요하다.)

이때, 간략하고 쉽게 사용할수 있는 기능이
Swift 14.0버전 이상부터 사용가능한 OSLog이다.


(요약하자면, 로그 카테고리를 통해 앱의 전반적인 시스템을 관리하는 하나의 객체라고 볼수있다.)

그러면 이 Logger는 print와 어떤점이 다를까?

1)print는 콘솔 출력용, osLog는 구조적으로 출력이 가능하다.

-print는 호출되는 부분에서 단순히 "콘솔창에 출력"용으로만 사용이 가능하며 구조적으로 사용하려고 한다면 추가적인 작업이 필요하며 파일저장,OS 콘솔 로그 출력등의 기능은 제공하지 않는다. 하지만 OSLog 이용하면 호출되는 시간의 로그들을 파일로 저장 및 OS 콘솔 로그 등의 출력을 쉽게 관리할수있으며, OSLog 디버깅 레벨에따라 로그의 구조를 따로 관리하여 저장할수 있다.


(출력하는 로그의 구조를 설정하여 콘솔창에 출력)


(os console창에 따로 프로그램의 로그만 출력도 가능하다.)

2)print는 Thread safe하지 않고 OSLog Thread-safe하기 때문에 안원인 추적에 용이하다.

-print는 Thread safe하지 않기때문에 다중 스레드 운영 환경에서 로그를 남길시 print내용이 전부 출력되지 않는 등 Thread-safe하기 위한 구성을 따로 조성해야 하는 반면 OSLog Thread safe하기 때문에 안전하기 출력이 가능하다.

그러면 이 OSLog 통해 프로젝트의 로그 내용을 어떻게 관리하는지 대략적으로 파악해보자.

프로젝트에서의 사용

1)서버 통신과의 오류 발생시 에뮬레이터 상에서 로그 내용을 저장

-본 프로젝트에서는 Firebase의 데이터를 CRUD하는 과정 중 에러가 발생했을 때 에뮬레이터에 데이터 저장하여 파일로 만든후 로그를 남기는 용도로 사용하였다.

(Firebase뿐만 아니라, 소켓통신의 경우도 사용이 가능하며, 특히 소켓통신의 경우 브레이크 포인트를 적중 시 일정시간 후 소켓 연결이 끊길수도 있기 때문에 이러한 Logging 기능을 통해 에러내용을 관리하는것이 앱 운영 관점에서 중요하다.)

2)소스코드의 에러 발생 지점과, Call Stack Trace까지 저장하여 앱이 중지되거나 에러 발생시 모든 로그를 발생

-OSLog의 단편적인 기능을 확장하여 자세한 원인 분석이 필요한 경우 에러 발생시 호출지점과 관련 된 Call Stack을 출력하여 추적을 용이하게끔 만들었다.


(파일로 관리할수도 있고 OS 콘솔창을 통해 확인하여 관리할수도 있다.)

소스 코드

Enum

enum LogLevel {
case debug
case info
case warning
case error
case fatal
}

Struct

struct Logger {
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "gjonegg")
static func writeLog(_ level: LogLevel, message: String, isNeededStackTraceInfo : Bool = true, line : Int = #line, fileName : String = #file) {
    let logType: OSLogType
    var logMessage = ""
    var emoji = ""
    switch level {
    case .debug :
        logType = .debug
        emoji = "ℹ️"
    case .info:
        logType = .info
        emoji = "✅"
    case .warning:
        logType = .default
        emoji = "⚠️"
    case .error:
        logType = .error
        emoji = "❌"
    case .fatal:
        logType = .fault
        emoji = "🚫"
    }
    logMessage = "[\(Date().getCurrentTime())] : \(emoji) : \(message) -> \(fileName.split(separator: "/").last!) :\(line)\r\n"
    if isNeededStackTraceInfo{
        logMessage += Thread.callStackSymbols.joined(separator: "\r\n")
    }
    if level == .error || level == .fatal {
        #if DEBUG
        saveLog(logMessage)
        #endif
    }
    os_log("%@", log: log, type: logType, logMessage)
}
    private static func saveLog(_ logMessage: String) {
    DispatchQueue.global().async {
        if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
            let currentTime = Date().getCurrentTime(Dataforamt : "yyyy-MM-dd")
            let fileName = "error_log_\(currentTime).txt"
            let fileURL = documentsDirectory.appendingPathComponent(fileName)
            print(fileURL) // 경로 이걸로 확인
            do {
                if FileManager.default.fileExists(atPath: fileURL.path) {
                    let fileHandle = try FileHandle(forWritingTo: fileURL)
                    fileHandle.seekToEndOfFile()
                    if let data = logMessage.data(using: .utf8) {
                        fileHandle.write(data)
                        fileHandle.closeFile()
                    }
                } else {
                    try logMessage.write(to: fileURL, atomically: false, encoding: .utf8)
                }
            } catch {
                fatalError("로그 파일 열기 또는 추가 실패: \(error)")
            }
        }
    }
}

사용법

func deleteAccount(completion: @escaping (FireAuthError) -> Void)     
guard let currentUserUID = Auth.auth().currentUser?.uid else {
        Logger.writeLog(.error, message: "[\(FireStorageDBError.unavailableUUID.code)] : \(FireStorageImageError.unavailableUUID.description)")
        completion(.error(FireStorageDBError.unavailableUUID.code, FireStorageImageError.unavailableUUID.description))
        return
    }
    ...
    ...
    기타 로직 구현

이런식으로 에러핸들링에 로그를 남기면 Debug환경에서 에뮬레이터 환경에서 에러 발생시 자동으로 파일로그 txt파일 생성 및 os 콘솔 Log에 출력된다.

마치며

print사용으로 디버깅 대비 OSLog 통해 Log를 관리하면 다음과 같은 이점이 있다.

  1. os콘솔창을 이용하여 카테고리 및 유형 통해 구조적으로 로그를 쉽게 확인 및 적용 시킬수있다.

  2. Thread-Safe한 영역에서 Log를 관리하기 때문에 안정적인 로그를 관리할수 있다.

  3. 로그에 대한 적용을 확장시킬수있다.

profile
으아아

1개의 댓글

comment-user-thumbnail
2023년 10월 31일

와 좋은 글 잘 보고 갑니다!

답글 달기