요약 — 오늘 한 것과 왜 했는가
단일 바이너리가 STAGING/PROD를 런타임에 결정하도록 듀얼 환경을 통합한 뒤, 이전 구조의 흔적들을 한 번에 털어냈다. 핵심은 버전 불일치로 인한 업데이트 미감지 사고를 원천 차단하는 가드를 파이프라인에 박는 것이었다.
| 작업 | 목적 | 결과 |
|---|---|---|
| 인스톨러 트랙 분리 | 책임 경계 명확화 | 패키징 스크립트 ↔ NSIS 개선 후보 분리 |
| 버전 0.17.3 단일화 | 중복 정의 제거 | CMake·소스·NSI 전체 동기화 |
| 데드 코드 제거 | 참조 없는 모듈 정리 | update_checker 300줄 순감 |
| 환경 라벨 마커 폐지 | 구 구조 잔재 제거 | CountDots·DotsToEnvName 등 NSI 180줄 정리 |
| 다운그레이드 완화 | 롤백 테스트 편의 | MB_OK → MB_OKCANCEL |
| 업로드 가드 추가 | 사고 원천 차단 | NSI 버전 ≠ 인자 버전 시 업로드 중단 |
| Origin 화이트리스트 검증 도구 | 회귀 점검 | 정상/비정상 Origin 직접 주입 스크립트 |
배경 — 통합은 끝났지만 흔적이 남아 있었다
이전 구조에서는 STAGING 빌드와 PROD 빌드가 별도였다. 빌드 타임에 환경 라벨을 박고, 인스톨러도 환경별로 두 벌 존재했다. 듀얼 환경 통합 이후 단일 바이너리가 런타임에 환경을 결정하게 됐지만, 코드와 스크립트 곳곳에 이전 구조의 흔적이 남아 있었다.
- CMakeLists, 소스 상수, NSI 파일이 각자 다른 버전을 들고 있었다
- 참조가 0건인 업데이트 체커 모듈이 빌드에 등록된 채 있었다
- 인스톨러가 설치 시점에 STAGING/PROD 마커를 레지스트리에 박는 로직이 살아있었다
- 업로드 배치 스크립트에 버전 정합성을 확인하는 장치가 없었다
1. 인스톨러(NSIS) 개선 트랙 분리
릴리스 패키징 스크립트(코드 사이닝 → 인스톨러 빌드 → 인스톨러 사이닝)를 점검하다가 NSIS 쪽 결함이 섞여 들어와 혼선이 생겼다. 두 트랙을 같이 다루면 어디서 문제가 터졌는지 추적이 두 배로 어렵다.
결론: “패키징 스크립트는 정합성 OK, NSIS 개선은 별도 트랙”으로 명시적으로 분리했다.
분리된 NSIS 개선 후보 (우선순위 낮음, 별도 문서 관리):
dist/위생 가드 — 로그·임시 산출물이 인스톨러에 포함되지 않도록.nsi파일 단일화 — 활성 NSI 두 벌 공존 → 정본 하나로PRODUCT_VERSION외부 주입 —/DPRODUCT_VERSION=%VER%로 호출 시점 주입- OutFile 명명 규약 단일화
- 인스톨러 빌드 자동화 스크립트
- protocol command에 환경 마커 명시 — 한 PC에 두 환경 공존 시나리오 대비
- NSI 파일명 와일드카드 매칭 모호성 가드
명시적 비목표: “환경별 dist 분리”(듀얼 환경 통합 결정으로 폐기), “NSIS 결함을 패키징 스크립트 안에서 보강”(책임 경계 흐림으로 거부).
2. 버전 0.17.3으로 단일화
CMakeLists, 소스코드 상수, NSI 파일(두 벌)이 각자 버전을 들고 있었다. 이상적으로는 단일 출처에서 모두 주입받는 구조가 맞지만, 일단 모든 정의를 0.17.3으로 동기화하는 것을 우선했다.
런타임이 클라이언트에게 회신하는 버전은 레지스트리의 DisplayVersion을 단일 출처로 잡았다. NSI가 설치 시점에 박는 값이기 때문에 가장 신뢰할 만한 출처다.
3. 데드 코드 제거 — 약 300줄 순감
업데이트 체커 모듈이 두 벌 있었다. update_checker.{c,h}는 참조가 0건임을 확인했다. 빌드 등록에서 제외하고 파일째 삭제했다. 실제 사용 경로는 다른 한 벌로 단일화했다.
# 제거된 파일
update_checker.c (~200줄)
update_checker.h (~100줄)
# CMakeLists.txt에서 참조 제거
# 실제 사용: updater_module.c 단일 경로로 유지
4. 인스톨러 환경 라벨 마커 폐지 — NSI 180줄 정리
이전 구조에서는 인스톨러가 설치 시점에 STAGING/PROD를 판별해 레지스트리에 환경 마커를 박았다. 듀얼 환경 통합 이후에는 런타임이 환경을 결정하므로 이 마커는 의미가 없어졌다.
제거 대상: CountDots, DotsToEnvName 함수, 레지스트리 환경 마커 쓰기 블록 전체. NSI에서만 약 180줄 정리됐다.
5. 다운그레이드 차단 완화 — MB_OK → MB_OKCANCEL
기존에는 다운그레이드 시도가 MB_OK로 무조건 차단됐다. 환경 리셋이나 롤백 테스트 시 매번 막혀서 불편했다. 의도적 다운그레이드는 허용하되 확인 절차를 두는 것으로 완화했다.
; 변경 전
MessageBox MB_OK "다운그레이드는 지원되지 않습니다."
Abort
; 변경 후
MessageBox MB_OKCANCEL "현재 버전보다 낮습니다. 계속하시겠습니까?" IDOK proceed
Abort
proceed:
6. 업로드 배치에 버전 가드 추가 ← 핵심
가장 위험했던 시나리오는 이것이다. 인스톨러의 PRODUCT_VERSION과 업로드 배치에 넘긴 인자 버전이 어긋난 채로 GCS에 올라가는 경우다.
클라이언트는 레지스트리 DisplayVersion을 보고 최신 여부를 판단한다. 트리거(GCS에 올라간 파일명)와 인스톨러 내부 버전이 어긋나면 클라이언트가 영원히 업데이트를 감지하지 못하는 사고가 발생한다. 사용자 입장에서는 최신 버전이 있어도 업데이트 알림이 오지 않는다.
업로드 배치 실행 흐름 (가드 추가 후)
NSI 파일에서 !define PRODUCT_VERSION 추출
↓
인자로 받은 버전과 문자열 비교
↓
불일치?
↙ ↘
Yes No
업로드 중단 GCS 업로드 진행
에러 출력
# 배치 스크립트 핵심 로직 (의사코드)
NSI_VER=$(grep '!define PRODUCT_VERSION' installer.nsi | awk '{print $3}')
ARG_VER=$1 # 호출 시 전달받은 버전
if [ "$NSI_VER" != "$ARG_VER" ]; then
echo "ERROR: NSI 버전($NSI_VER) ≠ 인자 버전($ARG_VER) — 업로드 중단"
exit 1
fi
# 검증 통과 후 GCS 업로드
gsutil cp installer_v${ARG_VER}.exe gs://bucket/Apps/APPNAME/
추가로 스테이징 버전 형식을 4파트(x.y.z.w)에서 3파트(x.y.z)로 운영과 통일했다. 환경별로 버전 표기가 달라서 생기는 비교 오류를 없앴다.
7. 부가물 — Origin 화이트리스트 raw 검증 도구
WebSocket 서버의 Origin 화이트리스트가 의도한 대로 동작하는지, 정상/비정상 Origin을 직접 주입해서 확인하는 소형 Node.js 스크립트를 추가했다. 회귀 점검 용도다.
// ws_origin_test.js (개요)
const cases = [
{ origin: 'https://allowed.example.com', expect: 'PASS' },
{ origin: 'https://evil.example.com', expect: 'REJECT' },
{ origin: null, expect: 'REJECT' },
];
for (const c of cases) {
const result = checkOriginWhitelist(c.origin);
console.log(`${c.origin ?? 'null'}: ${result === c.expect ? '✓' : '✗ FAIL'}`);
}
회고
“통합은 끝났다”고 선언한 뒤에도 이전 구조의 흔적은 한참 남아 있다. 한 번에 다 털 수 없고, 사고 위험이 큰 것부터 차례로 잡아가는 게 현실적이다. 오늘 작업의 우선순위도 그 기준으로 정했다 — 데드 코드나 NSIS 정리보다 업로드 가드가 먼저였던 이유다.
| 교훈 | 적용 |
|---|---|
| 책임 경계가 흐려지면 디버깅이 두 배로 어렵다 | 패키징 스크립트 ↔ NSIS 개선 트랙을 명시적으로 분리 |
| 사고가 한 번이라도 나면 사용자가 영원히 영향받는 경로는 가드를 먼저 박는다 | 업로드 배치에 버전 정합성 검증 추가 |
| “비목표”를 명시하는 것도 작업의 일부다 | 폐기·거부 항목을 문서에 명시해 나중에 같은 고민을 반복하지 않도록 |
답글 남기기