← 글 목록

깃허브 협업 레포 커밋을 내 것처럼 보여주던 블로그 자동화 파이프라인 고치기

/ 8분 분량

블로그에 자동으로 올라가던 글들 중에 팀 프로젝트 커밋이 전부 내가 한 것처럼 표시되고 있다는 걸 발견하고, 원인을 찾아 고치는 과정에서 GraphQL 필터링까지 파고든 하루를 정리한다.

학습 주제

  • 주제: GitHub GraphQL API의 커밋 작성자 필터링, 자동화 파이프라인에서의 데이터 귀속(attribution) 처리
  • 날짜: 2026년 7월 24일

탐구 과정

내가 만든 LearningCollector 파이프라인은 GitHub 커밋과 PR을 자동으로 수집해서 블로그 초안을 생성하고 포스팅까지 해준다. 그런데 이 파이프라인이 조직/협업 레포까지 훑으면서, 브랜치에 있는 모든 커밋을 작성자 구분 없이 긁어오고 있었다. 그러다 보니 팀원이 작성한 커밋도 내가 한 것처럼 블로그에 올라가고 있었던 것이다.

코드를 뜯어보니 원인이 명확했다. github_collector.py가 커밋을 저장할 때 GraphQL 응답에 이미 들어있던 작성자 정보(author.user.login)를 그냥 버리고 있었다. PR 쪽은 작성자 필드를 이미 저장하고 있었는데, 커밋만 누락된 상태였다.

처음엔 단순하게 "태그로 구분만 하면 되겠다"고 생각했다. 블로그에는 이미 태그 기반 필터(?tag=)가 완성돼 있어서, 커밋 JSON에 작성자를 저장하고 초안 생성 단계에서 본인/친구이름 + 커밋/PR 태그를 코드로 강제 주입하면 프론트/백엔드는 건드릴 필요가 없었다.

여기서 한 번 멈췄다. 이미 포스팅된 과거 글들은 어떻게 할지가 문제였다. 처음엔 "삭제까지 할 필요가 있나, 태그만 붙여서 필터로 구분되게 하면 되지 않나" 싶었는데, 다시 생각해보니 팀원 입장에서는 자기가 모르는 사이에 자기 코드가 남의 개인 블로그에 분석 글로 올라가 있는 상황이 될 수 있다는 게 마음에 걸렸다. 이건 코드로 해결할 문제가 아니라 결국 "미리 알렸는가"의 문제였다. 결국 팀원 작성 글은 전부 삭제하는 쪽으로 방향을 정했다.

삭제를 실행하기 전에 재밌는 함정을 하나 발견했다. 작성자로 분류된 105개 글 중 상당수가 claude라는 이름으로 찍혀 있었는데, 이게 팀원이 아니라 내가 Claude Code로 커밋했을 때 git 작성자 이름이 자동으로 claude로 찍힌 것이었다. 레포도 전부 내 개인 프로젝트(LearningCollector, my-blog, monitoring)였다. 만약 "내 GitHub 계정이 아니면 전부 삭제"를 그대로 실행했다면, 내가 직접 작업한 글까지 통째로 날아갈 뻔했다. 다행히 실행 전에 확인해서 claude 작성자 23개는 제외하고, 실제 팀원 계정(xhae123, xihxxn, sunwoo1256, hjdd0309, ParkIlha 등)으로 된 글만 골라냈다. 중복 집계(커밋 draft와 PR draft가 같은 제목으로 매칭된 경우)를 정리하니 최종적으로 76개가 나왔다. 백업을 먼저 남기고 삭제를 진행했다.

문제는 여기서 끝이 아니었다. 태그 로직만으로는 다음 자동 실행 때 팀원 글이 또 쌓이게 되어 있었다. 그래서 파이프라인 자체를 고쳐서, 팀원 작성 커밋/PR은 무거운 API 조회(파일 diff 등) 전에 미리 걸러내도록 만들었다.

마지막으로 궁금해진 게 있었다. "애초에 GitHub 쪽에 조회 자체를 안 하게 만들 수는 없을까?" 클라이언트에서 받아온 뒤 거르는 건 어차피 API 호출 비용을 다 쓰고 나서 버리는 셈이라 비효율적이라고 느꼈다.

핵심 학습 내용

GraphQL author 필터의 존재

GitHub GraphQL의 history 쿼리에는 실제로 author 필터 인자가 있다는 걸 확인했다. 서버 단에서부터 원하는 작성자의 커밋만 받아올 수 있다는 뜻이다.

history(author: { emails: ["cjang6199@gmail.com"] }) {
  ...
}

