현재 프로젝트에 적용 중인 로깅 방식을 공유하려고 한다!
로깅 전략 의 필요성
로깅 전략은 어떤 로그를 어떤 방식으로 남길지, 로그를 얼마나 관리할 지 등이다.
그럼 애플리케이션에 어떤 로깅 전략이 필요할까?
아래는 가장 중요하다고 생각하는 두 가지 이다.
1. Error 로깅
네트워크 통신하는 부분에서 왜 Failure 하는지, Exception 이 어디서 어떻게 발생하는지 Message 는 무엇인지 로깅하고 싶을 수 있다. 오류가 반복된다면 fix 가 필요할 것이다.
2. 비즈니스와 연결
사용자가 어떤 flow 로 앱을 이용하는지 어떤 화면에 오래 체류하는지 체크해서 비즈니스와 연결 지어 이용할 수 있다.
로깅의 범위나 방향은 앱마다 다르다. 즉, 어떤 로깅 전략을 세울 지 앱 바 앱 이고 팀 바 팀 이다.
프로젝트에 맞는 로깅 전략을 세우는 것이 중요하다.
참고 로깅 전략 tecoble
https://tecoble.techcourse.co.kr/post/2020-07-30-use-logger/
이전에 Firebase 에 앱 등록을 마친 상태이다.
앱 등록하기
https://firebase.google.com/docs/android/setup?hl=ko
앱 등록을 마치면 기본적인 Analytics 는 동작할 것이다.
앱에서 어떤 화면에 많이 접속하는지 사용자는 몇 명인지 어떤 이벤트가 발생 중인지 등 앱에 대한 분석을 확인할 수 있다.
로깅 전략
우리 팀에서는 다른 로그보다 네트워크 오류 로깅 전략을 수립했다.
ViewModelScope 내에서 Repository 로 부터 데이터를 요청할 때 결과가 Failure 이면 로깅 하도록 했다.
이벤트 로깅 기본 세팅을 참고하여 gradle 설정이 우선이다.
https://firebase.google.com/docs/analytics/events?platform=android&hl=ko
먼저 AnalyticsHelper interface 를 정의했다. 인터페이스를 정의한 이유는 Firebase 가 아닌 다른 것을 이용해서 로깅할 수 있도록 유연하게 만들면서 동시에 단위 테스트가 로깅되지 않게 하기 위함이다. 테스트 부분은 나중에 추가로 설명하겠다.
interface AnalyticsHelper {
fun logEvent(event: AnalyticsEvent)
}
그 다음으로 구현체를 만들었다. context 를 필요로 해서 Application 단에서 미리 create 할 수 있도록하고 instance 는 앱 생명주기 전체에서 사용가능하도록 했다.
class FirebaseAnalyticsHelper private constructor(context: Context) : AnalyticsHelper {
private val firebaseAnalytics = FirebaseAnalytics.getInstance(context)
// logging 함수
override fun logEvent(event: AnalyticsEvent) {
val params = Bundle().apply {
event.extras.forEach {
putString(it.key, it.value)
}
}
firebaseAnalytics.logEvent(LOG_NAME, params)
}
companion object {
private const val LOG_NAME = "festago_log"
// 인스턴스를 저장된 가져옴
private var _instance: FirebaseAnalyticsHelper? = null
val instance: FirebaseAnalyticsHelper = _instance!!
// 인스턴스 생성
fun create(context: Context) {
if (_instance == null) {
_instance = FirebaseAnalyticsHelper(context)
}
}
}
}
Application onCreate 에서 다음과 같이 미리 초기화하고 사용한다. 앱 전역에서 사용하려면applicationContext 를 사용해서 초기화해야하는데 액티비티에서 최초 초기화하게 되면 액티비티 Context 를 넣을 수 있고 메모리 문제가 발생할 수 있기 때문이다. 또한 사용하는 곳에서 전부 context 를 넣어야한다면 context 가 없는 곳에서 로깅하지 못하는 문제도 있을 것이라 판단하였다.
ExampleApplication: Application() {
override fun onCreate() {
super.onCreate()
FirebaseAnalyticsHelper.create(applicationContext)
}
}
이제 ViewModel 을 생성할 때 인자로 함께 추가한다.
private val vm: ExampleViewModel by viewModels {
ExampleViewModelFactory(
ExampleDefaultRepository(...),
FirebaseAnalyticsHelper.instance,
)
}
그 다음 viewModelScope 내부 Result 가 failure 일 때 로깅하였다.
fun loadExamples() {
viewModelScope.launch {
ticketRepository.loadExamples()
.onSuccess { it ->
...
}.onFailure {
...
analyticsHelper.logNetworkFailure("load_Examples", it.message.toString())
}
}
}
아래와 같이 네트워크 에러 로깅을 확장함수로 만들어 사용한다.
fun AnalyticsHelper.logNetworkFailure(key: String, value: String) {
logEvent(
AnalyticsEvent(
type = "Network Failure Type",
extras = listOf(AnalyticsEvent.Param(key, value)),
),
)
}
그럼 이제 앱을 실행하고 log 를 확인할 수 있는데
로깅하고 5분정도 지나면 Realtime Analytics 에서 확인할 수 있다. 실시간이 아닌 정보들은 Analytics Dashboard 혹은 Events 에서 확인할 수 있다. 또한 Debug 기기를 등록하면 DebugView 에서도 확인 가능하다.
테스트는 어떻게 변화했을까?
class ExampleViewModelTest {
private lateinit var vm: ExampleViewModel
private lateinit var exampleRepository: ExampleRepository
private lateinit var analyticsHelper: AnalyticsHelper
...
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
ticketRepository = mockk()
analyticsHelper = mockk(relaxed = true)
vm = TicketListViewModel(ticketRepository, analyticsHelper)
}
...
mockk 라이브러리를 사용해서 외부에서 주입하는 로그를 목 객체로 만들어줬다. relaxed 를 true 로 만들어 따로 행동을 지정하지 않아도 알아서 작동하도록 하였다! 이렇게 되면 context 를 넣어 초기화할 필요도 없어지고 테스트가 Firebase 에 로깅되지도 않는다.
추후에 오류처리 방식이 변하거나(CallAdapter 등) 추가 로깅 방식이 생길 예정이고 위 코드는 리팩터링 가능성이 있다!
'우아한테크코스' 카테고리의 다른 글
내가 꿈꾸는 프로그래머로서의 삶 (0) | 2023.10.08 |
---|---|
자동 DI 라이브러리 만들기 : [우아한테크코스 5기 AN_베르] (0) | 2023.10.02 |
Kotlin coroutine 강의로 이해하기 - 2 : [우아한테크코스 5기 AN_베르] (0) | 2023.07.30 |
Kotlin coroutine 강의로 이해하기 - 1 : [우아한테크코스 5기 AN_베르] (0) | 2023.07.16 |
[안드로이드] MVP 패턴을 MVVM 패턴으로 리팩터링하기 (0) | 2023.07.09 |