iOS

Swift에서 가독성 높은 Logger 클래스 만들기

낙타가낙타낙타 2024. 11. 10. 17:20

 Swift에서 기본적으로 제공하는 print() 문은 간단하게 디버깅할 수 있지만, 로그 레벨이나 파일 위치 등 세부 정보를 제공하지 않아 큰 프로젝트에서는 효과적으로 오류를 추적하기 어려운 경우가 많습니다. 특히 여러 개발자들이 협업할 때는 코드 흐름과 오류 발생 위치를 빠르게 파악할 수 있는 체계적인 로그 시스템이 필요합니다.

 

 이 문제를 해결하기 위해, 과거 사이드 프로젝트에서 사용했던 Logger 형식을 수정해 더욱 효율적이고 직관적인 로그 출력을 구현하게 되었습니다. 이번 글에서는 이 Logger 클래스를 단계별로 만들어가며, 로그 레벨, 시간 포맷팅, 파일 위치 등 다양한 정보가 포함된 커스텀 로그 기능을 Swift에서 구현하는 방법을 공유합니다.

 

목표

  • 로그 레벨 지원: 로그의 중요도에 따라 debug, info, warning, error, severe 등의 레벨을 구분하여 관리할 수 있도록 합니다.
  • 위치 정보 제공: 로그를 남긴 파일 이름, 함수 이름, 라인 및 컬럼 번호를 표시해 로그가 출력된 위치를 쉽게 추적할 수 있게 합니다.
  • 타임스탬프 지원: 로그가 출력된 시점을 기록하여 시간 정보를 확인할 수 있도록 합니다.
  • 스레드 안전성: 멀티 스레드 환경에서도 안정적으로 동작하도록 구현합니다.

 

Logger 클래스 전체 코드 및 구조

Logger 클래스를 extension으로 구분하여, 기능별로 쉽게 이해할 수 있도록 구조화하였습니다.

import Foundation

final class Logger {
    static let shared = Logger()
}

 

MARK: - 로그 레벨 정의

extension Logger {
    fileprivate enum LogEvent: String {
        case d = "[💬]" // 디버그
        case e = "[‼️]" // 에러
        case i = "[ℹ️]" // 정보
        case v = "[🔬]" // 상세
        case w = "[⚠️]" // 경고
        case s = "[🔥]" // 심각
    }
}

설명:

  • LogEvent 열거형: 로그 레벨을 정의하는 열거형으로, 각 레벨은 한눈에 구분할 수 있도록 이모지와 함께 설정했습니다. 이 레벨을 사용해 로그의 중요도를 시각적으로 쉽게 구분할 수 있습니다.

 

MARK: - 로그 레벨별 함수

extension Logger {
    static func d(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.d, object, filename: filename, line: line, column: column, funcName: funcName)
    }

    static func e(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.e, object, filename: filename, line: line, column: column, funcName: funcName)
    }

    static func i(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.i, object, filename: filename, line: line, column: column, funcName: funcName)
    }

    static func v(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.v, object, filename: filename, line: line, column: column, funcName: funcName)
    }

    static func w(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.w, object, filename: filename, line: line, column: column, funcName: funcName)
    }

    static func s(_ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
        log(.s, object, filename: filename, line: line, column: column, funcName: funcName)
    }
}

설명:

  • 로그 레벨별 함수: 각 로그 레벨별로 호출할 수 있는 함수를 정의했습니다. 예를 들어, d()는 디버그 레벨의 로그를 출력하고, e()는 에러 레벨의 로그를 출력합니다.
  • 각 함수에서 #file, #line, #column, #function을 기본 매개변수로 사용하여 호출 위치의 정보가 자동으로 전달됩니다.

 

MARK: - 메인 로깅 함수

extension Logger {
    private static func log(_ level: LogEvent,
                            _ object: Any,
                            filename: String,
                            line: Int,
                            column: Int,
                            funcName: String) {

        let timestamp = dateFormatter.string(from: Date())
        let fileName = sourceFileName(filePath: filename)

        let logMessage = """
-------------------- LOG START --------------------
Timestamp : \(timestamp)
Level     : \(level.rawValue)
File      : \(fileName)
Line      : \(line), Column: \(column)
Function  : \(funcName)
Message   : \(object)
-------------------- LOG END ----------------------
"""

        DispatchQueue.global(qos: .utility).sync {
            print(logMessage, level)
        }
    }
}

설명:

  • 메인 로깅 함수: log() 함수는 로그 메시지를 출력하는 메인 함수로, 타임스탬프, 로그 레벨, 파일 이름, 함수 이름, 줄 번호, 컬럼 번호 등 여러 정보를 포함하여 로그를 출력합니다.
  • 동시성 제어: DispatchQueue.global(qos: .utility).sync를 사용해 멀티 스레드 환경에서도 안전하게 로그를 출력할 수 있습니다.

 

MARK: - 유틸리티 함수

extension Logger {
    // 시간 포맷팅을 위한 DateFormatter
    private static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return formatter
    }()

    // DEBUG 또는 VERBOSE 모드에서만 메시지를 출력하는 함수
    private static func print(_ message: String, _ level: LogEvent) {
        #if DEBUG || VERBOSE
        Swift.print(message)
        #endif
    }

    // 파일 경로에서 파일 이름만 추출하는 함수
    private static func sourceFileName(filePath: String) -> String {
        return (filePath as NSString).lastPathComponent
    }
}

설명:

  • 시간 포맷팅 최적화: DateFormatter는 정적으로 선언해 성능을 최적화하고, 타임스탬프를 일관되게 유지합니다.
  • 환경별 출력 제어: print() 함수는 DEBUG 또는 VERBOSE 모드에서만 메시지를 출력하도록 설정해, 릴리스 빌드에서는 로그가 출력되지 않도록 제어합니다.
  • 파일 이름 추출: 전체 파일 경로에서 파일 이름만을 추출하여 로그에 표시하는 함수입니다.

 

사용 예시

구현한 Logger 클래스는 다음과 같이 사용할 수 있습니다:

Logger.d("디버그 메시지")
Logger.e("에러 메시지")
Logger.i("정보 메시지")
Logger.w("경고 메시지")
Logger.s("심각한 에러 발생!")

 

마무리

 이 글에서는 Swift에서 커스텀 Logger를 만들면서 프로젝트의 유지보수성과 디버깅 효율을 높이는 방법을 소개했습니다. 이번에 구현한 Logger 클래스는 가독성 좋은 로그 출력을 제공하며, 타임스탬프, 파일 및 함수 위치 등 다양한 정보를 포함하고 있어 디버깅 시 유용하게 활용할 수 있습니다. Swift 프로젝트에서 체계적인 로그 시스템이 필요할 때, 이 Logger 클래스를 적용해 보세요.