우아한테크코스

[안드로이드] App 로깅 전략 with Firebase Analytics [우아한테크코스 5기 AN_베르]

베르_최성훈 2023. 8. 6. 16:57

현재 프로젝트에 적용 중인 로깅 방식을 공유하려고 한다!

 

로깅 전략 의 필요성

로깅 전략은 어떤 로그를 어떤 방식으로 남길지, 로그를 얼마나 관리할 지 등이다. 

 

그럼 애플리케이션에 어떤 로깅 전략이 필요할까?

아래는 가장 중요하다고 생각하는 두 가지 이다. 

 

1. Error 로깅

  네트워크 통신하는 부분에서 왜 Failure 하는지, Exception 이 어디서 어떻게 발생하는지 Message 는 무엇인지 로깅하고 싶을 수 있다. 오류가 반복된다면 fix 가 필요할 것이다.

 

2. 비즈니스와 연결

사용자가 어떤 flow 로 앱을 이용하는지 어떤 화면에 오래 체류하는지 체크해서 비즈니스와 연결 지어 이용할 수 있다.

 

로깅의 범위나 방향은 앱마다 다르다. 즉, 어떤 로깅 전략을 세울 지 앱 바 앱 이고 팀 바 팀 이다.

프로젝트에 맞는 로깅 전략을 세우는 것이 중요하다.

 

참고 로깅 전략 tecoble 

https://tecoble.techcourse.co.kr/post/2020-07-30-use-logger/

 

로그 전략을 통해 메시지를 남기자

tecoble.techcourse.co.kr

 

이전에 Firebase 에 앱 등록을 마친 상태이다. 

 

앱 등록하기

https://firebase.google.com/docs/android/setup?hl=ko

 

Android 프로젝트에 Firebase 추가  |  Firebase for Android

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 Android 프로젝트에 Firebase 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세

firebase.google.com

 

앱 등록을 마치면 기본적인 Analytics 는 동작할 것이다.

앱에서 어떤 화면에 많이 접속하는지 사용자는 몇 명인지 어떤 이벤트가 발생 중인지 등 앱에 대한 분석을 확인할 수 있다.

 

로깅 전략

우리 팀에서는 다른 로그보다 네트워크 오류 로깅 전략을 수립했다.

ViewModelScope 내에서 Repository 로 부터 데이터를 요청할 때 결과가 Failure 이면 로깅 하도록 했다.

 

이벤트 로깅 기본 세팅을 참고하여 gradle 설정이 우선이다.

https://firebase.google.com/docs/analytics/events?platform=android&hl=ko 

 

이벤트 로깅  |  Google Analytics for Firebase

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 이벤트 로깅 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. iOS+ Android 웹 Fl

firebase.google.com

 

  먼저 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 등) 추가 로깅 방식이 생길 예정이고 위 코드는 리팩터링 가능성이 있다!