Projects/hub-eleven

[트러블슈팅] 재고 API - (3) 동시성 처리 최적화 : k6 자동 측정 스크립트의 jq 문법 오류 해결

annovation 2026. 8. 4. 23:59

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의 COUNTTOTAL_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 프로그램까지 실행해봐야 한다.
  • 측정 원본과 최종 가공 결과를 분리해 저장하면 결과 계산이 실패하더라도 부하 테스트를 다시 실행하지 않고 복구할 수 있다.
  • 자동화 스크립트도 애플리케이션 코드처럼 정상 경로뿐 아니라 파싱 실패와 결과 파일 누락 같은 실패 경로를 검증해야 한다.

참고자료