"번다운 차트가 거짓말한다"고 했던 지난 편 기억하시나요?
이번엔 정직한 번다운을 만드는 방법을 알아봅시다.
스코프 크립: 목표가 계속 움직인다
프로젝트 중에 이런 일 겪어보셨죠?
Week 1: "100 포인트면 끝나요!"
Week 2: "아, 소셜 로그인도 추가해주세요" (+15)
Week 3: "모바일도 대응해야죠" (+20)
Week 4: "관리자 페이지는 당연히..." (+30)
열심히 일해서 50포인트를 완료했는데, 전체 스코프가 165로 늘어났습니다.
진행률은 오히려 떨어졌네요. (50% → 30%)
이게 바로 스코프 크립입니다.
번업 차트: 진실을 보여주는 도구
번다운 차트는 남은 작업만 보여줍니다.
번업 차트는 완료된 작업과 전체 스코프를 함께 보여줍니다.
class BurnupChart {
constructor() {
this.completed = [];
this.totalScope = [];
}
addWeek(completedWork, currentScope) {
this.completed.push(completedWork);
this.totalScope.push(currentScope);
}
getScopeCreep() {
const initial = this.totalScope[0];
const current = this.totalScope[this.totalScope.length - 1];
return (((current - initial) / initial) * 100).toFixed(1);
}
}
// 사용 예시
const chart = new BurnupChart();
chart.addWeek(0, 100); // 시작
chart.addWeek(20, 100); // Week 1
chart.addWeek(35, 115); // Week 2 (스코프 증가!)
chart.addWeek(50, 135); // Week 3 (또 증가!)
console.log(`스코프 크립: ${chart.getScopeCreep()}%`); // 35%
번업 차트를 보면 "우리가 느린 게 아니라 목표가 계속 멀어지고 있구나"를 알 수 있습니다.
누적 흐름도: 병목을 찾아라
칸반 보드의 각 컬럼에 있는 작업 수를 시간에 따라 그린 차트입니다.
def analyze_cumulative_flow(daily_data):
"""누적 흐름도 분석"""
bottlenecks = []
for day in daily_data:
# 각 단계별 작업 수
todo = day["todo"]
doing = day["doing"]
review = day["review"]
done = day["done"]
# 병목 감지
if review > doing * 2:
bottlenecks.append({
"day": day["date"],
"issue": "리뷰 병목",
"action": "리뷰어 추가 필요"
})
if doing > 10:
bottlenecks.append({
"day": day["date"],
"issue": "WIP 과다",
"action": "진행 중 작업 제한"
})
return bottlenecks
누적 흐름도에서 특정 영역이 부풀어 오르면? 그곳이 병목입니다.
사이클 타임 추적
작업이 시작부터 완료까지 걸리는 실제 시간을 측정합니다.
class CycleTimeTracker {
trackTask(task) {
const startDate = new Date(task.startedAt);
const endDate = new Date(task.completedAt);
const cycleTime = (endDate - startDate) / (1000 * 60 * 60 * 24); // 일 단위
return {
name: task.name,
cycleTime: cycleTime,
category: this.categorize(cycleTime),
};
}
categorize(days) {
if (days <= 1) return '🟢 빠름';
if (days <= 3) return '🟡 보통';
if (days <= 7) return '🟠 느림';
return '🔴 매우 느림';
}
getAverageCycleTime(tasks) {
const times = tasks.map((t) => this.trackTask(t).cycleTime);
return times.reduce((a, b) => a + b, 0) / times.length;
}
}
평균 사이클 타임이 늘어나면 프로세스에 문제가 있다는 신호입니다.
몬테카를로 시뮬레이션으로 예측
과거 데이터로 미래를 예측합니다.
import random
def monte_carlo_forecast(historical_velocities, remaining_work, simulations=1000):
"""완료 날짜 예측"""
results = []
for _ in range(simulations):
days = 0
work_left = remaining_work
while work_left > 0:
# 과거 속도 중 랜덤 선택
daily_velocity = random.choice(historical_velocities)
work_left -= daily_velocity
days += 1
results.append(days)
results.sort()
return {
"p50": results[int(len(results) * 0.5)], # 50% 확률
"p70": results[int(len(results) * 0.7)], # 70% 확률
"p90": results[int(len(results) * 0.9)], # 90% 확률
}
# 사용 예시
velocities = [3, 5, 2, 8, 4, 6, 3, 7] # 과거 일일 완료량
remaining = 100 # 남은 작업
forecast = monte_carlo_forecast(velocities, remaining)
print(f"50% 확률로 {forecast['p50']}일 내 완료")
print(f"90% 확률로 {forecast['p90']}일 내 완료")
"2주면 끝날 것 같아요" 대신 "70% 확률로 12일, 90% 확률로 16일"이라고 말할 수 있습니다.
리드 타임 분포도
작업 크기별로 걸리는 시간을 분석합니다.
const leadTimeDistribution = {
'XS (1-2 points)': {
average: '0.5일',
p50: '0.5일',
p90: '1일',
recommendation: '즉시 처리',
},
'S (3-5 points)': {
average: '2일',
p50: '1.5일',
p90: '3일',
recommendation: '일반 처리',
},
'M (8-13 points)': {
average: '5일',
p50: '4일',
p90: '8일',
recommendation: '분해 검토',
},
'L (20+ points)': {
average: '15일',
p50: '12일',
p90: '25일',
recommendation: '반드시 분해',
},
};
function estimateBySize(points) {
if (points <= 2) return leadTimeDistribution['XS (1-2 points)'];
if (points <= 5) return leadTimeDistribution['S (3-5 points)'];
if (points <= 13) return leadTimeDistribution['M (8-13 points)'];
return leadTimeDistribution['L (20+ points)'];
}
실시간 대시보드 만들기
class HonestDashboard:
"""정직한 프로젝트 대시보드"""
def __init__(self):
self.metrics = {}
def update_daily(self, data):
self.metrics = {
"진행률": self.calculate_progress(data),
"예상_완료일": self.forecast_completion(data),
"스코프_변경": self.scope_change(data),
"병목_지점": self.find_bottleneck(data),
"리스크": self.assess_risks(data)
}
def calculate_progress(self, data):
# 포인트 기반 진행률 (작업 개수가 아닌)
completed_points = data["completed_points"]
total_points = data["total_points"]
return f"{(completed_points/total_points*100):.1f}%"
def forecast_completion(self, data):
# 실제 속도 기반 예측
avg_velocity = data["avg_velocity_last_2weeks"]
remaining = data["remaining_points"]
days = remaining / avg_velocity if avg_velocity > 0 else "∞"
return f"{days:.0f}일 후"
def scope_change(self, data):
# 스코프 변경률
initial = data["initial_scope"]
current = data["current_scope"]
change = ((current - initial) / initial * 100)
return f"{change:+.1f}%"
def find_bottleneck(self, data):
# 가장 많이 쌓인 단계
stages = data["work_in_progress_by_stage"]
bottleneck = max(stages, key=stages.get)
return f"{bottleneck} ({stages[bottleneck]}개)"
def assess_risks(self, data):
risks = []
if data["velocity_trend"] < 0:
risks.append("속도 감소 중")
if data["scope_creep_rate"] > 10:
risks.append("과도한 스코프 변경")
if data["blocked_tasks"] > 3:
risks.append(f"블로커 {data['blocked_tasks']}개")
return risks or ["정상"]
정직한 커뮤니케이션
def honest_status_report():
"""정직한 상태 보고"""
return f"""
## 프로젝트 현황 (Week 8)
### 숫자로 보는 진실
- 완료: 120 포인트 / 180 포인트 (67%)
- 초기 스코프 대비: +35% 증가
- 현재 속도: 주당 15 포인트
- 예상 완료: 4주 후 (70% 신뢰도)
### 좋은 소식
- 핵심 기능 80% 완료
- 품질 지표 향상 (버그 -40%)
### 나쁜 소식
- 스코프 계속 증가 중
- 백엔드 병목 심화
### 필요한 결정
1. 추가 요구사항 동결 여부
2. 백엔드 개발자 지원 여부
3. 출시 일정 조정 여부
"""
숫자를 조작하지 마세요. 진실을 말하세요.
그래야 올바른 결정을 내릴 수 있습니다.
체크리스트: 정직한 번다운 만들기
프로젝트 시작 시:
- 번업 차트도 함께 준비
- 스토리 포인트 기준 통일
- 완료 기준 명확히 정의
- 사이클 타임 측정 도구 준비
매일:
- 완료된 포인트 기록
- 새로 추가된 스코프 기록
- 블로커 기록
- 각 단계별 작업 수 기록
매주:
- 평균 속도 계산
- 완료 예상일 재계산
- 스코프 변경률 분석
- 병목 지점 파악
마무리
정직한 번다운은 단순히 차트를 그리는 것이 아닙니다.
현실을 있는 그대로 보여주는 용기입니다.
"90% 완료"라는 거짓말 대신,
"67% 완료, 스코프 35% 증가, 4주 더 필요"라고 말하세요.
그래야 팀이 올바른 결정을 내릴 수 있습니다.
투명하고 정확한 프로젝트 관리가 필요하신가요? Plexo를 확인해보세요.
댓글 없음:
댓글 쓰기