트랙 AA1180분

측정 체계 설계

같은 Orbit 로그에서 “리드 타임” 중앙값을 구하면 33h, 62.7h, 5.9일, 9.3일이 모두 나온다. 시작을 업무 생성으로 잡는지 작업 시작으로 잡는지, 시간을 달력으로 세는지 업무 시간으로 세는지에 따라 달라진다. 두 사람이 같은 이름의 지표를 다르게 계산해 회의에 가져오면, 회의 시간은 정의를 맞추는 데 쓰인다. 이 지표는 어떤 결정에 쓰이는가. 같은 이름의 지표를 두 사람이 계산하면 같은 값이 나오는가. 이 모듈은 목표에서 질문과 지표를 끌어내고, 지표마다 척도와 운영적 정의(operational definition)를 정해 정의서로 남기고, 측정 계획(measurement plan)으로 묶는다.

진행
설명 50분, 실습 110분, 정리 20분
결과
지표 정의서 5개(pm-lab/metrics/), 측정 계획(pm-lab/measurement-plan.md)
명령
duration.sh, awk, gh project field-create, gh api graphql, templates/metric-spec.md
cd ~/gh-pm-lab
source pm-lab/env.sh
mkdir -p pm-lab/metrics

1목표에서 질문으로, 질문에서 지표로

지표를 먼저 고르면 “계산할 수 있는 것”을 재게 되고, 그 숫자로 무엇을 정할지는 나중에 찾게 된다. 바실리(V. Basili), 칼디에라(G. Caldiera), 롬바흐(H. D. Rombach)가 1994년에 정리한 목표-질문-지표(Goal-Question-Metric, GQM) 방법은 순서를 반대로 한다. 측정의 목표를 먼저 적고, 그 목표를 이뤘는지 판단하는 데 필요한 질문을 적고, 질문마다 답이 되는 지표를 붙인다.

목표는 네 부분으로 적는다. 목적(purpose, 예: 개선한다), 관심 속성(issue, 예: 적시성), 대상(object, 예: 변경 요청 처리 과정), 관점(viewpoint, 예: 프로젝트 관리자)이다. 1994년 글의 예시도 “프로젝트 관리자의 관점에서 변경 요청 처리의 적시성을 개선한다”는 목표에, “지금 처리 속도는 얼마인가”라는 질문과 평균 사이클 타임, 표준편차, 상한을 넘은 사례 비율이라는 지표를 붙였다. Orbit에 같은 틀을 쓰면 다음과 같다.

층Orbit
목표PM의 관점에서(관점) Orbit 업무 흐름(대상)의 예측 가능성(관심 속성)을 높여, 상태 보고에서 약속하는 완료 날짜가 지켜지게 한다(목적)
질문 1업무 하나를 시작하면 며칠 안에 끝나는가
질문 2끝나기까지 어디서 기다리는가
질문 3한 주에 몇 건을 끝내는가
질문 4끝낸 일을 다시 하는 일이 얼마나 있는가
질문 5지금 동시에 몇 건을 하고 있는가
질문지표이 숫자로 정하는 것
1처리 시간(작업 시작 → 마지막 업무 완료)의 중앙값과 85%상태 보고의 완료 예상일, 업무 하나를 약속할 때 쓰는 기간
2첫 리뷰 대기(PR 생성 → 첫 리뷰)의 중앙값과 85%리뷰 방식을 바꿀지
3주별 처리량(업무 완료 건수)마일스톤 완료 날짜 예측의 입력, 범위 조정
4다시 열린 사례 비율완료 조건과 QA 방식을 바꿀지
5진행 중 업무 수(WIP)새 업무를 시작할지, 진행 중 업무 한도를 바꿀지

이 다섯 지표가 이 모듈에서 정의서를 쓸 대상이다. 질문 하나에 지표가 여러 개일 수 있고, 지표 하나가 여러 질문에 답할 수도 있다. 어느 질문에도 연결되지 않는 지표는 계산하지 않는다.

2측정 프로세스의 정보 모델

