오늘 진짜 재밌는 작업을 했다.

처음에는 그냥 QCP 하나를 분석하는 일이었다. DB 접속 화면에서 username만 바꾸고 password를 비워두면 어떻게 되는지 확인하는 정도였다.

그런데 하다 보니 이야기가 점점 커졌다.

보안 패치
→ 실제 브라우저 검증
→ 직접 gRPC 호출 검증
→ Heroshot 증빙 이미지
→ Playwright 증빙 도구
→ dev-scripts 재현 프레임워크

버그 하나 잡으러 갔다가, 다음 버그들을 더 빨리 잡기 위한 작은 공장까지 만들고 나온 느낌이었다.

시작은 이상한 조합 하나였다

문제의 핵심은 credential pair였다.

DB connection에는 보통 저장된 계정 정보가 있다.

Username: querypie
Password: <strong>*</strong>****

그런데 사용자가 화면에서 username만 root로 바꾸고 password는 비워둔다고 해보자.

Username: root
Password: 비어 있음

이때 시스템이 잘못 처리하면 이런 위험한 조합이 만들어질 수 있다.

username = root
password = 기존에 저장돼 있던 password

이건 느낌이 좋지 않다.

사용자는 password를 새로 입력하지 않았는데, 시스템 내부에서 “새 username + 기존 password”가 합쳐지는 셈이기 때문이다.

username과 password는 따로 노는 값이 아니라 하나의 credential pair다. username을 바꿨다면 password도 다시 입력해야 한다.

그래서 정책은 단순하게 정했다.

username이 바뀌었으면 password도 반드시 다시 입력해야 한다.

앞문도 잠그고, 뒷문도 잠갔다

패치는 두 군데에 넣었다.

첫 번째는 Front-end다.

사용자가 화면에서 username을 바꾼 뒤 password를 비운 채 Connect를 누르면, 바로 UI에서 막는다.

Password is required when username is changed.

여기서 중요한 건 단순히 에러 문구만 띄우는 게 아니다. 실제 Engine 접속 요청이 나가지 않아야 한다.

두 번째는 Engine이다.

브라우저에서 막는 것만으로는 부족하다. 누군가 브라우저를 우회해서 gRPC를 직접 호출할 수도 있다.

그래서 Engine의 SessionService.open에서도 같은 조건을 다시 검사하게 했다.

username changed + password omitted
=> InvalidArgument

이 구조가 마음에 들었다.

UI에서 1차 차단
Engine에서 2차 차단

앞문도 잠그고, 뒷문도 잠근 셈이다.

그런데 “진짜로 막히는지” 보여주고 싶었다

코드 패치와 단위 테스트만으로도 기능 검증은 가능하다.

하지만 이번에는 누가 봐도 이해할 수 있는 증빙을 만들고 싶었다.

그래서 실제 브라우저를 움직였다.

Playwright
→ 로컬 Google Chrome 실행
→ QueryPie UI 접속
→ connection 선택
→ username 변경
→ password 비움
→ Connect 클릭

실제로 테스트한 흐름은 이랬다.

1. Databases 화면 접속
2. QueryPie Connections 펼침
3. querypie-metadb 선택
4. Username을 querypie에서 root로 변경
5. Password는 빈 값으로 둠
6. Connect 클릭
7. validation 메시지 확인
8. Engine open 요청이 발생했는지 네트워크 감시

결과는 깔끔했다.

Validation visible: True
Engine open request count after click: 0

이 두 줄이 꽤 중요하다.

첫 번째 줄은 화면에 validation 메시지가 실제로 떴다는 뜻이다.

두 번째 줄은 Connect를 눌렀지만 실제 DB 접속 요청이 Engine까지 가지 않았다는 뜻이다.

즉 브라우저가 화면에서 먼저 막았고, 서버에 접속 요청도 보내지 않았다.

그래도 서버를 직접 때려봤다

UI에서 막힌다고 끝내면 아쉽다.

보안 검증에서는 늘 이런 질문을 해야 한다.

“그럼 브라우저를 우회하면?”

그래서 grpcurl로 Engine을 직접 호출했다.

요청은 이런 식이었다.

SessionService.open
username = root
password = omitted
objectType = CLUSTER

결과는 기대한 그대로였다.

InvalidArgument
Password is required when username is changed.

이제 자신 있게 말할 수 있다.

화면에서도 막히고,
Engine을 직접 호출해도 막힌다.

Heroshot으로 증빙을 예쁘게 묶었다

여기서 또 한 번 욕심이 났다.

Playwright 캡처와 terminal output을 파일로만 남기면 나중에 읽기 어렵다. 그래서 Heroshot을 이용해 하나의 증빙 이미지로 묶었다.

처음에는 2x2 카드 형태로 만들었다.

1. 실제 브라우저 baseline
2. 브라우저 validation 화면
3. UI probe summary
4. Direct Engine gRPC probe

그런데 문제가 있었다.

1440px 이미지 안에 UI 화면 두 장과 터미널 두 개를 넣으니 글씨가 작았다.

그래서 고해상도 버전을 다시 만들었다.

기존: 1440 x 1503
신규: 2880 x 3006

이제 UI label, validation 메시지, gRPC 결과가 훨씬 잘 보인다.

물론 token과 cookie는 전부 마스킹했다.

qp_access_token=[REDACTED]

증빙은 예쁘게 만들수록 좋지만, 보안값은 절대 예쁘게 노출하면 안 된다.

