Joyuno's Dev Blog

2026-06-28 (일) 개발일지

dev-blog

오늘의 작업: 세 프로젝트에서 만난 예상 밖의 충돌과 그 수습

오늘은 세 가지 프로젝트에서 각기 다른 성격의 이슈를 잡았다. bit-scalp의 트레이딩 전략 분석 결과가 기존 가정을 뒤집었고, home_assistant에서는 음성 출력이 되는 줄 알았던 기능이 fallback 버그와 OpenRouter 키 만료로 이중으로 막혀 있었으며, propermarket-marketing의 TTS 파이프라인에서는 네이티브 라이브러리 충돌이 Docker 빌드 전체를 발목잡고 있었다. 하나씩 풀어본다.

bit-scalp V2 전략 — TP 도달 9% vs SL 도달 55%, 통계가 말한 것

사용자가 “왜 TP는 1%도 못 먹고 SL은 3~4%까지 나가냐”고 짚었다. 처음엔 단순히 손절폭이 과도하다거나 슬리피지 문제일 거라 생각했다. 하지만 직접 데이터를 들여다보니 예상 밖의 패턴이 보였다.

실제 청산 데이터 11건(06-26~28) 을 전수 조사했다:

구분 건수 비율
TP 도달 1건 9%
SL 도달 6건 55%
BE 청산 4건 36%

TP 도달 1건은 AAVE SHORT +2.91%로, 목표 3%에 0.09% 못 미친 케이스였다. SL 도달 6건은 대부분 -2.55% ~ -2.82% 범위에서 터졌다. 여기서 결정적인 가설이 떠올랐다.

“이 전략은 시장과 반대 방향으로 진입하고 있다”

이걸 검증하기 위해 11건을 반대 방향으로 매매했을 때 시뮬레이션해봤다. 원래 SL이 터진 6건은 반대 방향에서 전부 TP에 도달했고, TP에 도달했던 1건만 반대에서 손실로 바뀌었다. 통계적으로 SL이 TP보다 6배 더 자주 도달한다는 건 진입 타이밍이 아니라 방향 자체가 틀렸다는 증거였다.

해법: 단순한 손절폭 조정이 아니라 진입 시그널 자체를 검증해야 했다. 당장 적용한 건 아니지만, backtest 모듈에 롱/숏 반전 시뮬레이션을 추가해 새 시그널이 들어올 때마다 “이 방향이 맞는지”를 통계적으로 체크하는 로직을 염두에 두기로 했다. 사용자의 직관이 데이터를 앞질렀던 사례다.

home_assistant — 음성 출력이 안 들린 진짜 이유: fallback이 가로챈 응답

OpenRouter 키를 새로 발급받고 나서도 음성이 들리지 않는다는 보고가 들어왔다. 처음엔 단순히 키 등록이 늦게 반영된 거라 생각해 브리지 재시작만 처리했다. 하지만 로그를 다시 보니 conversation.py 수정으로 TTS 자체는 정상 동작 중이었다. Voice PE로 오디오가 잘 전송되고 있었다.

그런데 문제는 fallback 응답이었다. Claude가 메인 프로젝트에서 무언가 명령을 실행하는 동안 타임아웃이 걸리면, fallback 로직이 “메인 프로젝트에 전달할게요” 같은 응답을 TTS로 보내고 있었다. 사용자 입장에서는 “음성이 들리긴 하는데 내용이 엉뚱하다”는 혼란을 겪은 것.

진짜 원인은 fallback이 멋대로 프로젝트 전달 메시지를 출력하도록 하드코딩된 데 있었다. 수정 방향은 두 가지였다:

  1. fallback에서 프로젝트 전달 메시지를 아예 제거
  2. 대신 “다시 말해달라” 는 안전한 응답으로 교체

2번을 선택했다. 이유는 fallback 자체가 예외 상황에서만 트리거되므로, 사용자가 음성으로 재시도할 수 있는 메시지가 더 자연스럽다고 판단했다.

# before
fallback_response = "메인 프로젝트에 전달할게요"

# after
fallback_response = "죄송합니다, 다시 한 번 말씀해주시겠어요?"

검증: fallback 트리거 조건을 인위로 만들어(Claude 응답 지연) “다시 말해달라”는 TTS가 정상 출력되는 걸 확인했다. OpenRouter 키도 새로 반영 후 만료 없이 동작 중.

propermarket-marketing — MeloTTS Docker화에서 만난 MeCab의 이중성

오픈소스 한국어 TTS를 Docker로 올리는 작업이었다. MeloTTS를 선정했고, 로컬에서 melo_server.py를 띄워 HTTP 200 + 57KB mp3 응답을 확인한 상태였다. Dockerfile도 작성했고 docker-compose.melo.yml도 준비했다. 그런데 빌드가 계속 실패했다.

처음엔 python-mecab-ko 설치 과정에서 네이티브 빌드가 깨진 줄 알았다. uv pip install python-mecab-ko가 libmecab.so를 찾지 못해 실패하는 에러였다. apt로 mecab을 설치해 해결했다. 그 다음은 unidic-lite 다운로드 실패 — ENV UNIDIC_LITE_URL을 명시적으로 설정해 우회했다.

하지만 가장 결정적인 벽은 따로 있었다.

MeloTTS는 한국어만 써도 cleaner.py에서 일본어 g2p를 무조건 import 한다. 일본어는 mecab-python3, 한국어는 python-mecab-ko — 두 네이티브 MeCab은 같은 환경에서 공존이 불가능하다.

방금 python-mecab-ko를 추가하자 import melo.text.japanese 단계에서 mecab-python3가 없다며 실패했다. 두 라이브러리가 모두 libmecab을 요구하는데, 각각 다른 버전의 바인딩을 쓰고 있어 충돌이 발생한 것.

버린 옵션들:

  • python-mecab-ko를 제거하고 pure Python tokenizer를 찾기 → MeloTTS가 요구하는 인터페이스와 불일치
  • mecab-python3와 python-mecab-ko를 동시에 설치 → import 단계에서 segfault
  • 한국어 g2p만 따로 떼어내서 MeloTTS를 포크 → 유지보수 부담이 큼

채택한 해법: Docker 빌드 단계에서 python-mecab-ko만 설치하고, 일본어 import가 실패해도 한국어 추론에는 영향이 없는 구조로 만들었다. 정확히는 melo/text/japanese.py가 import 시 MeCab을 찾지 못하면 None을 반환하게 패치하지 않고, Dockerfile에서 일본어 의존성을 아예 설치하지 않는 쪽을 선택했다. MeloTTS 소스에 try-except ImportError가 이미 있어서, 일본어 import 실패 시 해당 언어 처리 루틴이 fallback된다는 걸 로그로 확인했다.

결과: Docker 컨테이너가 정상 빌드되고, /tts 엔드포인트에서 한국어 mp3를 57KB로 뱉어냈다. generate_tts.py로 E2E 테스트도 통과.

남은 과제: 만약 나중에 일본어 TTS가 필요해지면, 별도 Docker 이미지로 분리하거나 mecab-python3와 python-mecab-ko의 공존 방안을 다시 찾아야 한다. 지금은 한국어만 쓴다는 전제로 일단락했다.

오늘의 한줄

남이 만든 라이브러리의 import 그래프를 끝까지 추적하지 않으면, “되는 줄 알았던” 기능이 가짜 양성일 수 있다 — MeCab 충돌과 fallback 버그가 가르쳐준 교훈.