티스토리 뷰

 

 

버그는 반드시 수정해야 한다. 그중에서도 재현하기 어려운 버그는 쉽게 손을 댈 수도 없다. 

이때 로그를 사용하면 재현하지 않고도 버그의 흐름을 파악할 수 있도록 한다.

 

그래서 Swift의 통합 로깅 API인 Logger에 대해서 알아보도록 하자.

 

 

📚 Logger란?

 

Swift에서의 Logger는 구조화된 로깅 api의 일부이며 Swift의 concurrency 및 디버깅과 잘 통합되도록 설계된 로깅 시스템이다.

 

Logger는 os 프레임워크 기반으로 만들어졌으며 os 프레임워크 기능을 더 swift하게 사용할 수 있도록 추상화된 형태로 iOS 14.0부터 도입되었다.

공식문서를 살펴보면 apple에서는 디버깅을 할 때 print보다 Logger를 권장하고있다. 기본적으로 성능적인 측면에서 Logger가 성능 최적화 되어 있다고 한다.

 

Logger는 iOS 14.0 이상부터 사용할 수 있지만, 기반은 os_log에 있기 때문에 그 이하 버전을 개발한다면 os_log을 사용하면 될 거 같다.

 

이외에도 로그는 데이터 저장을 level에 따라 분류하여 메모리와 디스크의 데이터 저장소에 모으게 된다.

 

Log Level 디스크 저장 여부 성능
Debug 아주 낮음 (거의 없음)
Info ⚠️ log tool로 수집할 때만 저장 낮음
Default 중간
Fault 가장 성능 부하가 큼
Error 중간

 

  • Debug : 개발 중 유용한 정보를 기록한다. (디버깅에 유용한 메세지)
  • Info : 문제 해결에 도움은 되지만 필수는 아닌 정보를 기록한다. (주로 상태 확인용 로그)
  • Default: 문제 해결에 꼭 필요한 정보를 기록한다. (실패로 이어질 수 있는 조건, 중요한 정보)
  • Fault: 코드 실행 중 발생한 오류를 기록한다.
  • Error: 심각한 오류나 버그를 기록한다.

 

 

어떤 상황에 어떤 레벨의 로그를 써야 할지 감이 잘 안 올 수도 있다.

 

그중에서도 ErrorFault에 대해서 제일 헷갈릴 거 같아서 이 두 부류에 대해 짧게 작성해보고자 한다.

 

예를 들어 API통신 코드를 작성했을 때 실패했을 경우에 대한 로그를 작성하려 한다면 Error를 사용하는 것을 권장한다.

우리가 코드 구성을 할 때 "이 부분은 API실패에 해당하는 로그야". 하고 이미 예상된 실패에 대해서 코드를 작성하기 때문에 어떤 필드를 누락해서 실패했는지, 혹은 어떤 원인으로 통신에 실패했는지를 알기 쉽다

 

반면 Fault는 어떤 기능을 실행했을 때 시뮬레이터가 꺼지는 경우, 혹은 일정 기능이 아예 동작하지 않아서 앱을 종료시키고 다시 킨 후에만 작동되는 방식이라면 예상하지 못한 버그지만, 사용자 경험에 심각한 문제를 야기하는 경우에 사용하는 것을 권장한다.

Fault는 로그가 반드시 디스크에 기록되며 전후 맥락을 파악하여 어떤 부분이 문제인지를 알기가 쉽기 때문이다.

 

반면에 Fault는 Log level 중에서 가장 오래 저장되기 때문에(일반적으로 로그가 며칠간 유지) 성능 측면에서도 부하가 제일 크다. 따라서 Fault는 신중하게 사용하는 것이 중요..!

 

주로 이런 건 팀원들과 상의한 후 결정되지만, 개인프로젝트를 통해서 적용할 수도 있으니 참고정도만 하면 좋을 거 같다.

 

🤔 print vs Logger

 

