AI·

슬랙을 데이터 파이프라인으로 — 변경 감지 수집기

Jira·Confluence 앱이 슬랙 채널에 쏘는 변경 알림을 Socket Mode 봇으로 받아, 생성·수정·삭제를 기록하는 수집기를 만든 기록. 삭제 알림의 별도 처리, 메시지 파싱과 ref 추출, 수집 완료 표시, systemd 서비스화까지 정리합니다.

지난 글에서 폴링을 걷어내고 이벤트 기반으로 방향을 잡았습니다. 이번엔 그 첫 관문 — 변경이 일어난 순간을 잡아채는 수집기를 만듭니다. 그런데 별도의 웹훅 서버를 세우는 대신, 이미 팀이 쓰는 슬랙을 파이프라인으로 삼았습니다.

왜 슬랙이었나

Jira·Confluence에는 변경 알림을 슬랙 채널로 쏘는 앱이 있습니다. 이슈가 생기거나 바뀌거나, 페이지가 지워지면 채널에 메시지가 올라오죠. 많은 팀이 원래 켜 두는 기능입니다.

그러면 굳이 외부에 포트를 열고 웹훅 서버를 운영할 이유가 없었습니다. 그 채널의 메시지를 받아 읽기만 하면 변경 스트림이 그대로 손에 들어오니까요. 인프라를 새로 세우는 대신, 이미 있는 걸 파이프로 쓰는 셈입니다. 변경 알림을 한 채널로 일원화해 두면, 우리 수집기는 그 채널 하나만 바라보면 됩니다.

Socket Mode로 채널을 듣는다

슬랙 봇이 메시지를 받는 방법은 두 가지입니다. 외부 URL로 이벤트를 받는 방식(HTTP)과, 봇이 슬랙으로 먼저 연결을 여는 Socket Mode.

사내 서버는 외부에 포트를 열기 부담스러워 Socket Mode를 택했습니다. 봇이 슬랙으로 아웃바운드 연결만 유지하면, 인바운드 포트 없이도 채널 메시지를 실시간으로 받습니다. 방화벽에 구멍을 내지 않아도 되니 사내망에 딱 맞았죠.

봇을 새로 만들 땐 필요한 권한 스코프와 앱 매니페스트를 정리해 둬야 나중에 헤매지 않습니다. 채널 메시지를 읽는 권한, 그리고 뒤에서 쓸 리액션·댓글 권한 같은 것들이죠. 이걸 처음에 문서로 박아 두면, 다른 서버에 다시 설치할 때 그대로 복사해 쓸 수 있습니다.

생성·수정과 삭제는 결이 달랐다

한 가지 예상 못 한 갈림길이 있었습니다. 생성·수정은 Jira·Confluence 앱이 보내주는 알림 메시지를 그대로 받아 쓰면 됐는데, 삭제는 그 알림에 잘 담겨 오지 않았습니다.

그래서 삭제만 따로 처리했습니다. 아틀라시안 쪽 설정에서 **삭제 이벤트용 알림(인커밍 웹훅)**을 별도로 걸고, 우리가 필요한 필드만 담아 보내도록 템플릿을 지정한 겁니다. 여기서 웹훅 URL은 아틀라시안 설정에서만 쓰이지, 우리 프로그램이 직접 호출하는 게 아닙니다. 우리 입장에선 결국 "같은 채널에 삭제 메시지도 들어온다"가 전부입니다.

삭제는 특히 단순하게 갔습니다. 지워진 문서에서 우리가 필요한 건 id와 제목뿐이라, 알림 템플릿을 이렇게 못박았습니다.

[DELETED] {content.id} — {content.title}

이러면 삭제 메시지를 파싱할 때 규칙이 명확합니다. [DELETED] 접두어를 보고 삭제로 판정하고, 뒤에서 id와 제목만 떼어 오면 됩니다.

메시지에서 '무엇이 바뀌었나'만 추린다

