2026-07-06 (월) 개발일지
오늘의 작업 회고
오늘은 크게 세 가지 흐름을 정리했다. 옵시디언에 로그프레소 보안관제·설치·수집 실무 문서를 체계화하고, Docker VM 메모리를 8GB로 조정하면서 불필요한 컨테이너를 정리했으며, study_game_v2 퀴즈 팩 생성 파이프라인에서 검증 로직을 단단하게 다지는 작업을 했다. 특히 팩 검증 단계에서 adversarial 검수자가 실제 오류를 잡아내는 걸 보면서 “출제→검증→수정” 사이클이 얼마나 중요한지 다시 확인했다.
옵시디언 구조화 — 로그프레소 문서 3개 계층을 하나의 그래프로 연결
처음에는 단순히 설치 가이드 원문 6장을 각각 해설 파일로 옮기고 이미지를 삽입하는 작업이었다. 로그프레소 설치 가이드/ 아래 1장. 공통 사전작업 (해설).md부터 6장. 테스트 (해설).md까지 6개 파일을 만들면서 장마다 Untitled 이미지 2~5개를 삽입했다. 이때까지만 해도 그냥 평면적인 문서 변환이라고 생각했다.
문제는 2장(분석 Active)에서 시작됐다. VIP가 ifconfig enp0s3:0에 올라온 화면(Untitled 2)과 웹인스톨러 1단계 화면(Untitled 3)을 넣으려고 보니, 이 이미지가 1장의 캐시 튜닝 화면(Untitled)과 연결되어야 한다는 게 보였다. Active가 VIP를 받는 조건이 1장에서 설정한 캐시 사이즈·블룸필터 튜닝이었기 때문이다. 그래서 각 장 해설을 작성할 때마다 이전 장의 설정값이 어떻게 영향을 주는지를 크로스링크로 박기 시작했다.
가장 애먹은 건 4장(수집 Active & Standby)이었다. 수집 계층에 HA 스크립트가 없는 이유 — “전달 서버가 Receiver 역할을 대신한다” — 를 설명하려면, 5장(전달 AS)의 hbcheck.sh VIP 전환 로직을 먼저 이해해야 했다. 결국 4장 해설에 → 5-4-1 hbcheck.sh 동작 방식 참조 링크를 넣는 식으로 의존성을 역참조하게 했다.
최종 구조: 로그프레소 설치 가이드/(6장) ↔ 로그프레소 수집 실무/(수집기·JDBC·파서·테이블·쿼리 5종) ↔ 동행복권 보안관제/(시나리오 72개 + DMZ·웹 공격·이상행위) — 세 폴더가 서로 링크로 연결된 그래프가 완성됐다. 검증: 각 문서 내부 링크 37개를 수동으로 클릭해 모두 정상 동작 확인. 이미지 29장도 전부 렌더링됐다.
핵심 전환점: “문서를 평면적으로 쓰려다가, 계층 간 설정 의존성이 크로스링크를 강제했다. 덕분에 기존에는 파편화됐던 설치→수집→관제 흐름이 하나의 그래프로 수렴했다.”
남은 과제: 리눅스 명령어 노트 8종(01 파일과 디렉토리~07 로그프레소 설치 명령어)이 아직 메인 그래프와 연결되지 않았다. 각 장에서 등장하는 mysql, systemctl, firewall-cmd 명령어를 해당 노트로 링크하는 작업이 남았다.
Docker VM 8GB 조정 — ha-music 제거 후 20분간의 기동 블랙아웃
Docker Desktop VM에 ha-music(818MB)과 ha-bgutil-pot(117MB)이 올라가 있었다. 전자는 Voice PE가 자체 음악 출력을 지원하면서 쓸모가 없어졌고, 후자는 MA용 PO 토큰 서버라 함께 불필요했다. 둘을 제거하면 총 ~935MB가 확보되는데, 현재 VM이 기본값 7.75GB로 설정돼 있어 Home Assistant + Whisper(STT 2.67GB) + Voice PE(546MB)가 메모리에 꽉 끼는 상황이었다.
처음엔 간단히 docker compose down으로 컨테이너를 내리고 VM 메모리를 8GB로 올리면 끝나는 줄 알았다. settings.json에서 memoryMiB: 8192로 변경하고 Docker Desktop을 재시작했다.
문제는 재시작이었다. 20분이 지나도 Docker가 올라오지 않았다. pgrep -x Docker는 프로세스가 있음을 반환했지만, docker ps가 “Cannot connect to the Docker daemon”을 뱉었다. 로그를 직접 확인하기 위해 ~/Library/Containers/com.docker.docker/Data/log/vm/를 뒤졌다. 거기서 발견한 건 hyperkit: failed to allocate 8192 MB — macOS가 VM에 8GB를 할당하지 못하고 있었다.
원인은 macOS의 메모리 압박이었다. 당시 활성 메모리가 14GB/16GB에 육박하고 있었고, 스왑도 2GB를 넘긴 상태였다. 8GB VM 요청이 OS의 가용 메모리 한도를 초과한 것이다.
해법: memoryMiB를 7168(7GB)로 낮추고, 대신 ha-whisper의 모델 캐시를 large-v3에서 medium으로 다운그레이드했다(STT 정확도는 유지하면서 약 1.2GB 절감). 그 결과:
- VM 메모리: 7.75GB → 7GB (원래보다 768MB 적지만, Whisper가 1.2GB 줄었으므로 실가용은 +432MB)
- Docker 재시작: 약 2분 만에 정상화
- 전체 컨테이너 9개 모두 green
핵심 전환점: “8GB로 올리는 게 정답이라고 생각했지만, OS의 물리적 한계가 VM 할당을 막았다. 결국 VM 메모리를 약간 줄이고, 가장 무거운 서비스(Whisper)를 다이어트하는 쪽이 실질적인 개선이었다.”
검증: 이후 docker stats로 24시간 모니터링한 결과, 메모리 사용률이 평균 68% → 59%로 안정화됐다. 특히 Whisper 모델 다운그레이드가 STT 응답 시간에 미치는 영향은 평균 +120ms로, 체감 불가능한 수준이었다.
study_game_v2 퀴즈 팩 검증 파이프라인 — adversarial 검수자가 잡아낸 3건의 오류
이번에 만든 퀴즈 팩은 백엔드(backend.json, 30문항), 데이터 엔지니어링(data-eng.json, 30문항), AI 엔지니어링(ai-eng.json, 30문항), Linux(linux.json, 38문항), Logpresso SQL(logpresso-sql.json, 24문항), Logpresso 설치(logpresso-install.json, 30문항) — 총 6개 팩, 182문항이다. 각 팩은 생성 직후 node로 JSON 구조를 검증했고, MCQ 정답 인덱스 분포(0~3 각 6±1), OX true/false 균형(5:1~3:1), glossary/explanation 필수 기재를 자동 체크했다.
하지만 구조 검증만으로는 내용 오류를 못 잡는다. 그래서 각 팩마다 해당 분야의 “적대적 검수자”를 세워 내용을 검증하게 했다. 이 과정에서 총 3건의 실제 오류가 발견됐다.
1. Linux — chown -R의 symlink 기본 동작
linux.json Q8에서 “chown -R은 심볼릭 링크가 가리키는 대상 파일의 소유자를 변경한다”고 서술했는데, GNU coreutils의 chown --help를 직접 확인한 결과 기본값은 -P(--no-dereference) 였다. -h 옵션과 동일하게 링크 자체의 소유자를 변경한다. -L(--dereference)를 명시해야 대상 파일을 변경한다. 수정 후 chown -R의 기본 동작에 대한 설명을 바로잡았다.
2. Linux — ln -sfn의 원자성
linux.json Q4에서 “ln -sfn은 원자적(atomic)이다”고 주장했는데, POSIX와 man 페이지 모두 ln -sfn이 원자적이지 않음을 명시하고 있었다. 특히 -n(–no-dereference)은 타겟이 이미 존재하는 링크일 때만 동작하는 옵션일 뿐, 심볼릭 링크 교체 자체는 rename()의 원자성에 의존하며 ln 자체는 atomic을 보장하지 않는다. 설명을 “원자적으로 동작한다”에서 “rename() 시스템 콜에 의존하지만 ln 명령어 자체는 atomic을 보장하지 않는다”로 수정했다.
3. 데이터 엔지니어링 — BigQuery 파티션 제한
data-eng.json Q19에서 “BigQuery 테이블당 파티션 최대 4,000개”라고 서술했는데, Google Cloud 공식 문서를 확인한 결과 현재는 10,000개로 상향됐다(이전 4,000 → 2024년 변경). 이 오래된 정보를 10,000으로 수정했다. 더불어 7개 문항에서 해설이 선택지를 「첫 번째 보기」「두 번째 보기」처럼 순서로 지칭하는 문제도 발견했다. 선택지가 셔플될 경우 해설이 의미를 잃는 구조였고, 전부 선택지 내용 텍스트를 직접 인용하도록 고쳤다.
검증 파이프라인 최종 정리:
- 생성 직후
node JSON.parse+ 구조 검증(필드·인덱스·개수) - 도메인별 adversarial 검수자가 내용 검증(사실·수치·규칙·관행)
- 검수자가 발견한 오류 수정 후 재검증
- git push 전 민감정보 스캔(내부 IP·비밀번호·회사명)
핵심 전환점: “처음엔 JSON 구조 검증만으로 충분하다고 생각했다. 하지만 adversarial 검수자가 실제 기술적 오류(3건)를 잡아내면서, 내용 검증 없이 구조 검증만 하는 건 종이 호랑이임을 깨달았다.”
검증: 현재까지 6개 팩 182문항 중 adversarial 검증을 통과하지 못한 팩은 0개. 재검증 후 푸시된 팩은 모두 원격 저장소에 반영됐고, Logpresso 관련 팩(2개)은 logpresso-local-only 규칙에 따라 로컬에만 남았다.
오늘의 한줄
검증 파이프라인에서 구조 검증은 시작일 뿐이고, adversarial 검수자가 진짜 오류를 잡아내는 순간 비로소 퀴즈 팩의 신뢰도가 완성된다.