Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Near/app/src/main/java/com/alarmy/near/utils/logger/NearLog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.alarmy.near.utils.logger

import android.util.Log
import com.alarmy.near.BuildConfig

private const val TAG = "Near"
private const val STACK_TRACE_INDEX = 5

/**
* 호출자 정보를 포함한 로그 메시지를 생성합니다
* 스택 트레이스 추출에 실패하면 원본 메시지를 반환합니다
*/
private fun buildLogMessage(message: String): String {
return try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼하게 처리주셨네요👍
예외가 발생하면 어떤 상황에서 발생할까요? 만약 예외가 발생하고 catch되어 message로 나오는 상황이면 어떤 예외인지,어쩌면 예외가 발생했는지도 모르게 다른 부분을 디버깅 하는 상황이 생길 수 있을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코멘트 감사합니다 :D
우선 예외가 발생하는 경우는 로그에 해당 파일까지 남기는 형태로 작성하였는데, 파일을 찾는 방식이
앱의 스택 트레이스를 순회하면서 탐색하고 있습니다! 그래서 이 순회과정에서 문제가 생길 수 있어 try~catch로 예외처리를 두었어요

예외처리를 두긴 하였지만 웬만하면 에러가 발생 안하지 않을까.. 싶습니다

val stackTrace = Thread.currentThread().stackTrace

// 실제 호출자를 찾기 위해 스택을 순회
for (i in 4 until stackTrace.size) {
val element = stackTrace[i]
val fileName = element.fileName ?: continue
val methodName = element.methodName ?: continue

// 로그 관련 메서드들을 건너뛰고 실제 호출자 찾기
if (!methodName.startsWith("log") &&
!methodName.contains("\$default") &&
!fileName.contains("Log")
) {
val cleanFileName =
fileName
.replace(".java", "")
.replace(".kt", "")

return "[$cleanFileName::$methodName (${element.fileName}:${element.lineNumber})] $message"
}
}

// 찾지 못하면 기본 인덱스 사용
if (stackTrace.size > STACK_TRACE_INDEX) {
val element = stackTrace[STACK_TRACE_INDEX]
val fileName =
element.fileName
?.replace(".java", "")
?.replace(".kt", "")
?: "Unknown"

"[$fileName::${element.methodName} (${element.fileName}:${element.lineNumber})] $message"
} else {
message
}
} catch (exception: Exception) {
// 스택 트레이스 추출 실패 시 원본 메시지 반환
message
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

buildLogMessage 함수에서 호출자 정보를 찾기 위해 스택 트레이스를 사용하는 로직이 불안정하고 잠재적인 버그를 포함하고 있어 개선이 필요합니다.

  • 하드코딩된 인덱스: for (i in 4 until stackTrace.size)STACK_TRACE_INDEX = 5 처럼 스택의 특정 인덱스를 사용하면, 코드의 변화(예: 인라인 함수, 람다 사용)에 따라 스택 구조가 바뀌었을 때 호출자를 정확히 찾지 못하게 됩니다. 현재 로직으로는 실제 호출자인 3번 인덱스를 건너뛰게 되어 잘못된 정보가 로깅될 수 있습니다.
  • 취약한 파일 이름 필터링: !fileName.contains("Log") 조건은 파일 이름에 "Log"가 포함된 다른 클래스(예: LoginViewModel)의 로그까지 건너뛸 위험이 있습니다. 로거 파일 자체(NearLog.kt)만 명시적으로 제외하는 것이 더 안전합니다.
  • 파일 확장자 제거 방식: .replace(".java", "").replace(".kt", "") 방식은 .java.kt가 파일명 중간에 들어가는 경우 오작동할 수 있으며, 다른 확장자를 가진 파일에는 대응하기 어렵습니다. substringBeforeLast('.')를 사용하는 것이 더 안정적입니다.

이러한 문제들을 해결하기 위해, 스택 트레이스를 순회하며 현재 로깅 파일(NearLog.kt)이 아닌 첫 번째 요소를 찾아 정보를 가공하는 방식으로 수정하는 것을 제안합니다. 이 방식이 훨씬 더 안정적이고 유지보수하기 좋습니다.

추가적으로, Thread.currentThread().stackTrace는 호출될 때마다 스택 전체를 순회하므로 약간의 성능 저하를 유발할 수 있습니다. 하지만 디버그 빌드에서만 사용되므로 허용 가능한 트레이드오프일 수 있습니다. 이 점을 인지하고 계시면 좋을 것 같습니다.

    return try {
        val stackTrace = Thread.currentThread().stackTrace
        // 스택 트레이스에서 현재 로거 파일을 제외한 첫 번째 호출 지점을 찾습니다.
        // stackTrace[0] = Thread.getStackTrace
        // stackTrace[1] = buildLogMessage
        // stackTrace[2] = logd, logi, ...
        // 따라서 3번 인덱스부터 검색을 시작하는 것이 합리적입니다.
        val callerElement = stackTrace.drop(3).firstOrNull {
            it.fileName != "NearLog.kt"
        }

        if (callerElement != null) {
            val fileName = callerElement.fileName
            val cleanFileName = fileName?.substringBeforeLast('.') ?: "Unknown"
            "[$cleanFileName::${callerElement.methodName} (${fileName}:${callerElement.lineNumber})] $message"
        } else {
            message
        }
    } catch (exception: Exception) {
        // 스택 트레이스 추출 실패 시 원본 메시지 반환
        message
    }

}

/**
* 디버그 모드인지 확인합니다
*/
private fun isLoggingEnabled(): Boolean = BuildConfig.DEBUG

// Verbose 로그
fun logv(
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.v(tag, buildLogMessage(message))
}

// Debug 로그
fun logd(
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.d(tag, buildLogMessage(message))
}

// Info 로그
fun logi(
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.i(tag, buildLogMessage(message))
}

// Warning 로그
fun logw(
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.w(tag, buildLogMessage(message))
}

// Error 로그
fun loge(
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.e(tag, buildLogMessage(message))
}

// Error 로그 (이름 포함)
fun loge(
name: String,
message: String,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.e(tag, buildLogMessage("$name: $message"))
}

// Error 로그 (예외 포함)
fun loge(
message: String,
throwable: Throwable,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.e(tag, buildLogMessage(message), throwable)
}

// Error 로그 (이름과 예외 모두 포함)
fun loge(
name: String,
message: String,
throwable: Throwable,
tag: String = TAG,
) {
if (!isLoggingEnabled()) return
Log.e(tag, buildLogMessage("$name: $message"), throwable)
}