채널 메시지는 사람이 읽기 좋게 꾸며져 옵니다. 우리에게 필요한 건 거기서 딱 네 가지입니다.

  • source: jira냐 confluence냐
  • event: created / changed / deleted
  • ref: 문서를 특정하는 키 (Jira 이슈 키, Confluence 페이지 id)
  • title / url: 사람이 알아볼 제목과 원문 링크

메시지 안에서 이 값들을 뽑아냅니다. 특히 ref는 원문 URL에서 규칙적으로 추출할 수 있습니다 — Confluence면 페이지 경로에 박힌 숫자 id를, Jira면 이슈 키를 떼어 오죠. 이 ref가 나중에 "같은 문서인가"를 판단하는 기준이 되기 때문에, 여기서 정확히 뽑아 두는 게 중요했습니다.

기록 포맷도 군더더기를 걷어냈습니다. 처음엔 슬랙 메시지의 타임스탬프(slack_ts)와 채널 id도 같이 담았는데, 둘 다 뺐습니다. 이벤트 시각을 담는 ts가 이미 있으니 slack_ts는 중복이고, 채널은 하나로 고정돼 있으니 매 건에 적어 둘 이유가 없었습니다. 필요 없는 필드는 처음부터 안 담는 게 깔끔합니다.

{ "source": "jira", "event": "changed", "ref": "PROJ-2807",
  "title": "슬랙 봇 — 변경 수집 테스트", "url": "https://…/browse/PROJ-2807",
  "ts": "2026-08-07T04:47:01Z" }

받았으면, 받았다고 표시하기

작은 기능 하나를 더 붙였습니다. 수집이 끝나면 그 슬랙 메시지에 리액션(또는 댓글)으로 "수집 완료"를 남기는 것.

별것 아닌 것 같지만 운영에서 큰 차이를 만듭니다. 채널만 봐도 어떤 변경이 처리됐고 어떤 게 아직인지 한눈에 보이니까요. 파이프라인이 살아 있다는 신호이기도 합니다. 다만 이걸 켜려면 봇에 리액션 권한 스코프를 추가하고 앱을 재설치해야 했습니다. 스코프를 바꾸면 재설치가 따라온다는 걸 이때 다시 확인했습니다.

systemd 서비스로 상주시키기

수집기는 항상 떠 있어야 하는 상주 프로세스라, systemd 서비스로 등록했습니다. 서비스 이름은 역할이 바로 드러나게 log-ingester로 지었습니다. (기본으로 붙는 밋밋한 이름은 로그에서 뭐가 뭔지 헷갈려서 바꿨습니다.)

배포도 단순하게 만들었습니다 — git pull 후 서비스 재시작을 하는 스크립트 하나. 코드가 바뀌면 이 스크립트만 돌리면 됩니다.

# /etc/systemd/system/log-ingester.service (요지)
[Service]
ExecStart=/opt/app/.venv/bin/python app.py
Restart=always

비공개 채널로 바꿔도 봇이 그 채널에 초대돼 있기만 하면 그대로 동작합니다. 채널 접근 권한과 상주 실행, 이 두 가지만 챙기면 수집기는 조용히 제 몫을 합니다.

여기까지

  • 웹훅 서버를 새로 세우지 말고, 이미 쓰는 슬랙 채널을 파이프로.
  • Socket Mode로 인바운드 포트 없이 실시간 수신.
  • 생성·수정은 앱 알림, 삭제는 별도 웹훅 템플릿으로 결을 나눠 처리.
  • 메시지에서 source·event·ref·title만 추리고, 중복 필드는 버린다.
  • 수집 완료를 채널에 표시해 운영 가시성을 확보(스코프 추가 → 재설치).
  • systemd로 상주 + git pull 재시작 스크립트.

이제 변경이 실시간으로 흘러들어옵니다. 그런데 이걸 어떤 파일에, 어떻게 쌓아야 나중에 안전하게 꺼내 쓸 수 있을까요? 다음 글에서 5분 버킷과 '봉인' 개념으로 적재 포맷을 설계합니다.