여기서 끝냈으면 그냥 좋은 패치였다

그런데 작업하면서 이런 생각이 들었다.

“이거 다음 QCP에서도 또 필요하지 않을까?”

분명 또 온다.

어떤 QCP는 실제 UI 조작이 필요하고, 어떤 QCP는 API 응답을 봐야 하고, 어떤 QCP는 gRPC를 직접 때려야 한다.

그때마다 Playwright 스크립트 새로 만들고, token redaction 만들고, screenshot 저장하고, Heroshot 설정 만들면 너무 아깝다.

그래서 재사용 가능한 기반을 만들었다.

qcp/knowledge-lab/test-tools/playwright-evidence/

이제 새로운 증빙 시나리오는 얇은 scenario 파일만 만들면 된다.

공통 기반이 처리하는 일은 꽤 많다.

- E2E token 로드
- 로컬 Chrome 실행
- QueryPie cookie 주입
- 실제 UI 조작
- API/gRPC/CLI probe 실행
- token/password redaction
- terminal-style evidence PNG 생성
- proof.html 생성
- Heroshot 최종 이미지 생성
- Markdown evidence report 생성

이건 그냥 스크립트 하나가 아니라, 작은 증빙 파이프라인이다.

dev-scripts도 같이 정리했다

작업하다 보니 dev-scripts/ 아래에도 재현 스크립트가 꽤 많았다.

api-token-allowed-zone-repro/
mysql-initial-err-repro/
qcp-5440-dac-due-permissions/
slack-dm-workflow-repro/
dml-snapshot-repro/
sac-repro/

각각 필요한 일을 잘 하고 있었지만, 공통 로직이 반복되고 있었다.

state 저장/로드
secret redaction
HTTP 호출
grpcurl 호출
mysql helper
subprocess 실행
JSON event 출력
runtime health 확인

그래서 여기도 공통 기반을 만들었다.

dev-scripts/lib/qpie_repro/

구성은 이렇게 나눴다.

context.py
runtime.py
state.py
redact.py
http.py
grpc.py
db.py
shell.py
result.py

이제 새 repro 스크립트는 표준 명령을 갖는다.

doctor
setup
reproduce
verify
cleanup
report

각 명령의 의미도 명확하다.

doctor    사전 조건 확인
setup     fixture 준비
reproduce 재현 실행
verify    기대 결과 확인
cleanup   fixture 정리
report    결과 저장

이 구조가 좋은 이유는 역할이 분리되기 때문이다.

dev-scripts = 재현 실행 도구
qcp/knowledge-lab/test-results = 실행 결과와 증빙
playwright-evidence = 브라우저 + Heroshot 증빙 생성

이제 dev-scripts가 단순한 스크립트 창고가 아니라, 재현 자동화 기반에 가까워졌다.

검증도 빼먹지 않았다

만들었으면 돌려봐야 한다.

공통 라이브러리와 템플릿 문법 검사:

PASS

dev-scripts 전체 Python 문법 검사:

PASS

표준 템플릿 실행:

doctor    PASS
setup     PASS
reproduce PASS
verify    PASS
report    PASS

새 scaffold로 임시 repro를 생성하고 doctor까지 실행했다.

PASS

그리고 QCP-5442 Engine gRPC 재검증도 다시 했다.

PASS
InvalidArgument
Password is required when username is changed.

여기까지 오면 기분이 꽤 좋다.

단순히 “패치했습니다”가 아니라, “다음에도 재사용할 수 있게 길을 닦았습니다”가 된다.

커밋도 일부러 나눴다

제품 코드, 증빙 도구, dev-scripts 기반을 한 커밋에 섞지 않았다.

대략 이런 식으로 나눴다.

fix(apps/engine): Require password when overriding username
docs(qcp): Add QCP-5442 validation evidence toolkit
chore(dev-scripts): Add reusable repro helpers

이렇게 나눠두면 나중에 리뷰하기 쉽다.

제품 패치만 보고 싶으면 첫 번째를 보면 되고, 증빙 파이프라인을 보고 싶으면 두 번째를 보면 되고, 재현 프레임워크를 보고 싶으면 세 번째를 보면 된다.

작업이 커질수록 커밋을 잘 나누는 게 정말 중요하다.

오늘의 교훈

이번 작업에서 제일 좋았던 건 이거다.

버그 하나를 고치면서, 다음 버그를 더 빠르고 더 정확하게 검증할 수 있는 기반까지 만들었다.

처음엔 credential pair 검증 하나였다.

결과적으로는 이런 것들이 남았다.

1. Front + Engine 이중 방어 패턴
2. 실제 브라우저 기반 UI 검증
3. 직접 gRPC probe 검증
4. Heroshot 고해상도 증빙 이미지
5. Playwright evidence toolkit
6. dev-scripts 공통 repro framework

다음부터는 QCP 검증할 때 이렇게 말할 수 있다.

실제 화면에서 돌려봤고,
서버 직접 호출도 때려봤고,
증빙 이미지까지 자동으로 뽑아놨어요.

이건 꽤 든든하다.

한 줄 요약

이번 작업은 단순한 보안 패치가 아니었다.

QueryPie QCP 재현과 검증을
실제 브라우저 + API/gRPC + Heroshot 증빙까지
재사용 가능한 형태로 표준화한 작업

이었다.

버그 하나 잡으러 갔다가, 재현 자동화 공장을 하나 세우고 나왔다.

이런 날은 개발자로서 꽤 신난다.