Timber는 확장 가능한 트리(Tree) 기반 아키텍처를 갖춘 Android용 경량 로깅 라이브러리로, 수천 개의 프로젝트에서 표준 android.util.Log를 대체했습니다. GitHub, 2024에 따르면, 이 라이브러리는 10,000개 이상의 스타를 획득했으며 10억 회 이상 설치된 애플리케이션에서 사용됩니다. Timber는 Log API의 세 가지 주요 문제(자동 태그 부재, 필수 isLoggable 확인, 호출의 정적 특성)를 해결합니다.
핵심 사항
Timber는 Jake Wharton이 2013년 표준 android.util.Log의 대안으로 만든 Android용 오픈 소스 라이브러리입니다. Timber의 핵심 아이디어는 필수 수동 태그가 있는 정적 Log API를 스택을 통해 호출 소스를 자동으로 결정하는 메커니즘으로 대체하는 것입니다.
라이브러리는 트리와 복합(Composite) 패턴 아키텍처 패턴을 기반으로 구축되었습니다. 고정된 동작을 가진 단일 Log 클래스 대신, Timber는 트리의 "숲(포레스트)"을 관리합니다. 각 트리는 콘솔, 파일, Crashlytics, 원격 서버 등 자체 출력 채널을 담당합니다. 개발자는 원하는 수의 트리를 추가하고 결합할 수 있습니다.
Google I/O 2019에 따르면, Timber는 Google에서 Android 애플리케이션 로깅의 모범 사례로 권장됩니다. 이 라이브러리는 APK에서 10KB 미만을 차지하며 외부 종속성이 없어 모든 규모의 프로젝트에 이상적인 선택입니다.
Timber는 대규모 팀에서 일관되지 않은 태그 문제를 해결합니다. 각 개발자가 수동으로 태그를 작성하면 오타와 불일치가 불가피합니다. 한 클래스는 "MainActivity"로 기록되고 다른 클래스는 "MAIN_ACTIVITY"로 기록됩니다. Timber는 클래스 이름에서 자동으로 태그를 파생합니다: MainActivity.kt → 태그 MainActivity.
아키텍처는 중앙 정적 클래스 Timber와 추상 클래스 Timber.Tree의 두 가지 구성 요소로 이루어져 있습니다. Timber는 파사드 역할을 하여 각 로그 호출을 심어진 모든 트리에 위임합니다. 각 트리는 메시지를 처리할지 여부와 처리할 경우 어디로 보낼지 결정합니다.
DebugTree는 라이브러리에 포함된 표준 Tree 구현입니다. 호출 스택을 분석하여 태그를 결정합니다. Timber.d() 호출 지점에서 8프레임 위로 올라가 로그 메서드를 호출한 클래스 이름을 찾습니다. DebugTree는 BuildConfig.DEBUG를 확인하므로 릴리스 빌드에서 자동으로 비활성화됩니다(아무것도 출력하지 않음).
Forest(포레스트) — 심어진 모든 트리의 모음입니다. Timber.d("message") 메서드가 호출되면 라이브러리는 심어진 순서대로 모든 트리에 메시지를 반복적으로 전달합니다. 각 트리는 레벨, 태그 또는 내용에 따라 메시지를 필터링하고 자체 방식으로 처리할 수 있습니다.
심는 순서가 중요합니다. 먼저 심어진 트리가 먼저 처리됩니다. 커스텀 트리(예: Crashlytics)가 Logcat에 도달하기 전에 메시지를 처리할 수 있도록 DebugTree를 마지막에 심는 것이 좋습니다.
Timber는 스레드 안전합니다. 모든 메서드는 내부 잠금을 통해 동기화됩니다. 이렇게 하면 서로 다른 스레드의 메시지가 혼합되지 않습니다. 그러나 커스텀 트리 내부에서는 동기화가 개발자의 책임입니다. 트리가 파일에 쓰는 경우 synchronized 또는 ReentrantLock을 사용해야 합니다.
// Application.onCreate에서 트리 포레스트 초기화
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
Timber.plant(CrashReportingTree())
Timber.plant(FileLoggingTree())
Timber.i("Timber planted with 3 trees")
}
}
설치는 build.gradle에 하나의 종속성을 추가하여 수행됩니다. 라이브러리는 Maven Central에 com.jakewharton.timber:timber 아티팩트로 게시되어 있습니다. 2024년 기준 현재 버전은 5.0.1이며, 최신 안정 업데이트입니다.
// build.gradle (Module: app)
dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
}
설치 후 최소 설정 — Application.onCreate에서 DebugTree를 심습니다. 이 단계가 없으면 Timber는 예외를 발생시키지 않고 모든 로그 호출을 무시합니다. 이것은 안전한 기본 동작입니다. 트리가 심어지지 않으면 라이브러리는 최소한의 오버헤드로 유휴 상태로 실행됩니다.
Jake Wharton, 2023에 따르면, 새 사용자의 Timber 문제 중 70%는 초기화를 잊었거나 잘못되었기 때문입니다. 트리가 없을 때 Timber는 오류를 생성하지 않습니다. 개발자는 Logcat에 로그가 나타나기를 기대하지만 아무 일도 일어나지 않습니다.
테스트를 위해 Timber는 Timber.asTree()를 제공합니다. 현재 트리 또는 null을 반환하는 메서드입니다. 이는 단위 테스트에 편리합니다. 트리를 모의 객체로 대체하고 로그 메시지가 올바른 수준과 태그로 전송되었는지 확인할 수 있습니다.
커스텀 트리 — 표준 Log API 대신 Timber를 사용하는 주요 이유입니다. Tree 메서드를 재정의하여 모든 수준의 로그를 Crashlytics, 파일 시스템, Remote Config 또는 자체 서버로 라우팅할 수 있습니다.
class CrashReportingTree : Timber.Tree() {
override fun isLoggable(tag: String?, priority: Int): Boolean {
// 크래시 리포팅용 Error 및 WTF만
return priority >= Log.ERROR
}
override fun log(priority: Int, tag: String?,
message: String, t: Throwable?) {
if (t != null) {
FirebaseCrashlytics.getInstance()
.recordException(t)
} else {
FirebaseCrashlytics.getInstance()
.log("[$tag] $message")
}
}
}
재정의할 메서드: isLoggable(tag, priority) — 메시지를 처리할지 결정하는 필터입니다(기본 구현은 true 반환). log(priority, tag, message, t) — 주요 처리 로직입니다. prepareLog(priority, tag, throwable, message, args) — 포맷팅 전에 호출되며, 처리 전에 메시지를 수정할 수 있습니다.
커스텀 트리의 중요한 장점은 리플렉션이 없다는 것입니다. 많은 로깅 프레임워크와 달리 Timber는 태그나 수준을 결정하기 위해 Reflection API를 사용하지 않습니다. 태그는 호출 스택(Throwable.stackTrace) 분석을 통해 계산되며, 훨씬 더 빠르게 작동합니다.
비교 Timber와 표준 Log API는 네 가지 주요 차이점을 보여줍니다: 자동 태그, varargs 문자열 포맷 지원, 여러 출력 채널, 초기화 없이 안전한 동작.
| 매개변수 | android.util.Log | Timber |
|---|---|---|
| 태그 감지 | 수동, 문자열 상수 | 자동, 호출 스택을 통해 |
| 포맷팅 | 연결 또는 String.format | 내장 varargs + %s 플레이스홀더 |
| 출력 채널 | Logcat만 | 트리: Logcat, 파일, Crashlytics 등 |
| 초기화 없는 동작 | 항상 작동 | 아무것도 출력하지 않음 |
| 성능 | 기본 수준 | isLoggable을 통한 지연 포맷팅 |
Timber에 대한 주요 반대 의견 — 타사 라이브러리에 대한 의존성입니다. 최소한의 로깅이 필요한 간단한 프로젝트의 경우 Timber 사용이 과도할 수 있습니다. 그러나 Google Play Console, 2024에 따르면 Google Play 상위 1000개 앱의 60% 이상이 Timber를 사용하여 신뢰성과 효율성을 입증하고 있습니다.
릴리스 빌드에서 Timber의 성능은 표준 Log API와 동등합니다. 트리가 심어지지 않은 경우 Timber.d() 메서드는 트리 존재 여부를 확인하고(하나의 if) 문자열 포맷 없이 반환됩니다. 이는 항상 실행되는 연결을 사용하는 Log.d()보다 빠릅니다.
첫 번째 규칙 — 테스트에서 항상 Timber 초기화를 확인하세요. Timber.asTree()를 사용하여 트리가 심어졌는지 확인합니다. 단위 테스트에서는 assert 검사를 위해 메시지를 목록에 저장하는 TestTree를 심습니다.
두 번째 규칙 — 같은 프로젝트에서 Timber와 android.util.Log를 혼용하지 마세요. 프로젝트가 이미 Timber를 사용하는 경우 모든 새 로그 호출은 Timber를 통해 이루어져야 합니다. 혼용하면 메시지가 중복되고 분석 시 혼란을 초래합니다.
세 번째 규칙 — BuildConfig.DEBUG를 확인하지 않고 CrashReportingTree를 심습니다. DebugTree와 달리 크래시 트리는 디버그와 릴리스 모두에서 작동해야 합니다. 이를 통해 테스트 오류도 크래시 리포팅 시스템에 포착됩니다.
네 번째 규칙 — 내장 Timber 수준을 사용하세요: Timber.v(), Timber.d(), Timber.i(), Timber.w(), Timber.e(), Timber.wtf(). 숫자 우선순위로 Timber.log()를 직접 호출하지 마세요. 코드 가독성이 떨어지고 리팩토링이 복잡해집니다.
다섯 번째 규칙 — 라이브러리 및 모듈의 경우 Timber.tag("CustomTag")를 사용하세요. 이 메서드는 전역 설정에 영향을 주지 않고 재정의된 태그가 있는 임시 트리를 반환합니다. 이를 통해 사용자 정의 식별자로 라이브러리 코드에서 로깅할 수 있습니다.
자주 묻는 질문
네 — Timber는 라이브러리에서 안전하게 사용할 수 있습니다. 애플리케이션에 트리가 심어지지 않은 경우 Timber 호출은 오류를 발생시키지 않습니다. 라이브러리의 경우 로그 소스를 식별하기 위해 Timber.tag("LibraryTag")를 사용하는 것이 좋습니다.
호출 스택(stack trace)을 통해 — DebugTree는 Timber.d() 호출 지점에서 8프레임 위로 올라가 클래스 이름을 추출합니다. Throwable.stackTrace 메서드는 Reflection API 오버헤드 없이 호출 클래스를 결정하는 데 사용됩니다.
Logcat은 로그를 보기 위한 Android 시스템 유틸리티입니다. Timber는 로그를 작성하기 위한 라이브러리입니다. Timber는 DebugTree를 통해 Logcat에 메시지를 출력하지만, 커스텀 트리를 통해 파일, Crashlytics, Sentry 및 기타 채널로 보낼 수도 있습니다.
아니요 — Timber는 Android SDK(android.util.Log)에 종속됩니다. KMP 프로젝트의 경우 Kermit 또는 Napier를 고려하세요. 이들은 유사한 트리 아키텍처를 가진 멀티플랫폼 로깅 라이브러리로 Android, iOS, JVM 및 JS에서 작동합니다.
Timber.uprootAll()을 사용하세요. 이 메서드는 등록된 모든 트리를 제거합니다. Timber.uproot(tree)는 특정 트리를 제거합니다. 이는 테스트 메서드 간 상태를 재설정하기 위해 테스트에서 유용합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.