ISO/IEC/IEEE 15939:2017(Systems and software engineering — Measurement process)은 ISO/IEC/IEEE 15288과 12207의 측정 프로세스를 자세히 풀어 쓴 표준이다. 이 표준의 정보 모델(information model)은 “누가 무엇을 알고 싶은가”에서 “어떤 시각을 기록하는가”까지를 층으로 나눈다. 층마다 이름을 붙여 두면, 숫자가 이상할 때 어느 층에서 틀렸는지 찾을 수 있다. 질문 1의 처리 시간으로 층을 채우면 다음과 같다.

층뜻처리 시간의 예
정보 요구(information need)결정을 위해 알아야 하는 것약속한 완료 날짜를 지킬 수 있는가
측정 가능한 개념(measurable concept)정보 요구를 잴 수 있는 성질로 옮긴 것업무 흐름의 속도와 그 흔들림
대상과 속성(entity, attribute)재는 대상과 그 성질업무 이슈, 사건의 시각
기본 측정(base measure), 측정 방법(measurement method)속성 하나를 값 하나로 바꾸는 규칙작업 시작 신호 시각, 업무 완료 시각. event-log.sh의 추출 규칙(실습 20, 22)
파생 측정(derived measure), 측정 함수(measurement function)기본 측정을 계산으로 묶은 값사례별 처리 시간 = 두 시각 사이의 업무 시간
지표(indicator), 분석 모델(analysis model)파생 측정을 요약해 판단에 쓰는 값기간별 처리 시간 중앙값과 85%
판단 기준(decision criteria)어떤 값이면 무엇을 하는지85%가 팀이 약속에 쓰는 기간보다 길어지면 원인을 찾는다
정보 산출물(information product)결정하는 사람이 받는 결과주간 보고의 흐름 절, 상태 보고의 일정 절

실습 22에서 고친 두 문제도 층으로 나눌 수 있다. 작업 시작이 18시 스냅숏에서 와서 첫 커밋보다 늦게 찍힌 문제는 측정 방법의 문제이고, 끝나지 않은 사례를 빼는 문제는 측정 함수의 문제다.

3척도와 허용되는 계산

스티븐스(S. S. Stevens)는 1946년 Science에 실은 글에서 측정 척도(scale of measurement)를 네 가지로 나누고, 척도마다 쓸 수 있는 통계를 정했다. 척도를 정하는 기준은 “값을 어떻게 바꿔도 뜻이 유지되는가”다. 순서 척도는 순서만 지키면 어떤 숫자를 붙여도 되므로, 붙인 숫자에 따라 결과가 바뀌는 계산(평균)은 뜻이 없다.

척도값 사이에 의미가 있는 것허용되는 계산GitHub 데이터의 예
명목(nominal)같다, 다르다건수, 비율, 최빈값area 라벨, 저장소, 담당자
순서(ordinal)위에 더해 크다, 작다위에 더해 중앙값, 분위수, 순위Priority(P0~P3), Size(S, M, L), Status 단계
구간(interval)위에 더해 차이의 크기. 0은 임의로 정한 점위에 더해 차이, 평균, 표준편차날짜, 시각
비율(ratio)위에 더해 비. 0은 “없음”위에 더해 비, 배수, 변동 계수처리 시간, 건수, 추정 공수(h)

날짜는 구간 척도다. 두 날짜의 차이(처리 시간)는 비율 척도가 되지만, “10월 12일은 10월 6일의 두 배”는 뜻이 없다. Priority는 순서 척도다. 키트의 samples/a-project-items.tsv는 우선순위를 P0~P3 네 단계로 쓰는 팀의 Project 항목 22건을 흉내 낸 예시 파일이다. 열은 이슈 번호, 영역, Priority, Status, 추정 공수(h)다.

head -4 github-pm-starter-kit/samples/a-project-items.tsv
tail -n +2 github-pm-starter-kit/samples/a-project-items.tsv | cut -f2,3 | sort | uniq -c
number	area	priority	status	estimate
101	design	P1	Done	8
102	engineering	P0	Done	16
103	engineering	P3	Todo	4
      1 design	P0
      4 design	P1
      6 design	P2
      3 engineering	P0
      4 engineering	P1
      1 engineering	P2
      3 engineering	P3

