k6 자동 측정 스크립트의 jq 문법 오류 해결
💡부하 테스트는 완료됐지만 결과 계산 단계에서 오류가 발생했다
k6를 이용한 재고 차감 부하 테스트는 정상적으로 완료됐지만, Micrometer 스냅샷과 k6 결과를 조합해 최종 판정을 생성하는 단계에서 jq 문법 오류가 발생했다.
jq: error: syntax error, unexpected if
jq: error: May need parentheses around object key expression
jq: 3 compile errors
부하 요청과 측정값 수집은 이미 끝난 상태였고, JSON 결과를 계산하는 jq 코드만 실행되지 않은 상황이었다.
문제가 발생한 코드
💡객체의 값으로 사용한 if 표현식을 괄호로 감싸지 않았다
| .average = {
lockWaitMs:
if .delta.waitAcquired > 0 then
(
($B.wait.acquired.totalTime
- $A.wait.acquired.totalTime)
/ .delta.waitAcquired
) * 1000
else
null
end,
lockHoldMs:
if .delta.holdSuccess > 0 then
(
($B.hold.success.totalTime
- $A.hold.success.totalTime)
/ .delta.holdSuccess
) * 1000
else
null
end
}
jq 객체는 다음과 같이 키와 값으로 구성된다.
{
key: value
}
단순한 숫자나 필드 조회는 바로 값으로 사용할 수 있지만, if-then-else-end와 같이 여러 구문으로 구성된 복합 표현식은 객체의 값이라는 범위를 명확하게 나타내기 위해 괄호로 감싸야 한다.
# 문법 오류
{ value: if true then 1 else 0 end }
# 정상
{ value: (if true then 1 else 0 end) }
괄호가 없으면 jq 파서는 value: 뒤에 나온 if를 객체의 값으로 정상 해석하지 못하고 unexpected if 오류를 발생시킨다.
수정한 코드
💡if 표현식 전체를 괄호로 감쌌다
| .average = {
lockWaitMs:
(if .delta.waitAcquired > 0 then
(
($B.wait.acquired.totalTime
- $A.wait.acquired.totalTime)
/ .delta.waitAcquired
) * 1000
else
null
end),
lockHoldMs:
(if .delta.holdSuccess > 0 then
(
($B.hold.success.totalTime
- $A.hold.success.totalTime)
/ .delta.holdSuccess
) * 1000
else
null
end)
}
수정 전후의 계산 방식은 동일하다. 괄호는 계산 결과를 바꾸는 것이 아니라, if 표현식 전체가 객체 필드의 값이라는 것을 jq 파서에 명확하게 알려준다.
코드의 동작 원리
💡스냅샷 A와 B의 차이를 이용해 측정 구간의 평균 시간을 계산한다
Micrometer Timer의 COUNT와 TOTAL_TIME은 애플리케이션 시작 이후 누적된다. 따라서 본 측정 직전의 스냅샷 A와 측정 직후의 스냅샷 B를 저장하고 두 값의 차이를 계산한다.
측정 구간 요청 수
= B.count - A.count
측정 구간 누적 시간
= B.totalTime - A.totalTime
평균 시간
= 측정 구간 누적 시간 / 측정 구간 요청 수
Timer의 시간 단위는 초이므로 마지막에 1000을 곱해 밀리초로 변환한다.
평균 락 대기시간(ms)
= (대기 TOTAL_TIME 차이 / 락 획득 COUNT 차이) × 1000
평균 락 점유시간(ms)
= (점유 TOTAL_TIME 차이 / 성공 COUNT 차이) × 1000
요청 수가 0인 경우에는 0으로 나누는 오류를 방지하기 위해 평균값을 null로 기록한다.
검증 결과
💡기존 측정 파일을 이용해 부하 테스트를 다시 실행하지 않고 결과를 복구했다
- 오류가 발생하기 전에 k6 결과와 Micrometer 스냅샷 A·B가 각각 JSON 파일로 저장되어 있었다.
- 이번 오류는 측정 단계가 아니라 저장된 원본을 가공하는 최종 jq 계산 단계에서 발생했다.
- 따라서 부하 테스트를 다시 실행하지 않고, jq 문법을 수정한 뒤 기존 원본 파일을 입력으로 결과 계산 단계만 다시 실행해 verdict를 생성했다.
- 현재 자동화 스크립트가 중간 단계부터 자동 재개하는 것은 아니며, 복구 과정에서는 수정된 jq 계산 명령만 별도로 실행했다.
최종 판정: VALID
요청 수: 13,668건
성공 수: 13,668건
락 타임아웃: 0건
예상하지 못한 실패: 0건
락 소유권 상실: 0건
성공 TPS: 227.8
평균 API 응답시간: 883.75ms
API p95: 1,020.76ms
API p99: 1,191.37ms
평균 락 대기시간: 878.32ms
평균 락 점유시간: 4.09ms
재고 감소량, k6 성공 요청 수, 락 획득 수, 락 점유 완료 수가 모두 13,668건으로 일치해 동시성 정합성 검사도 통과했다.
배운 점
💡셸 문법 검사만으로는 내부 jq 문법까지 검증할 수 없다
bash -n은 셸 스크립트 문법만 검사하며 문자열 안에 작성된 jq 프로그램의 문법은 검사하지 않는다.- jq 코드가 포함된 자동화 스크립트는 실제 JSON 파일을 입력해 jq 프로그램까지 실행해봐야 한다.
- 측정 원본과 최종 가공 결과를 분리해 저장하면 결과 계산이 실패하더라도 부하 테스트를 다시 실행하지 않고 복구할 수 있다.
- 자동화 스크립트도 애플리케이션 코드처럼 정상 경로뿐 아니라 파싱 실패와 결과 파일 누락 같은 실패 경로를 검증해야 한다.
참고자료
'Projects > hub-eleven' 카테고리의 다른 글
| [리팩토링] 재고 API - (3) 동시성 처리 최적화 : Micrometer 도입 (0) | 2026.08.02 |
|---|---|
| [리팩토링] 재고 API - (3) 동시성 처리 최적화 : Micrometer 란? (0) | 2026.07.30 |
| [부하테스트] 재고 API - (2) k6 부하 테스트 환경과 개발 환경 분리 (0) | 2026.07.29 |
| [부하테스트] 재고 API - (1) k6로 테스트 해보기 (0) | 2026.07.28 |
| [동시성 처리] Redisson은 어떻게 Redis Pub/Sub을 이용해 재시도(Retry) 로직을 구현할 수 있을까? (0) | 2026.06.24 |