내가 별로 안좋아하던게 몇몇개 있었다. 그 중 하나가 Test 코드이고, 또 다른걸로는 Logger가 있다.
최근들어 만들고 버리는 프로젝트가 아닌, 앞으로 계속 책임져야할 프로젝트를 맡게 되니 생각이 좀 달라졌다. 생각이 좀 짧았다는 생각도 많이 들었다.
오늘은 그 중 하나였던 Logger에 대해 알아보도록 한다.
print는 콘솔에 데이터를 출력한다. Logger도 사실 이와 다르지 않다. 성능상의 이점이니 뭐니 이런건 매우 의미가 없다고 생각하고, 그냥 Print를 좀더 있어 뵈게 찍는다. 라고 생각하면 좋다. 사실 그래서 별로 안좋아했는데, 체계화된 Logging이 얼마나 중요한지 많이 깨닫게 되어 이번 회사 프로젝트에 첫 도입하게 되었음.
print를 찍으면 나는 보통 그냥 이렇게 찍었다.
print(error)
이렇게 찍으면 당연히 error가 뭔지 나오겠지만, 이게 한두개가 아니다보니까 어디서난 에런지, 이게 에런지 그냥 print인지 전혀 알수가 없었다. 게다가
print("=======================")
본인은 이런 print도 즐겨 써서, 저번에 회사앱 돌리면서 콘솔창을 보니 매우 맘에 안들고 디버깅 속도에 지장이 생긴다는걸 깨달아 버렸음.
회사 앱은 BLE, Network와 연동하기 때문에, 여러 Service Layer들과 ViewModel이 소통을 해야한다. 심지어 같은 BLE, Network 더라도 내가 목적마다 여러개 만들어놨다. 그래서 복잡성이 높아졌는데, 이를 Logger로 로그를 체계화를 시켜 디버깅을 빠르게 해야겠다 라는 생각이 들었다.
적어도 내 기준에선,
라고 생각했음.
인터넷에 뒤져보면 OsLog로 열심히 구조화 해놓은게 많던데 나는 내 기준을 가지고 이 기준에 맞춰 구조화를 진행해보았다.
import OSLog
/*
MARK: - Logger System
Network: 서버(Firebase) 관련 데이터
Bluetooth: 블루투스 관련 데이터
Debug: Debugging을 위한 데이터
Data: 서버나 블루투스에서 받아온 데이터
error: 서버나 블루투스, UI등에서 발생하는 Error 데이터
*/
extension OSLog {
static let subsystem = Bundle.main.bundleIdentifier!
static let network = OSLog(subsystem: subsystem, category: "Network")
static let bluetooth = OSLog(subsystem: subsystem, category: "Bluetooth")
static let debug = OSLog(subsystem: subsystem, category: "Debug")
static let data = OSLog(subsystem: subsystem, category: "Data")
static let error = OSLog(subsystem: subsystem, category: "Error")
}
extension Utility {
enum Log {
private enum Level {
case debug
case data
case network
case bluetooth
case error(error: Case)
case custom(categoryName: String)
case defaultPrint
fileprivate var category: String {
switch self {
case .debug:
return "Debug"
case .data:
return "Info"
case .network:
return "Network"
case .bluetooth:
return "Bluetooth"
case .error:
return "Error"
case .custom(let categoryName):
return categoryName
case .defaultPrint:
return "Default"
}
}
fileprivate var osLog: OSLog {
switch self {
case .debug, .defaultPrint:
return OSLog.debug
case .data:
return OSLog.data
case .network:
return OSLog.network
case .bluetooth:
return OSLog.bluetooth
case .error:
return OSLog.error
case .custom:
return OSLog.debug
}
}
fileprivate var osLogType: OSLogType {
switch self {
case .debug, .defaultPrint:
return .default
case .data:
return .info
case .network:
return .default
case .bluetooth:
return .default
case .error:
return .error
case .custom:
return .debug
}
}
}
static private func log(_ message: Any, _ arguments: [Any], level: Level, functionName: String, fileName: String) {
#if DEBUG
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let logger = Logger(subsystem: OSLog.subsystem, category: level.category)
let logMessage = "\(message) \(extraMessage)"
switch level {
case .debug, .custom:
logger.debug("[Debug] [\(fileName) -> \(functionName)]: \(logMessage, privacy: .public)")
case .data:
logger.info("[Data] [\(fileName) -> \(functionName)]: \(logMessage, privacy: .public)")
case .network:
logger.log("[Network] [\(fileName) -> \(functionName)]: \(logMessage, privacy: .public)")
case .error(let error):
logger.error("[\(error.rawValue) Error] [\(fileName) -> \(functionName)]: \(logMessage, privacy: .public)")
case .bluetooth:
logger.debug("[Bluetooth] [\(fileName) -> \(functionName)]: \(logMessage, privacy: .public)")
case .defaultPrint:
logger.debug("\(logMessage, privacy: .public)")
}
#endif
}
static func debug(_ message: Any, _ arguments: [Any] = [], functionName: String = #function, fileName: String = #file) {
log(message, arguments, level: .debug, functionName: functionName, fileName: fileName.fileName)
}
static func data(_ message: Any, _ arguments: [Any] = [], functionName: String = #function, fileName: String = #file) {
log(message, arguments, level: .data, functionName: functionName, fileName: fileName.fileName)
}
static func network(_ message: Any, _ arguments: [Any] = [], functionName: String = #function, fileName: String = #file) {
log(message, arguments, level: .network, functionName: functionName, fileName: fileName.fileName)
}
static func error(_ message: Any, error: Case, _ arguments: [Any] = [], functionName: String = #function, fileName: String = #file) {
log(message, arguments, level: .error(error: error), functionName: functionName, fileName: fileName.fileName)
}
static func bluetooth(_ message: Any, _ arguments: [Any] = [], functionName: String = #function, fileName: String = #file) {
log(message, arguments, level: .bluetooth, functionName: functionName, fileName: fileName.fileName)
}
static func defaultPrint(_ message: Any) {
log(message, [], level: .defaultPrint, functionName: "", fileName: "")
}
}
}
인터넷에 있던 다른 코드들은 목적만을 가지고 log를 찍는 모습이 많이 보였다.
그러나 앱의 규모가 점점 커지다보니 이 log가 목적도 목적이지만 대체 어디서 나타난 log인지 알 수가 없었음.
그래서 #function과 #file을 이용하여 이 함수가 무슨 목적이고, 어디서 온놈인지, 어느 함수인지 바로 알 수 있게 되었다.
[Debug][ContentView.swift -> testFunction()]: Test입니다
이런식으로 깔끔하게 Log를 찍을 수 있게 되었다.