두 영역 중 어느 쪽 백로그가 더 급한가. 흔히 하는 방법은 P0~P3을 0~3으로 바꿔 평균을 내는 것이다. P3을 “사실상 보류”로 보고 간격을 넓혀 6으로 바꿔도 순서는 그대로다(P0 < P1 < P2 < P3). 두 방법으로 평균을 낸다. 숫자가 작을수록 급하다.

awk -F'\t' 'NR > 1 { n[$2]++; a[$2] += substr($3, 2); b[$2] += ($3 == "P3" ? 6 : substr($3, 2)) }
  END { for (k in n) printf "%s %d건: P0~P3을 0~3으로 셀 때 평균 %.2f, P3을 6으로 셀 때 평균 %.2f\n", k, n[k], a[k] / n[k], b[k] / n[k] }' \
  github-pm-starter-kit/samples/a-project-items.tsv | sort
design 11건: P0~P3을 0~3으로 셀 때 평균 1.45, P3을 6으로 셀 때 평균 1.45
engineering 11건: P0~P3을 0~3으로 셀 때 평균 1.36, P3을 6으로 셀 때 평균 2.18

0~3으로 세면 engineering(1.36)이 design(1.45)보다 급하고, P3을 6으로 세면 design(1.45)이 engineering(2.18)보다 급하다. 순서를 지키는 두 숫자 붙이기가 반대 결론을 낸다. 순서 척도에 허용되는 계산으로 다시 본다.

for a in design engineering; do
  printf '%s 중앙값 ' "$a"
  awk -F'\t' -v a="$a" 'NR > 1 && $2 == a { print $3 }' github-pm-starter-kit/samples/a-project-items.tsv | sort | awk '{ v[NR] = $0 } END { print v[int((NR + 1) / 2)] }'
done
awk -F'\t' 'NR > 1 { n[$2]++; s[$2] += $5; if ($3 == "P0" || $3 == "P1") top[$2]++ }
  END { for (k in n) printf "%s P0·P1 %d/%d건, 추정 공수 평균 %.1fh\n", k, top[k], n[k], s[k] / n[k] }' \
  github-pm-starter-kit/samples/a-project-items.tsv | sort
design 중앙값 P2
engineering 중앙값 P1
design P0·P1 5/11건, 추정 공수 평균 9.1h
engineering P0·P1 7/11건, 추정 공수 평균 15.5h

중앙값(design P2, engineering P1)과 상위 두 등급의 비율(5/11건, 7/11건)은 숫자를 어떻게 붙여도 바뀌지 않고, 둘 다 engineering 백로그가 더 급하다고 말한다. 추정 공수는 비율 척도라 평균(9.1h, 15.5h)을 내도 된다. 상태 보고에 “평균 우선순위”를 적는 대신 “P0·P1 비율”이나 등급별 건수를 적는다.

스토리 포인트와 Size 필드

S, M, L처럼 이름을 붙인 Size는 순서 척도다. 1, 2, 3, 5, 8처럼 숫자로 적은 스토리 포인트도 팀이 “8은 1의 여덟 배 일이다”라는 규칙을 지켜 매기지 않으면 순서 척도이며, 합계와 평균이 뜻을 가지려면 그 규칙이 먼저 확인되어야 한다. 업무 수를 세는 처리량은 비율 척도라 이런 문제가 없다.

4운영적 정의: 같은 이름, 다른 숫자

운영적 정의(operational definition)는 측정값을 얻는 절차를 다른 사람이 그대로 따라 할 수 있게 적은 것이다. 데밍(W. E. Deming)은 『Out of the Crisis』(1986)에서 운영적 정의가 기준(criterion), 그 기준에 맞는지 확인하는 검사(test), 그 결과로 내리는 판정(decision)을 갖춰야 한다고 썼다. 기간 지표라면 시작 사건, 끝 사건, 시간을 세는 방법, 제외할 사례, 시간대가 기준에 들어간다.