반면 PR 쪽(repository.pullRequests)에는 이런 author 필터 인자가 아예 없다. 대안으로 search API가 있긴 하지만 요청 제한이 훨씬 빡빡해서, PR은 기존 방식(전체를 받아온 뒤 파일 diff 조회 전에 거르기)을 유지하는 게 합리적이었다. PR은 원래도 diff 조회처럼 무거운 작업 전에 걸러지고 있어서 비용 부담이 크지 않았다.

이메일 필터의 함정 — 계정은 여러 이메일을 쓴다

이메일로만 필터링(emails: [...])했더니 문제가 하나 생겼다. 내 계정이 개인 이메일과 학교 이메일 두 개를 섞어서 커밋해온 이력이 있었는데, 이메일 하나만 필터에 넣으면 다른 이메일로 찍힌 내 커밋이 누락될 수 있었다.

해결은 이메일 대신(혹은 함께) 계정의 node ID로 필터링하는 것이었다.

history(author: { id: "MDQ6VXNlcjxxxxx", emails: [...] }) {
  ...
}

id로 필터링하면 그 계정에 연결된 모든 이메일이 자동으로 매칭 대상이 된다. 이메일을 따로 관리할 필요 없이 계정 단위로 안전하게 걸러진다는 걸 알게 됐다.

실제 검증

팀원 활동이 많은 레포로 직접 검증해봤다.

  • 필터 미적용: 전체 2967개 커밋
  • 필터 적용: 1915개 커밋만 응답 (팀원 커밋 1052개는 GitHub 서버에서부터 아예 안 옴)
  • 작성자는 내 두 이메일(개인/학교) 계정만 포함, 팀원 계정은 하나도 섞이지 않음

다층 방어

GraphQL 서버 단 필터를 넣었다고 클라이언트 사이드 필터(_is_own_author)를 없애지는 않았다. merge commit이 GitHub 봇 계정으로 찍히는 경우처럼, 서버 필터가 못 잡는 예외가 있을 수 있어서 이중 방어로 남겨뒀다.

이해한 내용

  • 저장 시점에 데이터를 버리면 나중에 복구가 훨씬 비싸다. GraphQL 응답에 이미 작성자 정보가 있었는데 저장할 때 무시했던 게 이번 문제의 근본 원인이었다. 처음부터 필요할 수 있는 필드는 웬만하면 저장해두는 게 낫다는 걸 체감했다.
  • 필터링은 되도록 앞단(서버)에서 하는 게 비용을 줄인다. 클라이언트에서 받아온 뒤 거르는 것과 서버 쿼리 자체에서 거르는 것은 API 호출 횟수와 응답 크기 면에서 차이가 크다. 이번 경우엔 실제로 1052개 커밋의 조회 자체를 아낄 수 있었다.
  • 계정 식별은 이메일보다 ID가 안정적이다. 사람은 여러 이메일을 섞어 쓸 수 있지만, 플랫폼의 계정 ID는 하나로 고정돼 있어서 필터링 기준으로 더 신뢰할 수 있다.
  • 자동화가 만드는 결과물도 결국 사람이 확인해야 하는 지점이 있다. claude로 찍힌 커밋을 그대로 "팀원 것"으로 분류해서 삭제했다면 내 작업물까지 날렸을 것이다. 자동 분류 결과를 그대로 실행하기 전에 한 번 더 들여다보는 습관이 왜 필요한지 새삼 느꼈다.

실전 적용

  • 지금 GraphQL API를 쓰는 다른 프로젝트에서도, REST로 전체를 받아온 뒤 필터링하고 있는 부분이 있는지 점검해볼 계획이다. 쿼리 인자로 옮길 수 있는 필터는 최대한 서버 쪽으로 옮기는 게 낫다.
  • 여러 개인 프로젝트를 자동화 파이프라인으로 관리할 때, "작성자 = 나"라는 가정을 코드 어딘가에 깔고 있지는 않은지 다시 점검해보려고 한다. 협업이 섞이는 순간 이 가정이 깨진다는 걸 이번에 배웠다.
  • 삭제처럼 되돌리기 어려운 작업 전에는 백업 파일을 남기고, 대상 목록을 사람이 직접 눈으로 확인하는 단계를 습관화하려고 한다.

추가 학습 계획

  • GitHub GraphQL 스키마에서 PR에도 author 필터를 넣을 수 있는 다른 우회 방법(예: search API의 rate limit 최적화)이 있는지 더 찾아볼 예정이다.
  • GraphQL의 다른 커넥션 필드들도 어떤 필터 인자를 지원하는지 스키마 문서를 좀 더 체계적으로 훑어보고 싶다.
  • 자동화 파이프라인에서 "삭제/대량 변경" 같은 파괴적 작업을 안전하게 다루는 패턴(dry-run, 승인 단계 등)을 좀 더 공부해보려고 한다.