← 글 목록

GitHub 커밋 작성자 필터링, 서버 단에서부터 걸러내기

/ 8분 분량

내가 운영하는 블로그 자동화 시스템이 협업 레포의 팀원 커밋까지 전부 "내가 한 것"처럼 올리고 있다는 걸 발견하고, 이걸 바로잡는 과정에서 GraphQL의 author 필터까지 파고들게 된 하루를 정리한다.

학습 주제

  • 주제: GitHub 커밋/PR 수집 시 작성자 구분 문제 해결 — 태그 필터링부터 GraphQL 서버 사이드 필터까지
  • 날짜: 2026-07-20

탐구 과정

발단은 단순했다. 블로그에 올라간 글들을 보다가 이상한 걸 느꼈다. 팀 프로젝트 레포에서 나온 커밋인데, 친구가 작성한 것도 전부 내가 한 것처럼 표시되고 있었다. 원인을 찾아보니 github_collector.py가 GraphQL 응답에서 author.user.login 같은 작성자 정보를 받아오고도 저장할 때 그냥 버리고 있었다. 게다가 레포의 모든 브랜치 커밋을 작성자 구분 없이 긁어오는 구조였다.

처음엔 간단하게 접근했다. 이미 블로그에 태그 기반 필터(?tag=)가 구현돼 있으니, 커밋 JSON에 작성자 정보만 제대로 저장하고 초안 생성 시 "본인" 또는 "친구이름" + "커밋/PR" 태그를 코드로 강제 주입하면 될 거라 생각했다. AI가 만드는 태그 문구는 신뢰하기 어려워서, 태그 자체는 결정적으로(deterministic) 코드에서 주입하도록 했다.

그런데 여기서 더 큰 질문이 생겼다. "이미 포스팅된 과거 글들은 어쩌지?" 새 글에만 태그가 붙어봐야 이미 올라간 100개 넘는 글은 여전히 뒤죽박죽이었다. 태그만 붙여서 구분되게 할지, 아예 팀원이 작성한 글을 삭제할지 고민했다.

고민 끝에 삭제 쪽으로 결정했는데, 삭제 대상을 정확히 골라내는 과정에서 예상치 못한 함정을 발견했다. "팀원 작성"으로 분류된 105개 중 상당수의 작성자가 claude로 찍혀 있었다. 그런데 그 레포들을 보니 팀 프로젝트가 아니라 전부 내 개인 프로젝트(LearningCollector, my-blog, monitoring 등)였다. 즉 Claude Code로 커밋했을 때 git 작성자 이름이 claude로 찍힌 것일 뿐, 실제로는 전부 내가 한 작업이었다. 이걸 놓쳤으면 내 개인 프로젝트 글을 대량으로 지울 뻔했다.

핵심 학습 내용

문제의 근본 원인

GraphQL 응답에는 작성자 정보가 이미 들어있는데, 저장 로직이 그걸 버리고 있었던 게 문제의 시작이었다. PR은 이미 작성자 필드를 저장하고 있었으니, 커밋 쪽만 손보면 됐다.

1단계: 태그 강제 주입

# _inject_identity_tags() 개념
if 작성자 == GITHUB_USERNAME:
    tag = "본인"
else:
    tag = 실제_github_로그인_or_이름
tag += "/" + ("커밋" if 소스 == "commit" else "PR")

AI가 만든 초안에 태그 줄이 있으면 이어붙이고, 없으면 새로 추가하는 방식으로, AI 출력에 의존하지 않고 코드가 항상 정확한 태그를 붙이도록 만들었다.

2단계: 삭제 대상 정밀 식별

최종적으로 진짜 팀원(website 레포의 xhae123, xihxxn, sunwoo1256, hjdd0309, ParkIlha, hjdd05)이 작성한 82개를 찾았는데, 커밋 draft와 PR draft가 같은 제목으로 매칭돼 중복 집계된 게 있어서 실제 고유 글은 76개였다. 삭제 전에는 반드시 백업(teammate_posts_backup.json, claude 포함 105개 전체)을 남겼고, 삭제 대상 id를 재확인한 뒤에야 DB에서 지웠다.

3단계: 파이프라인 자체에서 차단

삭제만 하면 다음 자동 수집 때 또 쌓인다. 그래서 _save_dev_commits, _save_prs에 _is_own_author() 판별 로직을 넣어서, REST 상세 조회(파일 diff 등 무거운 API 호출) 전에 작성자를 먼저 확인하고 본인 계정 또는 claude가 아니면 그 자리에서 스킵하도록 바꿨다.