키트의 duration.sh는 시작 사건과 끝 사건과 시간 종류를 옵션으로 받아 사례마다 기간을 계산하고 분포를 요약한다. 정리한 Orbit 로그에 “리드 타임”의 정의 네 가지를 차례로 적용한다.

정의시작 사건시간중앙값85%평균포함하는 것
L1업무 생성달력9.3일14일9.7일백로그 대기, 밤과 주말
L2업무 생성업무 시간62.7h89.4h62.4h백로그 대기
L3작업 시작 신호달력5.9일8.7일5.9일밤과 주말
L4작업 시작 신호업무 시간33h53.9h35h일하는 시간대의 대기와 작업

L1과 L4는 같은 업무 257건을 쟀다. L2의 중앙값(62.7h)은 L4(33h)의 두 배 가까이 되는데, 차이는 대부분 업무를 만든 뒤 작업을 시작하기까지의 백로그 대기다. 실습 25에서 백로그 대기는 PM이 순서를 정한 결과라 줄일 대상으로 먼저 고르지 않았다. 팀의 흐름을 보는 지표로는 L4가, 요청한 사람이 기다린 기간을 보는 지표로는 L1이 맞다. 정의서에는 어느 정의를 어떤 질문에 쓰는지 적는다. 2단계의 compare.sh와 whatif.sh가 쓴 처리 시간은 L4다.

제외 조건도 정의의 일부다. 정리하지 않은 원본 로그에 L4를 적용하면 끝나지 않은 사례 3건이 따로 세어진다.

bash github-pm-starter-kit/duration.sh pm-lab/orbit-event-log.csv --start "작업 시작,첫 커밋,막힘" --clock business | tail -2
- 사례 260건: 계산 257건, 시작 사건 없음 0건, 끝 사건 없음 3건
- 평균 35h, 중앙값 33h, 85% 53.9h, 95% 64.7h, 최소 0.4h, 최대 84h

요약은 같지만, “끝 사건 없음 3건”을 보고서에 함께 적지 않으면 읽는 사람은 260건 모두가 계산에 들어갔다고 생각한다. 시간대도 적는다. 업무 시간은 지역 시각의 평일 09~18시로 센다. 한국 시각(+09:00)으로 기록된 로그를 협정 세계시(UTC) 기준으로 잘못 읽으면, 금요일 17:00(한국 시각)에 만든 PR은 금요일 08:00(UTC)이 되어 업무 시간 창 밖에 놓인다. 키트 스크립트는 로그에 적힌 지역 시각을 그대로 쓰므로, 로그를 만들 때의 --tz(실습 20)가 시간대의 정의가 된다.

5이미 나와 있는 지표 묶음

소프트웨어 팀의 지표를 묶어 제안한 틀이 몇 가지 있다. 그대로 가져오지 않고, 이 모듈의 첫 단계에서 정한 목표와 질문에 맞는 것을 골라 정의를 다시 적는다.

틀출처지표나 차원GitHub에서
DORA 지표DORA 연구 프로그램. dora.dev처리량: 변경 리드 타임(커밋에서 운영 배포까지), 배포 빈도, 실패한 배포의 복구 시간. 불안정성: 변경 실패율(즉시 대응이 필요했던 배포의 비율), 배포 재작업률(장애에 대응한 계획에 없던 배포의 비율, 2024년에 추가)배포 기록(Deployments API, 릴리스, Actions 실행), 커밋 시각. Orbit처럼 배포 기록이 없으면 계산할 수 없다. PR 병합을 배포로 바꿔 쓰면 다른 지표가 된다
흐름 프레임워크(Flow Framework)커스텐(M. Kersten), 『Project to Product』(2018)흐름 속도, 흐름 시간, 흐름 부하, 흐름 효율, 흐름 분포. 업무를 기능, 결함, 위험, 부채 네 종류로 나눠 잰다이슈 유형이나 라벨로 업무 종류를 나누고, 처리량(속도), 처리 시간(시간), 진행 중 업무 수(부하)를 계산한다
SPACE포스그렌(N. Forsgren) 외, ACM Queue(2021)만족과 안녕, 성과, 활동, 소통과 협업, 효율과 흐름. 한 차원만 재지 말고 여러 차원과 설문을 함께 보라고 권한다활동(커밋, PR 수)은 쉽게 잴 수 있지만 그것만으로는 생산성이 되지 않는다. 만족은 설문으로 잰다
DevEx노다(A. Noda), 스토리(M.-A. Storey), 포스그렌, 그라일러(M. Greiler), ACM Queue(2023)피드백 루프, 인지 부하, 몰입 상태 세 차원. 개발자의 인식(설문)과 작업 기록을 함께 잰다피드백 루프의 일부(첫 리뷰 대기, CI 시간)는 기록으로 잰다. 인지 부하와 몰입은 설문이 필요하다

