Joyuno's Dev Blog

2026-09-09 (수) 개발일지

dev-blog

오늘의 트러블슈팅 — 주석 한 칸이 파서를 멈추게 한 날

오늘은 세 가지 프로젝트에서 각각 다른 성격의 이슈를 잡았다. Logpresso 교육 플랫폼은 설계 검증 단계에서 Wave 1의 계약 위반 11건을 찾아내는 데 집중했고, LG CNS 쪽 프로시저 등록에서는 주석의 공백 규칙 하나가 문서 6페이지를 깨뜨리는 걸 발견했다. 로그프레소 테스트 VM 설치는 실측 검증 단계에서 계획과 현실이 두 군데 다른 걸 확인하고, 파이썬 인터프리터 고정 문제까지 겹쳐서 설치 오류를 추적했다.


LG CNS 프로시저 등록 — #--------- 공백 없는 주석이 파서를 깨뜨린 이유

프로시저 등록 중 set from = nvl($("from"), datetime) 같은 표현식에서 “올바르지 않은 쿼리 문자열” 오류가 났다. 처음에는 매개변수 파싱 문제로 좁혀서, table from=$("from") 같은 구문이 공식 지원 문법인지부터 의심했다. 문서에 “매개변수는 표현식을 입력할 수 있는 자리에만” 쓸 수 있다는 구절을 보고 테이블 이름 자리에는 매개변수가 안 들어가는 줄 알았다.

문서를 다시 보니 내가 틀렸다. table from=$("...") 는 공식 지원 문법이었다. 진짜 원인은 다른 데 있었다.

문서 전체를 스캔하던 중 문법 규칙 한 줄이 눈에 들어왔다.

# 뒤에 공백문자가 필요합니다. 공백문자가 없으면 주석 처리가 되지 않습니다.

#--------- 는 # 바로 뒤가 하이픈이라 주석이 아니다. 파서는 이걸 #--------- 라는 이름의 명령어로 읽고 실패했다. 문서 6페이지에 걸쳐 있는 구분선 주석 #--------- 검색 구문 처리 가 전부 깨지고 있었다.

시도한 가설들

  • table 명령어의 매개변수 문법 문제 → 문서 확인 결과 공식 지원이라 가설 폐기
  • *: 접두사로 테이블 주소를 명시하지 않아서 생긴 문제 → *: 를 붙여도 동일 오류
  • 쿼리 문자열 자체의 인코딩 문제 → nvl($("from"), ...) 구문은 정상

해결: #--------- → # --------- 로 하이픈 앞에 공백 한 칸 추가. 수정 후 해당 페이지 6건의 파싱이 전부 통과했다.

검증: 수정한 문서로 table from=$("from") ... 프로시저 등록 성공. 오류가 났던 쿼리 문자열 3건 재시험 → 전부 통과.