4단계: 아예 조회 자체를 걸러내기

여기서 궁금해진 게 있었다. "클라이언트에서 거를 게 아니라, 애초에 GitHub에 요청할 때부터 내 것만 달라고 할 수는 없을까?" 확인해보니 GraphQL의 history 쿼리에 실제로 author 필터가 있었다.

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

이 필터를 넣으면 팀원 커밋은 GitHub 서버 단에서부터 응답에 아예 포함되지 않는다. 처음엔 이메일만으로 필터링했는데, 내가 개인 이메일(cjang6199@gmail.com)과 학교 이메일(andy1692@khu.ac.kr) 두 개로 커밋한 이력이 있다는 걸 알게 됐다. 이메일 하나만 필터에 넣으면 다른 이메일로 한 내 커밋이 누락될 수 있는 상황이었다. 해결은 이메일 대신(혹은 함께) 계정의 node id로 필터링하는 것이었다. 계정 ID로 걸면 그 계정에 연결된 모든 이메일이 자동으로 커버된다.

실제 팀 레포(website)로 검증한 결과, 전체 2967개 커밋 중 필터 적용 시 1915개만 응답으로 왔다. 팀원 커밋 1052개가 아예 서버에서부터 안 온 것이다.

다만 PR은 이야기가 달랐다. GitHub GraphQL의 repository.pullRequests에는 author 필터 인자가 없었다. 대안인 search API는 요청 제한이 훨씬 빡빡해서 오히려 손해였다. 대신 PR은 원래도 무거운 파일 diff 조회 전에 거르고 있어서 비용 부담이 크지 않았고, 그대로 유지하기로 했다.

이해한 내용

  • GraphQL 쿼리의 필터 인자는 리소스마다 지원 범위가 다르다. 커밋 history에는 author 필터가 있지만, PR 목록 조회에는 없다는 걸 실제로 스키마를 확인해보고 알았다.
  • 이메일 기반 필터링은 "한 사람이 여러 이메일을 쓸 수 있다"는 변수를 놓치기 쉽다. 계정 ID로 필터링하면 그 계정에 연결된 모든 이메일을 한 번에 포괄할 수 있어서 더 안전하다.
  • 클라이언트 사이드 필터(코드에서 거르기)와 서버 사이드 필터(요청 자체를 줄이기)는 목적이 다르다. 서버 필터는 API 호출량과 응답 크기를 줄여주지만, merge commit이 봇 계정으로 찍히는 등 예외 케이스가 있을 수 있어서 클라이언트 필터를 안전망으로 남겨두는 이중 방어가 합리적이었다.
  • 자동화 스크립트에서 "작성자 이름"만 보고 판단하면 안 되는 경우가 있다. claude라는 이름이 찍혔다고 해서 무조건 남의 것이 아니라, 도구가 커밋했을 때의 흔적일 수도 있다는 걸 이번에 직접 겪었다.

실전 적용

  • 협업 레포를 다루는 자동화 파이프라인을 만들 때는, "누가 한 일인지"를 저장 단계에서부터 명시적으로 남겨두는 습관을 들여야겠다. 나중에 필터링이 필요해질 걸 미리 대비하는 셈이다.
  • GraphQL을 쓸 일이 있으면, 필요한 필드만 받아오는 것뿐 아니라 필터 인자를 먼저 스키마 문서에서 확인하는 순서로 접근하려 한다. 이번처럼 서버 단에서 걸러지면 API 호출 자체가 줄어드니 비용과 속도 면에서 이득이 크다.
  • 삭제처럼 되돌리기 어려운 작업은 반드시 백업 → 대상 재확인 → 실행 순서를 지키는 게 맞다는 걸 다시 확인했다.

추가 학습 계획

  • GitHub GraphQL의 다른 쿼리(이슈, 리뷰 등)에도 author 필터가 어디까지 지원되는지 스키마를 좀 더 훑어보고 싶다.
  • REST API와 GraphQL API의 rate limit 정책 차이를 정리해서, 언제 REST를, 언제 GraphQL을 쓰는 게 유리한지 기준을 세워보려 한다.
  • 협업 프로젝트 결과물을 개인 블로그/포트폴리오에 담을 때의 윤리적 기준(팀원 사전 동의, 크레딧 표기 등)도 따로 정리해두고 싶다.