지표는 움직이는 시점으로도 나눈다. 선행 지표(leading indicator)는 결과보다 먼저 움직여 손쓸 시간을 주고, 후행 지표(lagging indicator)는 결과를 확정된 값으로 보여 주지만 늦다. 처리 시간은 업무가 끝나야 값이 생기는 후행 지표다. 진행 중 업무 수와 첫 리뷰 대기는 지금 쌓이고 있는 대기를 보여 주는 선행 지표다. 측정 계획에는 후행 지표마다 짝이 되는 선행 지표를 하나 이상 둔다.

6Project 필드를 만들고 GraphQL로 읽기

정의서의 데이터 원천은 실행할 수 있는 명령으로 적는다. 이슈와 PR의 시각은 gh issue list, gh pr list의 --json(실습 15)으로, Project의 필드 값은 gh project item-list나 GraphQL로 읽는다. Status를 바꾼 이력은 공개 API로 조회할 수 없으므로(실습 18) 작업 시작은 스냅숏에서 온다. 크기를 순서 척도로 기록할 Size 필드가 필요하면 만든다.

gh project field-create "$PM_PROJECT" --owner "$PM_OWNER" \
  --name Size --data-type SINGLE_SELECT --single-select-options "S,M,L"

gh project item-list --format json은 필드 값을 필드 이름을 키로 돌려준다. 여러 Project나 조직 Project를 한 번에 읽거나, 필요한 필드만 골라 읽으려면 gh api graphql을 쓴다. 아래 명령은 사용자 Project의 항목마다 이슈 번호, area 라벨, Priority, Status, Estimate를 TSV로 쓴다. 결과는 samples/a-project-items.tsv와 같은 형식이다.