1. 로그는 바로 문자열로 반환되지 않는다.

print와 로그는 둘 다 콘솔을 통해서 내용을 출력한다는 점에선 똑같다. print는 print("123243)" 라고 작성하면 바로 문자열로 반환해서 출력된다. 그렇기 때문에 출력이 되든 말든 무조건 문자열 연산이 한번 발생하기 때문에 성능 손실이 생기기 쉽다.


로그 데이터는 최적화된 내부 표현으로 저장되며 필요할 때만 문자열로 변환된다. String이나 객체처럼 개인정보가 포함될 수 있는 값은 기본적으로 숨겨지지만, public 옵션을 추가하면 로그에 실제 값이 보이게 할 수 있으므로 디버깅용으로 확인하기 쉬운 정보들을 빠르게 확인하면서 민감한 정보는 기본적으로 가려지게 끔 활용도 있게 개발할 수 있다는 장점이 있다.

 

성능 최적화 면에서도 로그는 print처럼 무작정 실행하는 게 아니라, 조건을 판단한 후 필요할 때만 문자열로 변환되기 때문에 성능측면에서도 더 최적화되어 있다.

 

 

2. 구조적으로 출력 가능하다.

통합로깅시스템을 이용하면 xcode를 실행하지 않아도 console 앱을 실행시켜서 로깅이 가능하다. 

 

 

 

로그는 console App을 통해 로그 레벨에 따라 유형과 각각의 로그에 해당하는 시간, 메시지 등 더 많은 정보를 확인할 수 있고 디버깅 레벨에 따라 유형에 대한 색상 표시로 한눈에 어떤 디버깅 레벨에 해당하는지에 대한 정보를 알기 쉽다.

 

print는 단순한 stdout이기 때문에 입력한 대로 뱉어내는 출력이다. 그래서 필터링, 레벨, 보안 기능이 존재하지 않는다.

 

✏️ 사용방법

1. Framworks, Libraries .. 에서 +를 눌러 OSLog 프레임워크 추가

 

Logger는 앞서 말했든 osLog 프레임워크 기반으로 새로 생긴 것이기 때문에 Logger를 사용하기 위해선 osLog 프레임워크를 추가시켜줘야 한다.

 

(로그를 사용하는 모든 파일에서 import os를 해주면 프레임워크를 추가해주지 않아도 되지만, 누락될 수도 있고 어느 세월에 그걸 다 .. 아무튼 이런 이유로 프레임워크를 미리 추가시켜 주자!)

 

2. Build Phases에서 optional로 상태 변경해 주기

 

optional로 설정해 준 이유는 혹시나 어떤 iOS 버전에서 이 프레임워크가 존재하지 않을 수도 있기 때문에 이런 문제를 방지하고자 optional로 지정해 주었다. (혹시나 이유 모를 에러와 버그에 대한 방지수단)

 

3. Extension으로 Logger 관련 파일 관리하기

 

로그를 작성할 때마다 os_log("", log: , type: ) 이런 식으로 타입을 지정해야 하면 번거롭기도 하고, 타입 실수를 해서 잘못된 디버깅 정보를 알려줄 수도 있다. 

 

이런 혼란을 방지하기 위해서 extension으로 파일을 관리하는 게 유지보수성에도 활용도가 높기 때문에 사용해보고자 한다.

 

 

3-1. Log Level 관리하기

 

앞서 Log level은 debug, info, warning, fault, error 총 5가지의 레벨로 나누어진다고 했다!

 

로그 자체는 어떤 상태를 가질 필요가 없기 때문에, 의도된 방식으로만 사용하면 된다. 따라서 Log Level을 enum 타입으로 맵핑해 주는 과정이 필요하다!

 

enum Log {
    /// # Level
    /// - debug : 디버깅 로그
    /// - info : 시스템 상태 파악 로그
    /// - warning: 경고에 대한 정보 기록
    /// - fault : 실행 중 발생하는 버그
    /// - error :  심각한 오류
    enum Level: CaseIterable {
        case debug
        case info
        case warning
        case fault
        case error

        fileprivate var category: String {
            switch self {
            case .debug:
                return "⌨️ DEBUG"
            case .info:
                return "ℹ️ INFO"
            case .warning:
                return "⚠️ WARNING"
            case .fault:
                return "🚫 FAULT"
            case .error:
                return "❌ ERROR"
            }
        }
    }
...
}

 

각각의 category를 묶어준 다음에 콘솔에서 확인하기 쉽도록 로그레벨에 맞는 이모지와 함께 맵핑하는 과정을 거치도록 하였다.

 

3-2. Logger 인스턴스 캐싱

 

Logger는 내부적으로 리소스를 사용하기 때문에 매번 생성하는 것보단 캐싱을 해서 불필요한 생성을 방지하는 것이 효율성과 성능 측면에서 좋다. 

 

그래서 로그 레벨마다 별도의 Logger를 한 번만 생성하고 재사용하도록 해서 loggers에 딕셔너리로 저장되게 한다.

    // MARK: - Logger 캐시
    private static let subsystem = Bundle.main.bundleIdentifier ?? "yanni13.WiggleSnake"
    private static var loggers: [Level: Logger] = {
        var result: [Level: Logger] = [:]
        for level in Level.allCases {
            result[level] = Logger(subsystem: subsystem, category: level.category)
        }
        return result
    }()

 

3-3. 메시지 로깅

 

apple의 osLog Privacy는 로그 메시지를 출력할 때 개인 정보나 민감한 데이터인지 아닌지를 지정할 수 있다고 한다. 실제로 apple은 이 방식을 권장하고 있으며 계정 정보나 개인정보를 숨기는 데 사용할 수 있다.  

 

시스템 자체에 로그 수집이나 로그를 저장할 때 개인정보 유출을 방지할 수 있도록 도와준다.

 

public은 주로 로그 메시지가 단순한 디버깅 정보나, 상태 메시지에 적합하며 이외에는 디폴트 값을 private로 설정하는 것이 안전할 것이다.

 

특히 회원가입 필드에서 사용자의 이메일, 전화번호, 실명 등은 모두 private로 지정해야 하며 토큰 같은 것도 로그가 저장될 수 있으니 private로 지정해야 한다.

 

    private static func log(_ message: Any, level: Level) {
        guard let logger = loggers[level] else { return }
        let logMessage = "\(level.category): \(message)" // 메시지에 카테고리 포함

        switch level {
        case .debug:
            logger.debug("\(logMessage, privacy: .public)")
        case .info:
            logger.info("\(logMessage, privacy: .public)")
        case .warning:
            logger.warning("\(logMessage, privacy: .private)")
        case .fault:
            logger.fault("\(logMessage, privacy: .private)")
        case .error:
            logger.error("\(logMessage, privacy: .private)")
        }
    }

 

debug나 info는 주로 개발자가 개발에 필요한 어떠한 정보를 얻기 위해 사용하므로 public으로 설정하였고 이외의 level은 privacy로 처리하여 민감한 정보나 개인정보를 보호하도록 하였다.

 

 

3-4. extension으로 파일 관리

 

다른 뷰나 뷰모델에서 Log.debug("") 이런 식으로 사용하기 위해서 extension을 통해 관리해주어야 한다.

 

콘솔에서 식별하기 쉽도록 level별로 구분해 주도록 하였다.

 

// MARK: - utils

extension Log {
    /// # debug
    /// - Note : 개발 중 코드 디버깅 시 사용할 수 있는 유용한 정보
    static func debug(_ message: Any) {
        log(message, level: .debug)
    }

    /// # info
    /// - Note : 문제 해결시 활용할 수 있는, 도움이 되지만 필수적이지 않은 정보
    static func info(_ message: Any) {
        log(message, level: .info)
    }

    /// # warning
    /// - Note : 경고에 대한 정보, 잠재적으로 문제가 될 수 있는 상황
    static func warning(_ message: Any) {
        log(message, level: .warning)
    }

    /// # fault
    /// - Note : 실행 중 발생하는 버그나 잘못된 동작
    static func fault(_ message: Any) {
        log(message, level: .fault)
    }

    /// # error
    /// - Note : 심각한 오류나 예외 상황
    static func error(_ message: Any) {
        log(message, level: .error)
    }
}

 

 

 

⬇️ 전체 코드

더보기
import Foundation
import os.log

// MARK: - Log

enum Log {

    /// # Level
    /// - debug : 디버깅 로그
    /// - info : 시스템 상태 파악 로그
    /// - warning: 경고에 대한 정보 기록
    /// - fault : 실행 중 발생하는 버그
    /// - error :  심각한 오류

    enum Level: CaseIterable {
        case debug
        case info
        case warning
        case fault
        case error

        fileprivate var category: String {
            switch self {
            case .debug:
                return "⌨️ DEBUG"
            case .info:
                return "ℹ️ INFO"
            case .warning:
                return "⚠️ WARNING"
            case .fault:
                return "🚫 FAULT"
            case .error:
                return "❌ ERROR"
            }
        }
    }

    // MARK: - Logger 캐시

    private static let subsystem = Bundle.main.bundleIdentifier ?? "yanni13.WiggleSnake"
    private static var loggers: [Level: Logger] = {
        var result: [Level: Logger] = [:]
        for level in Level.allCases {
            result[level] = Logger(subsystem: subsystem, category: level.category)
        }
        return result
    }()

    private static func log(_ message: Any, level: Level) {
        guard let logger = loggers[level] else { return }

        let logMessage = "\(level.category): \(message)" // 메시지에 카테고리 포함

        switch level {
        case .debug:
            logger.debug("\(logMessage, privacy: .public)")
        case .info:
            logger.info("\(logMessage, privacy: .public)")
        case .warning:
            logger.warning("\(logMessage, privacy: .private)")
        case .fault:
            logger.fault("\(logMessage, privacy: .private)")
        case .error:
            logger.error("\(logMessage, privacy: .private)")
        }

    }

}

// MARK: - utils

extension Log {
    /// # debug
    /// - Note : 개발 중 코드 디버깅 시 사용할 수 있는 유용한 정보

    static func debug(_ message: Any) {
        log(message, level: .debug)
    }

    /// # info
    /// - Note : 문제 해결시 활용할 수 있는, 도움이 되지만 필수적이지 않은 정보

    static func info(_ message: Any) {
        log(message, level: .info)
    }

    /// # warning
    /// - Note : 경고에 대한 정보, 잠재적으로 문제가 될 수 있는 상황

    static func warning(_ message: Any) {
        log(message, level: .warning)
    }

    /// # fault
    /// - Note : 실행 중 발생하는 버그나 잘못된 동작

    static func fault(_ message: Any) {
        log(message, level: .fault)
    }

    /// # error
    /// - Note : 심각한 오류나 예외 상황

    static func error(_ message: Any) {
        log(message, level: .error)
    }
}

 

 

 


참고

https://sunidev.tistory.com/77

https://developer.apple.com/documentation/os/logger

https://ios-development.tistory.com/381

https://developer.apple.com/kr/videos/play/wwdc2020/10168/

 

'iOS > Swift' 카테고리의 다른 글

[swift] Swift Concurrency - Task (2)  (2) 2025.06.19
[swift] Swift Concurrency - Task (1)  (0) 2025.06.18
[iOS] @MainActor와 DispatchQueue의 차이  (0) 2025.03.06
[iOS/Swift] @escaping closure  (1) 2024.09.26
[iOS] lazy var에 대해 알아보기  (0) 2024.09.21
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함