기존 문제점
이벤트 하나를 BigQuery까지 보내려면 클라이언트와 데이터 엔지니어링 양쪽이 각각 선언을 해야 한다. 문제는 이 둘이 어긋나도 아무 에러가 나지 않는다는 점이다.
상황
우리 서비스는 Firebase SDK로 이벤트를 로깅하고, 이 이벤트가 BigQuery 테이블까지 흘러간다. 코드로 살펴보면 다음과 같다.
firebaseTagging('place_detail_page_view', {
id: props.placeId,
type: props.placeType,
status: props.isAvailable ? 'TRUE' : 'FALSE',
traffic_source: trafficSource,
})
장소 상세페이지에 진입했을 때 적재하는 place_detail_page_view 이벤트이며 그 장소의 아이디, 타입, 상태 등의 정보도 함께 로깅을 남긴다.
우리 클라이언트 개발자(앱/웹)는 단순히 ‘로깅’만 하면 끝이지만 그 이전과 이후의 흐름은 생각보다 복잡하다. 이벤트 적재의 큰 흐름을 먼저 살펴보자.
이벤트 적재의 흐름
이벤트 설계 담당자 입장에서의 이벤트 적재 흐름은 다음과 같다.
- 기획서/분석 니즈 기반으로 필요한 이벤트를 파악한다.
- 앱 플로우를 따라가며 기존 이벤트 현황을 확인한다.
- 추가 정의가 필요한 이벤트를 노션에 작성한다.
- 관리자 컨펌을 받는다.
- 협업 채널에 스레드를 생성해, 기획 담당 파트에 요청한다.
- 개발팀과 논의하며 명세를 확정한다.
- Firebase 이벤트 명세 스프레드시트에 기입한다.
- 배포 2일 전까지 Data Engineering 팀에 작업을 요청한다.
- 배포 후 BigQuery 정상 적재를 확인한다.
여기서 이벤트의 ‘로깅 구현’과 ‘적재’를 구분할 필요가 있다.
- 이벤트 로깅(Firebase) 구현: 클라이언트 개발 팀 (6번)
- Firebase → BigQuery raw(events_*) 적재: 자동 (Firebase export)
- raw → 오리지널 마트 적재: DE팀이 구축한 파이프라인 (8번 이후)
즉 이벤트를 BigQuery에 적재하기 위해서는 두 번의 작업이 필요한 것이다. 클라이언트 팀은 이벤트를 interface에 정의해서 각 로깅이 필요한 곳에서 로깅을 해야 한다. 그리고 DE(데이터 엔지니어링)팀은 클라이언트 팀이 로깅하여 적재된 raw 이벤트를 오리지널 마트에 적재하기 위한 스키마 JSON을 선언해두어야 한다.
DE팀이 작성한 스키마 JSON의 내용은 다음과 같다.
// data-catalog/rules/my-project.json
{
// ..
{
"event_name": "service_event",
"event_label": "sample_page_view",
"event_params_to_columns": [],
"legacy": []
},
}
// data-catalog/schemas/my-project/services/service.json
{
"event_params": {
"type": "STRING",
"id": "STRING",
"traffic_source": "STRING",
"status": "STRING",
// ..
}
}
여기서 문제점이 하나 있다. raw가 중간에서 아무거나 받아주기 때문에, 양쪽 선언이 어긋나도 에러가 발생하지 않는다는 점이다. 마트에 컬럼이 비거나 이벤트가 누락이 될 뿐, 어디서도 경고를 하지 않는다. 그래서 지금까지는 BigQuery에서 이벤트가 비는 걸 발견하면, 스키마 JSON 쪽인지 클라이언트 로깅 쪽인지 매번 수동으로 확인을 해야했다. 그래서 이 과정을 자동화하기로 논의가 되었다.
이벤트 검증 자동화 흐름
- main 브랜치에 PR을 올린다.
- 해당 레포지토리의 이벤트 명세가 정의된 인터페이스 파일을 조회한다.
- yaml 추출 패키지를 통해 ts → yaml을 변환한다.
- 생성된 yaml과 DE팀의 카탈로그 레포지토리에 정의된 이벤트 스키마를 비교/검증한다.
- 검증 결과 로그를 생성한다.
- 명세 불일치 발생 시 Slack 채널로 알림을 전송한다.
GitHub Actions
-
main 브랜치에 푸시하거나, PR을 올렸을 때 실행한다. (흐름 1단계)
on: push: branches: [main] paths: - 'src/modules/firebaseEvent/types/FirebaseEvent.types.ts' - '.github/workflows/event-schema.yml' pull_request: paths: - 'src/modules/firebaseEvent/types/FirebaseEvent.types.ts' - '.github/workflows/event-schema.yml' workflow_dispatch: -
현재 레포지토리로 체크아웃하고, Node.js를 설치해 세팅을 준비한다. (흐름 2단계)
jobs: generate-and-validate: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: .nvmrc -
generate-event-schema Composite Action을 사용해 ts → yaml로 변환한다. (흐름 3단계)
- name: Generate event schema uses: my-org/event-schema-tools/.github/actions/generate-event-schema@main with: file: src/modules/firebaseEvent/types/FirebaseEvent.types.ts interface: IFFirebaseEventMap입력으로 검증이 필요한 interface가 담긴 ts 파일과 interface명을 함께 전달한다.
-
data-catalog 레포지토리의 client-event-validate action을 이용해 3번에서 생성한 yaml 파일과 일치하는지를 검증한다. (흐름 4~6단계)
- name: Validate against data catalog uses: my-org/data-catalog/.github/actions/client-event-validate@main with: domain: my-service project: my-project client: front-end client-root: ${{ github.workspace }}/build/event_yamls slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} # 알림 채널 분기 — main push만 운영 채널, 그 외는 테스트 채널. env: ${{ github.event_name == 'push' && github.ref_name == 'main' && 'prod' || 'test' }}
TS → YAML 변환
이벤트 스키마 비교
타 레포지토리 접근 방식
현재 레포지토리의 워크플로우에서 같은 조직에 있는 타 레포지토리에 접근하기 위해서는 자격증명이 필요하다. 이 자격증명으로는 내 개인 PAT(Personal Access Token)을 레포 secret에 넣어서 시도해볼 수 있다. 내가 그 레포지토리에 접근 권한이 있기 때문에 가능한 경우이다. 하지만 만약 내가 퇴사를 하거나, 팀 이동을 하여 권한이 회수될 경우 이 자동화는 끊기게 된다. 이러한 자동화는 개인에 종속되면 안 된다고 판단했고, 워크플로우가 다른 레포지토리에 접근할 다른 방법을 찾아봤다.
- 프론트팀 공용 조직 토큰 활용: 프론트 레포들이 공용으로 쓰는 토큰에 repo 권한이 있다면 그대로 활용이 가능하다.
- Composite Action 방식: 대상 레포지토리에 Composite Action을 두고 조직 내 접근을 허용하면,
uses:만으로 접근이 가능하다. 즉, GitHub Actions를 통한 접근만 열어둔 것이다. - GitHub App: 봇 신원을 만들어서, 워크플로우가 실행될 때마다 그 신원으로 임시 토큰을 발급받아 쓰는 방식. 개인 종속은 확실히 풀리지만 App 등록과 권한 관리를 따로 해야 한다.
- 패키지 발행: GitHub Packages에 발행하면 체크아웃 없이 npm install로 해결할 수 있다.
이 중에서 기존에 사내에서 채택해온 방식이자 다른 팀이 채택하기에도 용이하다고 판단해 2번 Composite Action 방식을 선택했다. Composite Action 방식은 간단하다. 해당 레포지토리에 Actions를 하나 추가하고 설정에서 권한 하나만 열어주면 된다. uses:로 부르면 그 레포지토리 내용이 github.action_path에 딸려오기 때문에, 따로 체크아웃하지 않아도 카탈로그 파일을 읽을 수 있다.
권한 접근이 필요한 레포지토리에서는 아래와 같이 composite action을 만들어주면 된다.
# .github/actions/generate-event-schema/action.yml
name: 'Generate Event Schema YAML'
inputs:
source-dir:
required: true
output-path:
default: 'events.yaml'
outputs:
yaml-path:
value: ${{ steps.gen.outputs.path }}
runs:
using: 'composite'
steps:
- run: npm ci --omit=dev
shell: bash
working-directory: ${{ github.action_path }}
- id: gen
run: node ${{ github.action_path }}/cli.js --src "${{ inputs.source-dir }}" --out "${{ inputs.output-path }}"
shell: bash
그리고 설정이 하나 필요하다: Settings → Actions → General → Access → "Accessible from repositories in the my-org organization"를 설정해야 한다.
호출하는 쪽에서는 아래와 같이 위 레포지토리 composite action에 접근할 수 있다.
- uses: my-org/event-schema-tools/.github/actions/generate-event-schema@main
with:
source-dir: ./src/events
결과
상용 배포 이후 첫 슬랙 메시지가 전달이 되었다! 현재는 누락된 이벤트가 없어 효과가 크게 체감이 되지는 않지만, 앞으로 이번 자동화의 효과는 차차 두고 봐야할 듯하다.
맺으며
이번 작업을 진행하며 어려웠던 건 구현이 아니었다. 구현은 기존에 자주 해오던 GitHub Actions와 Slack API를 이용한 작업이었기에 크게 어렵지 않았다. 그런데 소통이 가장 큰 어려움이자 병목이었다. 데이터 엔지니어링 팀에 이벤트 적재 관련해서 논의를 해야 하고, typescript → yaml 라이브러리를 만든 타 파트 팀원과 소통을 해야 했다.
기존에는 내가 작업을 맡게 된 팀원들과 소통을 했다면, 이번에는 내가 좀 더 주도적으로 ‘이건 누구에게 묻는 것이 가장 적절할까?’에 대해서 고민하고 직접 묻고, 슬랙 히스토리와 노션 문서를 찾아가며 ‘기존에 이를 해결한 선례가 있을까?’를 찾으면서 주도성을 느낀 시간들이었다. 그리고 예상보다 빠르게 일을 마무리할 수 있어서 참 다행이었다.
나는 자동화를 참 좋아한다. 이번 건 비교적 단순했고 이미 갖춰진 게 많았지만 앞으로는 더 복잡하고, 더 많은 병목을 제거해주는 자동화를 만들어 기여하고 싶다.
이번 자동화는 단순히 우리 팀, 우리 프로젝트뿐 아니라 사내 전체적으로 도입해서 다른 팀원들의 불편함을 해결할 수 있으면 좋겠다.