printf 'number\tarea\tpriority\tstatus\testimate\n' > pm-lab/project-items.tsv
gh api graphql --paginate -f query='
query($o: String!, $n: Int!, $endCursor: String) {
  user(login: $o) {
    projectV2(number: $n) {
      items(first: 100, after: $endCursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          content { ... on Issue { number labels(first: 10) { nodes { name } } } }
          fieldValues(first: 20) {
            nodes {
              ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2FieldCommon { name } } }
              ... on ProjectV2ItemFieldNumberValue { number field { ... on ProjectV2FieldCommon { name } } }
              ... on ProjectV2ItemFieldDateValue { date field { ... on ProjectV2FieldCommon { name } } }
            }
          }
        }
      }
    }
  }
}' -f o="$PM_OWNER" -F n="$PM_PROJECT" --jq '
  .data.user.projectV2.items.nodes[] | select(.content.number != null)
  | (reduce (.fieldValues.nodes[] | select(.field.name != null)) as $v ({}; .[$v.field.name] = ($v.name // $v.number // $v.date))) as $f
  | [.content.number,
     ([.content.labels.nodes[].name | select(startswith("area:")) | ltrimstr("area:")] | first // "-"),
     ($f.Priority // "-"), ($f.Status // "-"), ($f.Estimate // "-")] | @tsv' >> pm-lab/project-items.tsv
부분하는 일
user(login:) { projectV2(number:) }사용자 Project를 번호로 찾는다. 조직 Project면 user를 organization으로, --jq의 .data.user를 .data.organization으로 바꾼다
$endCursor, pageInfo--paginate가 100건씩 다음 쪽을 이어 읽는 데 필요하다
... on ProjectV2ItemFieldSingleSelectValue 등필드 값은 유형마다 다른 객체로 온다. 단일 선택은 name, 숫자는 number, 날짜는 date에 값이 있다. 필드 이름은 공통 인터페이스 ProjectV2FieldCommon의 name으로 읽는다
reduce ... as $f필드 이름을 키로 하는 객체를 만들어 $f.Priority처럼 꺼낸다. 값이 없는 필드는 -로 쓴다

이 명령은 GitHub에 접속하므로 교안에는 출력을 싣지 않았다. 실행한 뒤 head -3 pm-lab/project-items.tsv로 열 다섯 개가 탭으로 나뉘어 있는지 확인하고, 3단계의 awk 명령에서 파일 이름만 바꿔 실행한다. 필요한 권한은 project 범위다.

7지표 정의서

정의서는 지표 하나에 한 장이다. 키트의 양식을 복사해 다섯 지표마다 채운다.

for m in lead-time first-review throughput reopen wip; do
  cp github-pm-starter-kit/templates/metric-spec.md "pm-lab/metrics/$m.md"
done
**지표 정의서**
- 이름: (예: 처리 시간)
- 목표와 질문: (목표-질문-지표. 예: 목표 "PM 관점에서 완료 약속의 정확도를 높인다", 질문 "업무 하나를 시작하면 며칠 안에 끝나는가")
- 이 숫자로 정하는 것: (예: 상태 보고의 완료 예상일, 진행 중 업무 한도를 바꿀지)
- 척도와 단위: (명목 / 순서 / 구간 / 비율 중 하나, 단위. 예: 비율 척도, 업무 시간(h))
- 운영적 정의
  - 대상: (예: kind:task 업무 이슈. 계획 취소로 닫힌 이슈는 뺀다)
  - 시작 사건: (예: 작업 시작, 첫 커밋, 막힘 중 가장 이른 사건)
  - 끝 사건: (예: 마지막 업무 완료. 다시 열린 업무는 마지막 완료까지)
  - 시간: (달력 시간 / 업무 시간 평일 09~18시, 시간대 Asia/Seoul, 공휴일 처리)
  - 끝나지 않은 사례: (예: 보고서에 건수를 적고, 분포는 끝난 사례로 계산한다)
- 데이터 원천: (gh 명령, API 경로, 파일. 예: event-log.sh 결과 pm-data/logs/event-log-clean.csv)
- 계산: (명령 한 줄. 예: bash duration.sh 로그 --start "작업 시작,첫 커밋,막힘" --clock business)
- 요약 방법: (예: 중앙값과 85%. 평균은 쓰지 않는다. 이유: 오른쪽 꼬리가 길다)
- 갱신 주기와 담당: (예: 매주 월요일 워크플로, 확인 담당 PM)
- 판단 기준: (어떤 값이나 신호가 나오면 무엇을 하는가)
- 선행·후행: (이 지표가 먼저 움직이는가, 결과를 뒤늦게 보여 주는가. 짝이 되는 지표)
- 남용 위험과 막는 방법: (예: 개인 평가에 쓰지 않는다, 업무를 잘게 쪼개 숫자를 줄이는 일을 막기 위해 처리량과 함께 본다)
- 개정 기록: YYYY-MM-DD, (바꾼 내용과 이유)

처리 시간을 채운 예다. 숫자는 4단계의 실행 결과에서 옮겼다.

**지표 정의서**
- 이름: 처리 시간 (팀 안에서 “리드 타임”이라고 부르지 않는다. 요청자 기준 기간은 “요청 리드 타임”으로 따로 정의한다)
- 목표와 질문: 목표 "PM 관점에서 Orbit 업무 흐름의 예측 가능성을 높인다", 질문 1 "업무 하나를 시작하면 며칠 안에 끝나는가"
- 이 숫자로 정하는 것: 상태 보고의 완료 예상일(85%를 쓴다), 진행 중 업무 한도를 바꿀지
- 척도와 단위: 비율 척도, 업무 시간(h)
- 운영적 정의
  - 대상: area 라벨이 있는 kind:task 업무 이슈. 계획 취소와 중복으로 닫힌 이슈는 뺀다
  - 시작 사건: 작업 시작(18시 스냅숏), 첫 커밋, 막힘 중 가장 이른 사건
  - 끝 사건: 마지막 업무 완료. 다시 열린 업무는 마지막 완료까지 잰다
  - 시간: 업무 시간(평일 09~18시), 시간대 Asia/Seoul. 공휴일은 빼지 않는다
  - 끝나지 않은 사례: 분포 계산에서 빼고 건수를 보고서에 적는다 (2026-09-04 기준 3건)
- 데이터 원천: event-log.sh "$PM_REPO" --snapshots pm-lab/pm-data/snapshots → log-check.sh --clean
- 계산: bash github-pm-starter-kit/duration.sh pm-lab/orbit-event-log-clean.csv --start "작업 시작,첫 커밋,막힘" --clock business
- 요약 방법: 중앙값과 85%. 2026-03 ~ 09 기준 중앙값 33h, 85% 53.9h, 평균 35h
- 갱신 주기와 담당: 매주 월요일 process-report 워크플로(실습 29), 확인 담당 pm-oh
- 판단 기준: 4주 연속 85%가 53.9h보다 길면 bottleneck.sh로 늘어난 단계를 찾고 주간 보고 맨 위에 적는다
- 선행·후행: 후행 지표. 짝이 되는 선행 지표는 진행 중 업무 수와 첫 리뷰 대기
- 남용 위험과 막는 방법: 개인별로 계산하지 않는다. 업무를 잘게 쪼개 처리 시간을 줄이는 일을 알아보려고 처리량과 다시 열린 비율을 함께 본다
- 개정 기록: 2026-09-07, 처음 작성
지표가 목표가 되면

영국의 경제학자 굿하트(C. Goodhart)는 1975년 학술회의에서 발표한 글에서 통제에 쓰려고 압력을 가하면 관찰되던 통계적 규칙성이 더는 유지되지 않는 경향이 있다고 썼다. 이 관찰이 굿하트 법칙(Goodhart's law)으로 불린다. “측정값이 목표가 되면 좋은 측정값이 아니게 된다”는 흔한 표현은 인류학자 스트래선(M. Strathern)이 1997년에 다시 정리한 문장이다. 사회심리학자 캠벨(D. T. Campbell)도 1976년 글(1979년 학술지 게재)에서 양적 지표를 의사결정에 많이 쓸수록 그 지표가 부패 압력을 받고, 재려던 과정을 왜곡한다고 썼다. 처리 시간을 개인 평가에 쓰면 업무를 쪼개거나 어려운 업무를 피하는 일이 생기고, 첫 리뷰 대기를 목표로 걸면 내용을 보지 않은 승인이 늘 수 있다. 정의서의 “남용 위험” 칸에는 이런 행동을 적고, 그 행동을 드러내는 짝 지표(처리량, 변경 요청 비율, 다시 열린 비율)를 함께 적는다. 2단계에서 정한 원칙대로 사람별 숫자는 일이 한쪽에 몰리는지 볼 때만 쓴다.

8측정 계획

정의서 다섯 장을 한 표로 묶은 것이 측정 계획이다. 계획에는 지표마다 누가, 언제, 어떤 명령으로 계산하고, 결과를 어디에 싣는지 적는다. 아래는 Orbit의 예다. 학습자 저장소의 계획은 같은 열로 pm-lab/measurement-plan.md에 적는다.

지표질문척도계산주기선행·후행싣는 곳
처리 시간 중앙값, 85%1비율duration.sh --start "작업 시작,첫 커밋,막힘" --clock business매주후행주간 보고 흐름 절, 상태 보고 일정 절
첫 리뷰 대기 중앙값, 85%2비율duration.sh --start "PR 생성" --end "리뷰 승인,변경 요청" --first-end --clock business (PR이 하나인 업무), bottleneck.sh매주선행주간 보고 흐름 절
주별 처리량3비율flow-metrics.sh의 주별 처리량매주후행마일스톤 예측 입력
다시 열린 사례 비율4비율(명목 자료의 비율)compare.sh의 다시 열린 사례 비율4주마다후행회고 자료
진행 중 업무 수5비율flow-metrics.sh --project의 WIP평일선행주간 사전 리포트

첫 리뷰 대기를 duration.sh로 계산하면 업무 하나에 PR이 둘인 경우(다시 열린 업무) 첫 PR만 잰다. PR마다 재는 bottleneck.sh와 값이 조금 다를 수 있으므로, 정의서에 어느 쪽을 쓰는지 적는다.

bash github-pm-starter-kit/duration.sh pm-lab/orbit-event-log-clean.csv --start "PR 생성" --end "리뷰 승인,변경 요청" --first-end --clock business | tail -2
- 사례 257건: 계산 234건, 시작 사건 없음 23건, 끝 사건 없음 0건
- 평균 4.9h, 중앙값 2.9h, 85% 11.1h, 95% 15.1h, 최소 0.5h, 최대 18.5h

사례 257건 중 PR이 있는 234건만 계산되고, PR이 없는 QA 업무 23건은 “시작 사건 없음”으로 세어진다. 다시 열린 업무 13건의 두 번째 PR도 빠지므로, PR 247건(실습 25의 123건과 124건)을 모두 재는 bottleneck.sh와 건수가 다르다. 정의서의 대상 칸에 “PR이 있는 업무의 첫 PR”을 적어야 하는 이유다.

막히면
증상확인과 해결
오류: 시작 사건이 로그에 없다: ...활동 이름이 로그와 다르다. cut -d, -f2 pm-lab/orbit-event-log-clean.csv | sort -u로 이름을 확인하고, 공백이 있는 이름은 큰따옴표로 감싼다
출력에 “로그에 없는 활동: 업무 닫힘”이 있다기본 끝 사건인 업무 닫힘이 이 로그에 없다는 안내다. 업무 완료로 계산했으므로 결과는 맞다
awk 평균이 모두 0이다파일의 열 구분이 탭이 아니다. head -2 파일 | od -c | head에서 \t가 보이는지 확인한다
GraphQL 결과가 비어 있거나 Could not resolve to a ProjectV2Project 번호나 소유자가 다르다. 조직 Project면 user를 organization으로 바꾼다
GraphQL 오류에 scopes가 나온다토큰에 Project 권한이 없다. gh auth refresh -s project를 실행한다(실습 4)
  • Orbit의 목표 하나에서 질문 다섯 개와 지표 다섯 개를 끌어냈다.
  • 처리 시간을 ISO/IEC/IEEE 15939의 정보 모델 층으로 나눠 적었다.
  • Priority의 평균이 숫자 붙이기에 따라 결론을 바꾸는 것을 확인하고, 대신 쓸 요약을 골랐다.
  • “리드 타임”의 네 정의로 중앙값이 33h에서 9.3일까지 달라지는 것을 확인하고, 질문에 맞는 정의를 골랐다.
  • DORA, 흐름 프레임워크, SPACE, DevEx 중 팀의 질문에 맞는 지표를 표시했다.
  • Project 필드를 GraphQL로 읽는 명령의 각 부분을 설명할 수 있다.
  • 지표 정의서 다섯 장과 측정 계획을 pm-lab/에 썼다.

마치며...

지표는 목표와 질문에서 나오고, 척도가 허용하는 계산과 다른 사람이 따라 할 수 있는 운영적 정의를 갖췄을 때 회의에서 같은 숫자로 쓰인다. Orbit 로그에서 “리드 타임”은 정의에 따라 33h부터 9.3일까지 달라졌고, 순서 척도인 Priority의 평균은 숫자를 붙이는 방법에 따라 반대 결론을 냈다. 정의서에는 계산 명령과 함께 남용 위험과 짝 지표를 적어 두었다. 다음 모듈에서는 이렇게 정의한 지표의 주별 값이 오르내릴 때, 그 움직임이 신호인지 늘 있는 흔들림인지 관리도로 판정한다.