파이프라인 이중집계 차단과 단말 자동화 마무리
목차
저녁 6시에 첫 커밋을 찍고 자정 가까이까지 이어진 이 시간대는 방향이 처음부터 명확했다. 새 기능을 추가하는 것보다, 기존 구조에서 언제 터질지 모르는 지뢰를 먼저 제거하는 흐름이었다. PV 집계 파이프라인의 이중집계 구조를 완전히 끊어내는 것, stocks.opsvoro.com(ExcelStocks)을 인벤토리에 공식 편입하는 것, nyangle 단말 자동화를 마지막 한 대까지 완성하는 것. 그리고 인프라 전반의 문서 정합 감사가 그 사이사이에 얽혀 있었다.
DEDICATED_LOGS 이중집계 구조 수술
이번 작업의 기술적 핵심은 site-pv.py에서 40번 서버 사이트 5개의 DEDICATED_LOGS 설정을 전부 제거한 것이다. 겉으로 보면 단순한 설정 삭제인데, 이 결정에 이르기까지 배경이 있다.
PV 집계 파이프라인은 크게 두 레이어로 나뉜다. site-pv.py는 각 사이트의 로컬 로그를 직접 읽어서 PV를 계산하고, overseas-parse.py는 해외 서빙 서버(k1~k8, opsvoro 등)의 로그를 별도로 읽어서 해외 트래픽 분을 합산한다. 이 두 경로가 서로 다른 로그 파일을 참조하면 아무 문제가 없다. 그런데 DEDICATED_LOGS가 overseas 합산에서 이미 쓰는 경로와 동일한 로그를 가리키면, 같은 데이터를 두 번 읽는 구조가 완성된다. 한쪽은 overseas 합산으로, 다른 쪽은 로컬 직접 읽기로.
이 구조에서 실제로 사고가 터진 전례가 있다. sportglance 사이트에서 특정 기간 동안 PV가 실제보다 정확히 2배로 집계됐다. 원인을 추적해보니 DEDICATED_LOGS가 overseas 경로와 겹쳐 있었고, 당시 로그 파일에 실제 데이터가 쌓여 있었기 때문에 이중 합산이 그대로 수치에 반영됐다. 파이프라인 자체는 정상 동작 중이니 에러가 없었고, 눈으로 숫자가 이상하다는 걸 알아채기 전까지는 문제가 있는지조차 파악이 안 됐다.
이번에 제거한 5개 사이트는 현재 해당 로그 파일이 0바이트 상태다. 지금 당장 값 변화가 없다는 게 사고가 나지 않은 이유이지만, 동시에 지금 제거하기에 가장 안전한 타이밍이기도 하다. 로그가 쌓이기 시작한 이후에 이 구조를 건드리면, 수치가 달라지는 게 버그인지 의도된 수정인지 구분하기 어려워진다. 그리고 운영 중에 PV가 갑자기 줄면 그에 대한 해명 비용이 생긴다. 지금처럼 "변화 없이 구조만 바르게 고치는" 시점이 이상적이었다.
# Before: DEDICATED_LOGS와 overseas 합산이 동일 경로 참조 - 이중집계 위험
SITES = {
"site-a": {
"DEDICATED_LOGS": ["/var/log/nginx/site-a.access.log"],
# overseas-parse.py도 같은 경로를 HOST_LOGS로 읽는 중
...
}
}
# After: DEDICATED_LOGS 제거 - overseas-parse.py 단일 진입점으로 통일
SITES = {
"site-a": {
# 해외 집계는 overseas-parse.py → HOST_LOGS 하나로
...
}
}
집계 구조는 단순할수록 버그가 줄어든다. 로그를 읽는 진입점이 두 개 이상이면 언젠가 반드시 충돌이 생긴다. 서버가 늘고, 신규 사이트가 붙고, 파이프라인이 확장되면서 이런 중복이 자연스럽게 만들어지기 때문에, 주기적으로 구조를 점검하지 않으면 sportglance 케이스가 반복된다.
ExcelStocks 파이프라인 편입
이중집계 정리와 같은 세션에서 stocks.opsvoro.com(ExcelStocks)을 파이프라인에 정식으로 올렸다. 이 사이트는 40번 서버에서 운영 중이고, 전용 로그 파일로 excelstocks.access.log를 별도로 쓴다.
새 사이트를 인벤토리에 편입할 때 반드시 두 군데를 함께 건드려야 한다.
- overseas-parse.py: HOST_LOGS에 40번 서버의 excelstocks.access.log 경로 등록. 빠뜨리면 해외 트래픽이 집계에서 통째로 누락된다.
- stats-dashboard.py: DISPLAY_ORDER에 stocks.opsvoro.com 항목 추가. 빠뜨리면 집계는 돌아가는데 대시보드에 표시되지 않는다.
이 두 파일을 세트로 업데이트해야 한다는 규칙이 코드 어디에도 명시적으로 강제되지 않는다. 경험으로 알고 있어서 지금은 빠뜨리지 않지만, 이 파이프라인을 처음 보는 사람은 한쪽만 등록하고 "왜 대시보드에 안 뜨지" 하고 한참 헤맬 수 있다. 새 사이트 추가 체크리스트를 어떤 형태로든 명문화해두는 게 필요하겠다는 생각이 들었다.
편입 작업과 동시에 하네스 정합감사 문서에도 stocks.opsvoro.com의 k8 인벤토리 편입 사실을 반영했다. 코드 변경과 문서 변경을 같은 커밋 세션 안에 처리하는 걸 원칙으로 하고 있는데, 이 원칙이 무너지면 문서가 실제 상태보다 조금씩 뒤처지기 시작한다. 처음엔 사소하게 보이지만 6개월 후에는 문서를 아예 신뢰할 수 없는 상태가 된다. 결국 실측으로 확인하는 비용이 올라가고, 그 시점에 정합을 맞추는 작업이 훨씬 더 크게 들어간다.
nyangle 단말 자동화 - 마지막 한 대 완성
이날 가장 오래 씨름한 건 nyangle 프로젝트의 마무리였다. 13대 단말 자동화 중 12대는 이미 완료된 상태였고, 마지막 한 대인 pt07이 남아 있었다. pt07은 디스크가 6G짜리여서 기존 설치 구성을 그대로 올리기에 용량이 부족했다. 이번에 12G로 재구축하면서 필요한 환경을 다시 구성했고, 최종적으로 13/13을 달성했다.
기술적으로 이번 작업의 핵심은 adb 직접 설치 경로를 우회하는 nyangle_adb_install.sh 정비였다. 기존 방식은 adb를 통해 APK를 직접 설치하는 흐름이었는데, 일부 단말에서 저메모리 상태일 때 Play UI가 렌더링을 제대로 완료하지 못하는 문제가 반복됐다. 설치 화면은 뜨는데 버튼이 그려지지 않거나, 버튼은 그려졌는데 터치 이벤트를 받지 못하는 케이스였다. 동일 조건에서도 단말마다 재현 여부가 달라서 원인을 특정하기 어려운 종류의 버그였다.
해결 방향은 두 갈래였다. 하나는 Play UI를 아예 거치지 않는 설치 경로를 추가하는 것이다. 저메모리 상태에서 UI 렌더링이 신뢰할 수 없으면, 렌더링 자체를 건너뛰는 게 낫다는 판단이었다. 다른 하나는 픽셀 버튼 탐지 로직이다. Play UI를 써야 하는 케이스에서, 버튼이 실제로 화면에 렌더링됐는지 확인하지 않고 고정 좌표만 때리는 방식은 실패율이 너무 높았다. 픽셀 좌표 기반으로 버튼 존재 여부를 확인한 뒤에 터치하는 방식을 넣었다. 완벽하진 않지만, 렌더 완료 확인 없이 맹목적으로 좌표를 때리는 것보다는 훨씬 안정적이다.
단말 자동화는 소프트웨어 자동화보다 변수가 많다. 같은 모델의 단말이어도 메모리 여유, OS 버전, 배터리 상태, adb 연결 안정성이 제각각이다. "일반 케이스"를 작성하면 반드시 예외가 나온다. 13대를 커버하는 과정은 결국 예외를 하나씩 쌓는 방식이었고, 스크립트가 두꺼워지는 건 그 필연적인 결과다. pt07의 CLAUDE.md에 12G 재구축 완료 사실, 메모리 대응 방식, adb 우회 경로 적용 여부를 기록해뒀다. 같은 조합의 문제를 다음에 다시 만났을 때 처음부터 다시 삽질하지 않도록.
인프라 문서 정합 감사
남은 시간은 인프라 전반의 문서 정합 작업이었다. 코드를 건드리진 않지만 운영 신뢰도를 유지하는 데 필수적인 부분이다.
| 항목 | 처리 내용 | 반영 파일 |
|---|---|---|
| gov-bot repo | archived 상태 반영 | hedvion-CLAUDE.md |
| 옛 MySQL 볼륨 | NAS 아카이브 후 삭제, admin 백업 3종 기삭제 반영 | hedvion-CLAUDE.md |
| ufw 3306 포트룰 | 내부 포트 잔존 룰 삭제 반영 | hedvion-CLAUDE.md |
| 해외 서빙 인벤토리 | k1~k8+opsvoro 2 현행화 | hedvion-CLAUDE.md |
| eval 원격실행 함정 | 인용 벗김 변종 케이스 추가 | hedvion-CLAUDE.md |
| GSC 메일 | Gmail 필터 자동분류 설정 기록 | seo-infra.md |
| opsvoro sitemap | 수정 이력 기록 | seo-infra.md |
| gov-bot CLAUDE.md | 서버 검증용 생성 | CLAUDE.md |
gov-bot은 실제로 archived된 상태였는데 문서에는 활성 봇처럼 기록되어 있었다. 이런 stale 항목이 쌓이면 나중에 "gov-bot이 왜 응답 안 하냐"는 디버깅에 시간을 쓰게 된다. 그 자체가 비용이고, 진짜 장애와 구분이 안 된다는 점에서 더 나쁘다.
MySQL 볼륨과 ufw 잔존 룰은 연결된 작업이었다. MySQL이 더 이상 해당 서버에서 직접 뜨지 않으니, ufw에 남아있던 3306 내부 포트 허용 룰은 실효가 없어진 상태였다. 잔존 룰이 있으면 포트 맵이 실제와 달라져서 방화벽 감사 때 혼선이 생긴다. 볼륨 아카이브와 함께 룰을 삭제하고, 두 가지 모두 문서에 반영했다.
eval 원격실행 함정에 인용 벗김 변종을 추가한 건 하네스 보안 기록 차원이었다. eval로 원격에서 받은 문자열을 실행할 때, 인용 처리 방식에 따라 의도치 않은 명령 실행이 가능한 케이스가 있다. 기존 문서에는 직접 실행 형태만 기록되어 있었고 인용 벗김 변종이 빠져 있어서 추가했다. 함정 문서는 실제 발생 가능한 변종을 다 열거해야 의미가 있다.
AdSense 수정은 결이 다르지만 이 시간대에 같이 처리했다. layout.tsx에 head 로더와 계정 meta를 추가해서 NEEDS_ATTENTION(광고코드 미검출) 상태를 해소했다. AdSense 검수에서 코드를 못 잡는 경우는 거의 항상 스크립트 위치 문제나 meta 태그 누락에서 온다. 이번도 동일한 원인이었고, 추가 후 정상 감지됐다.
이 시간대 전체를 돌아보면, 새로운 걸 만드는 시간보다 기존 것을 정확하게 유지하는 시간이 훨씬 많았다. 집계 파이프라인의 이중 참조를 없애고, 문서와 실제 상태를 맞추고, 오래된 규칙과 볼륨을 정리하는 것들. 당장 시스템이 멈추지 않아서 미루기 쉬운 작업들이다. 그런데 이런 것들이 조용히 쌓이다가 sportglance 2배 집계처럼 터진다. 그 관점에서 이번 시간대의 우선순위 판단은 맞았다고 본다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.