Search
⚙️

[Tech] 개발 데이터 수집 플랫폼 구축하기

태그
Backend
Product
날짜
2023/11/23
작성자
소개
안녕하세요, 라포랩스 백엔드 플랫폼 그룹에서 서버 엔지니어로 일하고 있는 김영진입니다.
서버 플랫폼 팀의 목표는 스쿼드 조직이 business impact에 집중할 수 있도록 지원하는 것입니다. 그렇기에 많은 스쿼드와 소통하고 공통적으로 필요로 하는 기능을 찾아내고 해결하게 되며, 안정적이고 빠르게 고객에게 전달할 수 있도록 돕는 역할을 합니다.
이번 글에서는 스쿼드 서버 엔지니어들의 개발 과정에서 발생하는 데이터를 모으고 분석하기 위한 개발 데이터 수집 플랫폼을 구축한 경험을 공유하고자 합니다.
라포랩스 플랫폼 그룹의 서버 엔지니어들이 어떤 일을 하는지 궁금하시다면? (link)
목차

사내 라이브러리는 유저 분석을 어떻게 해야 할까

서버 플랫폼 팀은 개발 편의성을 위한 여러 도구들을 제작하여 라포랩스 서버 엔지니어에게 제공하고 있습니다. CI/CD 과정을 자동화하기 위해 custom gradle task를 gradle plugin으로 만들기도 하고, 개발 환경인 kubernetes dev cluster에 e2e 테스트 서버를 쉽게 구축하는 도구를 CLI application으로 만들어 배포하기도 합니다. 즉, 라포랩스 서버 엔지니어를 대상으로 제품을 만들고 개선하고 있습니다.
자연스럽게 저희가 만든 개발 편의성 도구들에 대한 사용 후기가 궁금해졌는데요. 가령 어떤 도구를 어떤 팀에서 활발하게 사용하는지, 어떤 옵션을 주로 활용하는지, 그리고 어떤 지점에서 실패를 겪게 되는지와 같은 정보를 알고 싶었습니다.
초기에는 구두나 메신저를 통해 피드백을 받았지만, 이 방법은 시간과 노력을 들여 저희에게 피드백을 주기를 기다려야만 하는 수동적인 방법이었습니다. 따라서, 심각한 수준의 에러가 아니면 피드백을 활발히 받기 어렵다는 문제가 있었습니다.
이러한 문제를 해결하기 위해, 개발 편의성 도구를 사용할 때마다 이벤트 정보를 발행하도록 하고, 이 정보를 수집 및 분석할 수 있는 시스템을 구축하고자 했습니다. 이는 마치 앱/웹 애플리케이션의 사용자 분석 도구와 유사한 기능을 개발 편의성 도구에도 적용하는 것과 같습니다.

Do not reinvent the wheel

먼저, 개발 편의성 도구에서 발행하는 데이터를 수집, 저장, 시각화하기 위해 어떤 방법을 사용할지에 대해 고민하였습니다.
라포랩스는 데이터를 수집하고 분석하기 위한 도구로 빅쿼리를 활발하게 사용하고 있습니다. 팀에서 다루고자 하는 데이터도 빅쿼리에 적재할 수 있다면, 분석이나 시각화에서 Data Engineer나 Data Analyst분들과의 협업도 용이해질 것 같아 빅쿼리를 저장소로 선택하였습니다.
데이터를 빅쿼리에 적재하려면, 개발 편의성 도구에서 발생하는 여러 이벤트를 받고 가공하여 빅쿼리까지 전달해 주는 서버가 필요하다고 판단했습니다. 따라서 회사 내부망에서 운영되는 자체 서버를 구축하거나, AWS Lambda와 같은 서버리스 아키텍처를 활용하려 했습니다.
기존에 구상한 데이터 수집 아키텍쳐
대략적인 방향을 잡고 팀에 공유해 보니, firebase나 google analytics에서 제공하는 기능을 이용하면 별도의 데이터 적재 파이프라인을 직접 구현하지 않아도 빅쿼리에 원하는 데이터를 적재할 수 있을지 모른다는 의견이 나왔습니다.
이미 잘 구현된 제품으로 손 대지 않고 코 풀 수 있을 것이란 기대감으로 PoC를 실행했습니다. 그 결과, firebase SDK에 의존하지 않고 특정 endpoint에 HTTP POST 요청을 하는 것만으로도 원하는 데이터를 빅쿼리에 쌓을 수 있는 방법을 찾았습니다.
최종적으로 구현한 데이터 수집 아키텍쳐

Collecting custom events with google analytics