남은 과제: 이 저장소의 다른 문서들도 같은 패턴(#--- 구분선)을 쓰고 있을 가능성이 있다. 전체 스캔은 다음 작업으로 미뤘다.


Logpresso 교육 플랫폼 Wave 1 — 적대적 검증이 잡아낸 계약 위반 11건

설계 청사진 3안(mvp-lazy, trd-strict, learner-ux) 중 mvp-lazy가 41점으로 채택된 뒤, PRD/TRD 렌즈의 적대적 검증을 돌렸다. 검증 방법은 단순했다: 설계 문서가 PRD §5~§11과 TRD T01~T30의 수락 기준을 문자 그대로 만족하는가를 확인하는 것.

첫 번째 BLOCKER는 미리보기 동시 1개 초과 시 409를 내야 하는 계약이었다. backend/app/jobs.py에서 미리보기 중복 검사 로직이 빠져 있었다. PREPARING → READY 상태 전이 시 같은 mission_version_id + user_id 조합의 진행 중 시도가 있는지 확인하지 않고 통과시켰다.

두 번째는 access-log에 Authorization 헤더가 유출되는 보안 위반. FastAPI의 기본 로깅 미들웨어가 요청 헤더를 전부 기록했는데, 인증 관련 로그에서 Bearer <TOKEN> 이 그대로 남았다. main.py에 헤더 화이트리스트 필터가 없었다.

발견한 11건의 분류 | 범주 | 건수 | 대표 사례 | |—|—|—| | BLOCKER | 3 | 미리보기 동시 실행 미검증, 토큰 로그 유출, 임대 만료 후에도 러너 작업 허용 | | MINOR | 5 | 스키마 인덱스 누락 2건, 500 에러 시 응답 봉투 불일치, CORS 누락, created_at 기본값 누락 | | INFO | 3 | 주석 불일치, 문서와 실제 시그니처 다름, 불필요한 Optional 타입 |

해결: jobs.py에 get_active_attempt() 검사 로직 추가, main.py에 WHITELIST_HEADERS 필터(허용 헤더만 로깅), deps.py에 access_log_clean 미들웨어. conftest.py에 관련 테스트 케이스 3개(test_jobs_lease.py, test_keys.py, test_state_machines.py) 추가.

시행착오: 처음엔 “검증 안 된 기능이 아니라 테스트 누락”이라고 생각했다. 하지만 create_all로 실제 테이블을 만들고 TestClient로 예외 경로를 직접 때려보니 테스트가 아니라 코드 자체가 조건을 검사하지 않고 있었다.

검증: 임시 스크립트로 Authorization 로그 유출 재현 → 수정 후 같은 요청에서 "[REDACTED]" 로 치환됨 확인. 미리보기 2개 동시 생성 요청 → 409 CONCURRENT_PREVIEW_LIMIT 반환 확인.

남은 과제: 나머지 8건 중 5건(MINOR)은 다음 Wave 2에서 처리 예정. BLOCKER 3건만 이번 Wave 1에서 핫픽스로 커밋됐다.


Logpresso 테스트 VM — 실측 검증이 드러낸 계획과 현실의 두 군데 차이

VM 4대(COLL-yh, COLL2-yh, COLL3-yh, COLL4-yh)에 Logpresso Sonar 5.0 설치를 마쳤다고 기록돼 있었지만, 검증 스크립트를 돌리자 두 군데에서 터졌다.

첫 번째는 .51에서 dnf가 패키지를 “이미 있음”이라고만 하고 실제 검증을 건너뛰는 문제. check_benign.py가 dnf install의 exit code 0만 보고 “OK”로 판정했는데, 실제로는 의존성 누락으로 셸 단계에서 터지고 있었다.

두 번째는 파이썬 인터프리터를 python3.12로 하드코딩한 게 문제였다. EL8(Rocky 8.10)에는 python3.12에 pip이 없고, pexpect 라이브러리가 3.6용이라 인터프리터 버전이 맞지 않았다. 07-설계 §5의 “하드코딩 금지”를 내가 정작 어기고 있었다.

실측 데이터 (5:02 PM 기준) | VMID | 이름 | 태그(의도) | 실제 IP | OS | 로그프레소 설치 | |—|—|—|—|—|—| | 106 | COLL-yh | 51 | .51 ✅ | Rocky 8.10 | 설치만 하고 검증 안 함 | | 113 | COLL2-yh | 52 | .52 ✅ | Rocky 8.10 | 정상 | | 109 | COLL3-yh | 53 | .53 ✅ | Rocky 8.10 | 정상 | | 110 | COLL4-yh | 54 | .54 ✅ | Rocky 8.10 | dnf 문제로 셸 단계 실패 |

원인 추적

  • ssh 키 인증 실패도 의심했지만, BatchMode=yes로 똑같이 실패해서 키 문제는 아님
  • 세 번의 실험: ssh + 키 + BatchMode=yes → Permission denied(키 없음), ssh + 비밀번호 화면 없음 → 120초 대기 후 타임아웃, subprocess 직접 호출 → 60초 안에 완료
  • 결국 subprocess의 화면 차단 방식이 원인이 아니라, 설치 스크립트 자체가 python3.12에 의존한 게 문제

해결: check_benign.py를 dnf install → rpm -q 실제 검증 + --exclude=already-installed 플래그 추가. patch_benign.py로 쓸 수 있는 파이썬(python3, python3.11, python3.12 순서로 탐색)을 찾는 방식으로 변경.

검증: 패치 후 .51에서 rpm -q로 실제 설치된 패키지 47개 확인, 셸 스크립트 정상 통과. FWD2-yh(54)는 OracleLinux ISO가 물린 채 25시간째 네트워크가 없어서 수동 복구 필요 — 이건 VM 템플릿 문제라 스크립트로 못 고친다.


오늘의 한줄

문서의 주석 규칙부터 설치 스크립트의 검증 누락까지 — 성공 판정을 믿지 말고, 실제 동작을 직접 때려보는 수밖에 없다.