firebase와 google analytics, bigquery 통합으로 이벤트를 수집할 수 있는 방법에 대해 자세히 소개 드리겠습니다.
firebase console에서 개발 데이터 수집용 프로젝트를 하나 생성한 뒤, 프로젝트 설정 - 통합 탭에서 google analytics 와 bigquery와의 통합 설정을 할 수 있습니다.
google analytics 페이지에서 firebase와 연동된 data stream을 확인할 수 있는데, 이 data stream의 측정 ID가 나중에 필요하며, 측정 프로토콜 API 비밀번호도 생성해 줍니다. 바로 이 google analytics의 측정 프로토콜을 이용한 HTTP 요청을 통해 원하는 데이터를 google analytics 서버로 전송하고 수집할 수 있게 됩니다.
POST /mp/collect HTTP/1.1 HOST: www.google-analytics.com Content-Type: application/json <payload_data>
YAML
복사
URL parameter (query parameter) 로 api_secret, firebase_app_id, measurement_id 를 이전 Google Analytics 설정 단계에서 확인한 값으로 설정해야 하며 아래와 같은 형태의 JSON 문자열을 request body로 넣어주면 됩니다.
실제 데이터 적재 요청이 아닌 API 호출 검증 목적으로는 debug/mp/collect path 로 호출하면 어떤 부분이 잘못되었는지 등에 대한 정보를 응답해줍니다.
{ "appInstanceId": "00000000000000000000000000000000", "userId": "김영진", "events": [ { "name": "damoacli_command_started", "params": { "version": "1.1.1", "command": "version", } } ] }
YAML
복사
appInstanceId 는 firebase app이 설치에 대한 고유 ID로 client별로 다르게 설정되고 firebase sdk 등을 이용해 값을 찾아 사용해야 하지만, firebase app/web sdk 설치 없이 바로 데이터를 수집하고 싶은 상황이므로 appInstanceId의 조건인 32자 문자열을 임의로 0000.. 으로 설정하였습니다.
events 는 name(string)과 params(object)를 필드로 가지는 event 객체의 리스트로, 발행하고자 하는 이벤트를 이곳에서 정의하면 됩니다. name으로 사용할 수 없는 예약된 이벤트 이름이 있어 이를 피해 자유롭게 설정할 수 있으며, params object의 key-value도 자유롭게 설정할 수 있습니다.
위와 같이 데이터를 담아 HTTP POST 요청을 보내면 google analytics에서 실시간으로 이벤트가 발생하는 것을 볼 수 있으며, bigquery에서도 1분 내의 시간으로 데이터가 쌓이는 것을 확인할 수 있습니다.
Google Analytics 실시간 이벤트
BigQuery 데이터

사내 라이브러리에 적용하기

HTTP POST 요청을 통해 이벤트를 쌓을 수 있도록 프로젝트 설정이 완료되었으니, 개발 편의성 도구들에서 HttpClient 등을 이용해 이벤트를 발행하도록 구현하면 됩니다.
저희 팀은 custom gradle plugin이나 cli application 등 다양한 형태의 개발 편의 기능을 제공하고 있습니다. 이때, 이 도구들을 개발하기 위한 코드는 한 프로젝트 내에서 도구별로 gradle module로 분리되어 있습니다.
이렇게 동일한 프로젝트에서 개발되어 배포되는 여러 개발 편의성 도구에서, 동일한 규격으로 이벤트가 수집될 수 있도록 devex 모듈과 DevExEvent 인터페이스를 도입했습니다.
각 도구 module은 devex module에 의존하여 DevExEvent 인터페이스를 상속받아 해당 도구에서 발행하고 싶은 이벤트를 자유롭게 정의하고, 이를 적절한 시점에 publish 할 수 있도록 구성했습니다.
아래 예시는 개발 편의성 도구 중 하나인 cli application에서 사용자 데이터를 수집하기 위한 이벤트 구조입니다.
DevExEvent는 DevExEventPublisher를 통해 이벤트 저장소로 전송됩니다. DevExEventPublisher는 publishEvent 메소드를 인터페이스로 노출하며 어떤 방식으로, 어떤 저장소에 이벤트를 발행할지에 대한 관심사는 구현체에 위임합니다.
interface DevExEventPublisher { fun publishEvent(event: DevExEvent) }
Kotlin
복사
DevExEventPublisher 인터페이스를 구현한 구현체로 google analytics로 이벤트를 전송하는 구현체인 GAEventPublisher를 구현하였습니다.
class GAEventPublisher : DevExEventPublisher { private val client = OkHttpClient() private val gson = GsonBuilder().create() private val mediaType = MediaType.parse("application/json") override fun publishEvent(event: DevExEvent) { val gaEvent = event.toGAEvent() val requestBody = RequestBody.create(mediaType, gson.toJson(gaEvent)) val request = Request.Builder() .url(URL) .post(requestBody) .build() try { client.newCall(request).execute() } catch (e: Exception) { // Handle Error } } }
Kotlin
복사
GAEventPublisher에서 publishEvent 메소드 실행 시 다양한 하위 type을 가지며 type 별로 서로 다른 프로퍼티를 가진 DevExEvent를 google analytics가 이해할 수 있는 GAEvent로 변환하는 과정이 필요합니다. params 필드에 DevExEvent subtype 별로 다른 property를 가질 수 있도록 추상 클래스로 정의하였습니다.
data class GAEvent( val appInstanceId: String = "00000000000000000000000000000000", val userId: String, val events: List<Event>, ) { data class Event( val name: String, val params: Params, ) { sealed class Params() data class CliEventParams( val version: String, val userId: String, val command: String, val additionalData: String, ) : Params() } }
Kotlin
복사
실제 DevExEvent를 GAEvent로 변환하는 책임은 GAEventFactory로 몰아두는 방식으로 구현을 마무리하였습니다.
fun DevExEvent.toGAEvent(): GAEvent? { return when (this) { is DamoaCliEvent -> this.toGAEvent() ... else -> null } } fun DamoaCliEvent.toGAEvent(): GAEvent { return with(this) { GAEvent( userId = userId, events = listOf( GAEvent.Event( name = eventName, params = GAEvent.Event.CliEventParams( version = version, userId = userId, command = command, additionalData = additionalData.toString(), ), ), ), ) } }
Kotlin
복사
이제 위와 같은 구현체를 포함한 devex 모듈을 의존한 뒤, 이벤트 발행이 필요한 곳에서 DevExEvent 객체를 생성하고, DevExEventPublisher.publishEvent() 메소드를 통해 이벤트를 발행할 수 있게 되었습니다.

한계점

google analytics를 이용한 데이터 수집에는 별도 서버 운용 없이 데이터 수집이 가능하다는 장점이 있었지만, 구조가 단순한 만큼 한계점도 있었습니다.
첫 번째로, 원하는 param에 대한 조건문을 포함하도록 bigquery에서 쿼리를 작성하는 것이 꽤 불편합니다. 빅쿼리 테이블에서 RECORD type이며 REPEATED mode인 event_params column에 이벤트 발행 시 request body 중 params 객체의 값들이 저장되게 되는데, 이럴 경우 event_params.key 각각에 접근하기 위해서는 unnest를 해주어야 합니다.
WITH command_table AS ( SELECT event_timestamp, event_date, event_name, key as event_param_key, value.string_value AS value, FROM `events_intraday_*` CROSS JOIN UNNEST(event_params) WHERE key = 'command' )
SQL
복사
command key 에 대한 value를 가져오기 위해 unnest table을 만드는 예시
두 번째로, param.value 로 문자열을 사용할 경우 100자 제한이 있습니다. google analytics로의 이벤트 발행 기능을 이용해 런타임 에러를 리포팅하는 용도로도 사용할 수 있을 것으로 기대하였으나, 100자 제한은 에러 발생 시 stack trace 등 풍부한 정보를 담아 이벤트를 발행하기에는 부적절하였습니다.
세 번째는 google analytics 측정 프로토콜이 아직 beta API인 것입니다. breaking change가 생기거나 갑자기 기능을 지원하지 않게 된다면 데이터를 수집할 다른 방안을 알아보아야 할 수 있습니다.
마지막으로 이 기능은 인터넷이 연결되어 있지 않은 상태에서는 이벤트 발행에 무조건 실패하게 되는 문제점이 있습니다. 하지만 아직까지 모든 데이터를 손실 없이 수집해야 한다는 필요성이 크지 않아, 큰 단점으로 다가오지는 않았습니다. 만약 이러한 요구사항이 필요해진다면, offline일 경우에는 local machine에 데이터를 적재하고 online 상황일 때에 데이터를 전송하는 등의 추가 구현이 필요할 수 있습니다.

마치는 말

라포랩스 서버 플랫폼 팀은 위와 같은 방법을 통해 서버 플랫폼 팀의 고객인 스쿼드 소속 서버 엔지니어들의 행동 데이터를 수집하고 분석하고 있습니다. 여러 한계점이 존재하지만, 손쉬운 설정으로 데이터 수집 파이프라인을 구축할 수 있다는 점에 만족하고 있습니다. 이러한 장점이 필요한 다른 조직에서도 적용을 검토해 보면 좋을 것 같습니다.
이 글에서 몇 번 언급 드렸던 CLI application이나 custom gradle plugin를 개발한 경험도 공유드리고 싶은데, 이 부분은 다음 기회에 전해드리도록 하겠습니다.
도움이 되셨길 바라며 긴 글 읽어주셔서 감사합니다
김영진 Kim Youngjin
라포랩스 서버 플랫폼팀 : 서버 엔지니어 ”서비스를 개발하고 운영하며 다양한 문제들을 해결하는 과정에서 항상 더 나은 코드와 아키텍처를 고민하는 백엔드 개발자입니다.”
빠르게 성장하는 서비스를 개발하고 운영하는 경험에 함께 하고 싶으시다면?