본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 5. · 19 Views
Scrum 베스트 프랙티스 완벽 가이드
애자일 개발 방법론의 핵심인 Scrum의 베스트 프랙티스를 실무 중심으로 소개합니다. 스프린트 계획부터 회고까지, 초급 개발자도 바로 적용할 수 있는 구체적인 가이드를 제공합니다.
목차
1. 스프린트 계획
시작하며
여러분이 프로젝트를 시작할 때 "이번 주에 뭘 해야 하지?"라고 막막해한 적 있나요? 팀원마다 우선순위가 다르고, 작업량을 예측하기 어려워 일정이 늘 밀리는 상황을 겪어보셨을 겁니다.
이런 문제는 실제 개발 현장에서 매우 흔합니다. 명확한 계획 없이 시작하면 중복 작업이 발생하고, 중요한 기능이 누락되며, 팀원들은 각자 다른 방향으로 일하게 됩니다.
결국 프로젝트는 예상보다 2배, 3배 더 오래 걸리게 됩니다. 바로 이럴 때 필요한 것이 스프린트 계획입니다.
체계적인 스프린트 계획을 통해 팀 전체가 같은 목표를 향해 나아가고, 현실적인 일정으로 작업할 수 있습니다.
개요
간단히 말해서, 스프린트 계획은 1-4주 동안 팀이 달성할 목표와 작업을 정하는 회의입니다. 실무에서는 무작정 코딩부터 시작하는 것이 아니라, 먼저 "무엇을 만들 것인가"와 "어떻게 만들 것인가"를 명확히 합니다.
예를 들어, 로그인 기능을 개발한다면, UI 구현, API 연동, 에러 처리, 테스트 작성 등을 구체적인 태스크로 나누어 계획합니다. 기존에는 프로젝트 전체를 한 번에 계획했다면, 이제는 짧은 주기로 나누어 계획하고 실행합니다.
이렇게 하면 변경사항에 빠르게 대응하고, 위험을 조기에 발견할 수 있습니다. 스프린트 계획의 핵심 특징은 세 가지입니다.
첫째, 팀 전체가 참여하여 합의점을 찾습니다. 둘째, 작업량을 스토리 포인트로 추정하여 과부하를 방지합니다.
셋째, 명확한 스프린트 목표를 설정하여 방향성을 유지합니다. 이러한 특징들이 팀의 생산성과 만족도를 크게 높여줍니다.
코드 예제
// Sprint Backlog 관리 시스템
class SprintPlanner {
constructor(teamVelocity) {
// 팀의 평균 작업 속도 (스토리 포인트/스프린트)
this.teamVelocity = teamVelocity;
this.sprintBacklog = [];
this.totalPoints = 0;
}
// 백로그 아이템 추가 (과부하 방지)
addTask(task, storyPoints) {
if (this.totalPoints + storyPoints <= this.teamVelocity) {
this.sprintBacklog.push({ task, storyPoints, status: 'TODO' });
this.totalPoints += storyPoints;
console.log(`✓ "${task}" 추가됨 (${storyPoints}점)`);
return true;
} else {
console.log(`✗ 용량 초과! 현재: ${this.totalPoints}, 추가하려는: ${storyPoints}, 제한: ${this.teamVelocity}`);
return false;
}
}
// 스프린트 요약 출력
getSummary() {
return {
totalTasks: this.sprintBacklog.length,
commitedPoints: this.totalPoints,
capacity: this.teamVelocity,
utilizationRate: ((this.totalPoints / this.teamVelocity) * 100).toFixed(1) + '%'
};
}
}
// 실제 사용 예시
const planner = new SprintPlanner(30); // 팀 속도: 30포인트/스프린트
planner.addTask('사용자 로그인 API 개발', 8);
planner.addTask('프로필 페이지 UI 구현', 5);
planner.addTask('비밀번호 재설정 기능', 13);
planner.addTask('관리자 대시보드', 8); // 용량 초과로 거부됨
console.log('스프린트 계획:', planner.getSummary());
설명
이것이 하는 일: 이 코드는 팀의 작업 용량을 관리하고, 스프린트 계획 시 과도한 작업을 방지하는 시스템입니다. 첫 번째로, SprintPlanner 클래스는 팀의 평균 작업 속도(velocity)를 기반으로 초기화됩니다.
예를 들어 팀이 지난 3개 스프린트에서 평균 30 스토리 포인트를 완료했다면, 이번 스프린트도 30포인트를 목표로 설정합니다. 이렇게 하는 이유는 과거 데이터를 기반으로 현실적인 계획을 세우기 위함입니다.
그 다음으로, addTask 메서드가 실행되면서 각 작업의 스토리 포인트를 확인합니다. 만약 현재 누적 포인트에 새 작업을 추가했을 때 팀 속도를 초과하면, 해당 작업은 거부되고 경고 메시지가 출력됩니다.
이는 스프린트 실패를 미리 방지하는 핵심 메커니즘입니다. 마지막으로, getSummary 메서드가 스프린트의 전체 상태를 요약합니다.
총 작업 개수, 커밋한 포인트, 용량 활용률 등을 한눈에 볼 수 있어, 계획 회의에서 팀이 적절한 양의 작업을 선택했는지 즉시 판단할 수 있습니다. 여러분이 이 코드를 사용하면 스프린트 계획 회의에서 "이 작업까지 할 수 있을까?"라는 질문에 데이터 기반으로 답할 수 있습니다.
또한 과부하로 인한 번아웃을 방지하고, 팀의 지속 가능한 페이스를 유지할 수 있으며, 이해관계자에게 현실적인 약속을 할 수 있습니다.
실전 팁
💡 이전 스프린트 데이터 활용: 최소 3개 스프린트의 실제 완료 포인트를 평균내어 팀 속도를 계산하세요. 처음 시작하는 팀이라면 보수적으로 낮게 잡고 점차 조정합니다.
💡 버퍼 남기기: 팀 속도의 80-90%만 계획하세요. 예상치 못한 버그 수정이나 긴급 요청에 대응할 여유가 필요합니다. 100% 채우면 거의 항상 실패합니다.
💡 큰 작업 분할: 8포인트 이상의 작업은 더 작은 태스크로 나누세요. 작은 작업일수록 추정이 정확하고, 진행 상황 파악이 쉬우며, 팀원 간 협업도 원활합니다.
💡 우선순위 명확히: 비즈니스 가치가 높은 작업부터 백로그에 추가하세요. 용량이 부족하면 우선순위 낮은 작업은 다음 스프린트로 미루는 것이 현명합니다.
💡 팀 전체 참여: 개발자뿐만 아니라 디자이너, QA도 계획 회의에 참여해야 합니다. 각자의 관점에서 작업량을 검토하면 놓치는 부분이 줄어듭니다.
2. 데일리 스크럼
시작하며
여러분이 아침에 출근해서 "오늘 뭐 하지?"라고 고민하거나, 동료가 무슨 일을 하는지 몰라서 중복 작업을 한 경험이 있나요? 또는 문제가 발생했는데 며칠이 지나서야 팀이 알게 되어 프로젝트가 지연된 적이 있을 겁니다.
이런 문제는 커뮤니케이션 부족에서 발생합니다. 팀원들이 각자 고립되어 일하면 시너지가 나지 않고, 작은 문제가 큰 위기로 커질 수 있습니다.
특히 원격 근무 환경에서는 이런 문제가 더욱 심각합니다. 바로 이럴 때 필요한 것이 데일리 스크럼입니다.
매일 15분의 짧은 회의로 팀의 투명성을 높이고, 장애물을 빠르게 제거하여 생산성을 극대화할 수 있습니다.
개요
간단히 말해서, 데일리 스크럼은 매일 같은 시간에 15분 동안 진행하는 팀 동기화 회의입니다. 실무에서는 각 팀원이 세 가지 질문에 답합니다: "어제 무엇을 했나요?", "오늘 무엇을 할 건가요?", "어떤 장애물이 있나요?" 예를 들어, 백엔드 개발자가 API 응답 속도 문제를 언급하면, 프론트엔드 개발자는 그것을 고려하여 로딩 UI를 먼저 구현하기로 결정할 수 있습니다.
기존에는 주 1회 긴 미팅으로 상황을 공유했다면, 이제는 매일 짧게 동기화합니다. 이렇게 하면 문제가 발생한 지 24시간 이내에 팀이 인지하고 대응할 수 있습니다.
데일리 스크럼의 핵심 특징은 세 가지입니다. 첫째, 정해진 시간에 서서 진행하여 간결함을 유지합니다.
둘째, 상태 보고가 아닌 팀 동기화에 초점을 맞춥니다. 셋째, 문제 해결은 회의 후 별도로 진행합니다.
이러한 특징들이 회의 피로도를 낮추면서도 효과를 극대화합니다.
코드 예제
// Daily Scrum 자동화 시스템
class DailyScrum {
constructor(teamMembers) {
this.teamMembers = teamMembers;
this.updates = [];
this.blockers = [];
}
// 팀원 업데이트 추가
addUpdate(member, yesterday, today, blocker = null) {
const update = {
member,
yesterday,
today,
blocker,
timestamp: new Date().toLocaleString()
};
this.updates.push(update);
// 장애물이 있으면 별도 추적
if (blocker) {
this.blockers.push({
member,
issue: blocker,
reportedAt: new Date()
});
console.log(`⚠️ 장애물 발견: ${member} - ${blocker}`);
}
return update;
}
// 데일리 스크럼 요약
generateSummary() {
console.log('\n=== 📅 데일리 스크럼 요약 ===\n');
this.updates.forEach(update => {
console.log(`👤 ${update.member}`);
console.log(` ✓ 어제: ${update.yesterday}`);
console.log(` → 오늘: ${update.today}`);
if (update.blocker) {
console.log(` 🚨 장애물: ${update.blocker}`);
}
console.log('');
});
// 장애물 하이라이트
if (this.blockers.length > 0) {
console.log('🚨 해결 필요한 장애물:');
this.blockers.forEach((b, i) => {
console.log(`${i + 1}. [${b.member}] ${b.issue}`);
});
} else {
console.log('✅ 보고된 장애물 없음');
}
}
// 장애물 해결 표시
resolveBlocker(index) {
if (index < this.blockers.length) {
const resolved = this.blockers.splice(index, 1)[0];
console.log(`✓ 해결됨: ${resolved.issue}`);
}
}
}
// 실제 사용 예시
const scrum = new DailyScrum(['김철수', '이영희', '박민수']);
scrum.addUpdate('김철수', '로그인 API 구현 완료', '프로필 API 개발 시작', null);
scrum.addUpdate('이영희', '메인 페이지 UI 80% 완성', '반응형 레이아웃 적용', '디자인 가이드 문서 부족');
scrum.addUpdate('박민수', '테스트 코드 작성', '배포 파이프라인 구축', 'AWS 권한 문제');
scrum.generateSummary();
설명
이것이 하는 일: 이 코드는 데일리 스크럼 회의 내용을 체계적으로 기록하고, 장애물을 자동으로 추적하는 시스템입니다. 첫 번째로, DailyScrum 클래스는 팀원 목록으로 초기화되며, 각 팀원의 업데이트와 장애물을 저장할 배열을 준비합니다.
이는 매일의 회의 내용을 구조화하여 나중에 분석하거나 리포트를 생성할 수 있게 합니다. 그 다음으로, addUpdate 메서드가 실행되면서 각 팀원의 "어제 한 일", "오늘 할 일", "장애물"을 기록합니다.
특히 중요한 것은 장애물이 있을 때 즉시 별도 배열에 추가하고 경고 메시지를 출력한다는 점입니다. 이렇게 하면 스크럼 마스터나 팀 리더가 즉각적으로 인지하고 대응할 수 있습니다.
또한 타임스탬프를 기록하여 문제가 얼마나 오래 지속되는지 추적할 수 있습니다. 마지막으로, generateSummary 메서드가 전체 팀의 상황을 보기 좋게 정리하여 출력합니다.
각 팀원의 업데이트를 구조화된 형식으로 보여주고, 해결이 필요한 장애물을 별도로 하이라이트합니다. 이는 회의록 작성을 자동화하고, 중요한 이슈를 놓치지 않게 합니다.
여러분이 이 코드를 사용하면 데일리 스크럼을 더 효율적으로 진행할 수 있습니다. 회의 내용이 자동으로 기록되어 나중에 참고할 수 있고, 장애물이 방치되지 않도록 추적되며, 팀의 작업 패턴을 분석하여 프로세스를 개선할 데이터를 얻을 수 있습니다.
실전 팁
💡 같은 시간, 같은 장소: 매일 오전 10시 같은 고정 시간에 진행하세요. 루틴이 형성되면 팀원들이 자연스럽게 준비하고, 회의 시작이 빨라집니다.
💡 15분 엄수: 타이머를 사용하고, 깊은 논의는 회의 후 관련자끼리 별도로 진행하세요. 전체 팀 시간을 낭비하지 않는 것이 중요합니다. 한 사람당 2-3분 정도가 적당합니다.
💡 서서 진행: 앉아서 하면 회의가 길어집니다. 서서 하면 자연스럽게 간결해지고, 집중도도 높아집니다. 원격의 경우 카메라를 켜고 진행하세요.
💡 비동기 옵션 제공: 시차가 있는 글로벌 팀이라면 Slack이나 이메일로 세 가지 질문에 답하는 비동기 스크럼도 고려하세요. 중요한 것은 매일의 동기화입니다.
💡 장애물 즉시 처리: 회의에서 언급된 장애물은 그날 안에 해결 계획을 세우세요. 방치하면 다음 날 같은 이야기가 반복되고 팀의 사기가 떨어집니다.
3. 작업 보드 관리
시작하며
여러분이 프로젝트를 진행할 때 "지금 어디까지 왔지?", "누가 뭘 하고 있지?"라고 혼란스러웠던 적 있나요? 이메일과 메신저에 작업 내용이 흩어져 있어서 전체 상황을 파악하기 어려운 상황을 겪어보셨을 겁니다.
이런 문제는 작업 가시성 부족에서 발생합니다. 누가 무엇을 하는지 모르면 병목 지점을 발견할 수 없고, 작업 분배가 불균형해지며, 마감일이 다가와서야 문제를 인식하게 됩니다.
특히 여러 명이 협업하는 프로젝트에서는 이런 문제가 치명적입니다. 바로 이럴 때 필요한 것이 작업 보드 관리입니다.
칸반 보드나 스크럼 보드를 활용하면 팀 전체의 작업 흐름을 한눈에 파악하고, 병목을 즉시 발견하여 해결할 수 있습니다.
개요
간단히 말해서, 작업 보드는 모든 작업을 "할 일", "진행 중", "완료" 같은 상태별 칼럼으로 시각화하는 도구입니다. 실무에서는 Jira, Trello, GitHub Projects 같은 도구를 사용하여 각 작업 카드를 생성하고, 진행 상태에 따라 칼럼 간 이동시킵니다.
예를 들어, "로그인 기능 개발" 카드가 "진행 중" 칼럼에 3일 이상 머물러 있다면, 이는 개발자가 어려움을 겪고 있다는 신호입니다. 팀 리더는 즉시 도움을 제공할 수 있습니다.
기존에는 스프레드시트나 문서로 작업을 추적했다면, 이제는 드래그 앤 드롭으로 직관적으로 관리합니다. 이렇게 하면 회의 시간을 줄이고, 모든 팀원이 실시간으로 상황을 파악할 수 있습니다.
작업 보드의 핵심 특징은 세 가지입니다. 첫째, WIP(Work In Progress) 제한으로 동시 작업을 줄여 집중도를 높입니다.
둘째, 칼럼별 카드 수를 보면 병목 지점이 즉시 드러납니다. 셋째, 누가 무엇을 하는지 프로필 사진으로 한눈에 알 수 있습니다.
이러한 특징들이 팀의 투명성과 효율성을 크게 향상시킵니다.
코드 예제
// Kanban 보드 관리 시스템
class KanbanBoard {
constructor() {
this.columns = {
'TODO': { tasks: [], wipLimit: null },
'IN_PROGRESS': { tasks: [], wipLimit: 3 },
'REVIEW': { tasks: [], wipLimit: 2 },
'DONE': { tasks: [], wipLimit: null }
};
}
// 작업 추가
addTask(taskId, title, assignee) {
const task = { taskId, title, assignee, createdAt: new Date() };
this.columns['TODO'].tasks.push(task);
console.log(`➕ 작업 추가됨: ${title}`);
return task;
}
// 작업 이동 (WIP 제한 체크)
moveTask(taskId, toColumn) {
let task = null;
let fromColumn = null;
// 현재 작업 위치 찾기
for (const [colName, col] of Object.entries(this.columns)) {
const index = col.tasks.findIndex(t => t.taskId === taskId);
if (index !== -1) {
task = col.tasks[index];
fromColumn = colName;
col.tasks.splice(index, 1);
break;
}
}
if (!task) {
console.log(`❌ 작업 ID ${taskId}를 찾을 수 없습니다`);
return false;
}
// WIP 제한 확인
const targetCol = this.columns[toColumn];
if (targetCol.wipLimit && targetCol.tasks.length >= targetCol.wipLimit) {
console.log(`⚠️ WIP 제한 초과! ${toColumn}은 최대 ${targetCol.wipLimit}개까지만 가능합니다`);
// 원래 위치로 되돌리기
this.columns[fromColumn].tasks.push(task);
return false;
}
// 작업 이동
targetCol.tasks.push(task);
console.log(`✓ "${task.title}" 이동됨: ${fromColumn} → ${toColumn}`);
return true;
}
// 보드 상태 시각화
visualize() {
console.log('\n📊 === 작업 보드 현황 ===\n');
for (const [colName, col] of Object.entries(this.columns)) {
const wipInfo = col.wipLimit ? ` (${col.tasks.length}/${col.wipLimit})` : ` (${col.tasks.length})`;
console.log(`${colName}${wipInfo}:`);
if (col.tasks.length === 0) {
console.log(' (비어있음)');
} else {
col.tasks.forEach(task => {
console.log(` - [${task.taskId}] ${task.title} (@${task.assignee})`);
});
}
console.log('');
}
}
// 병목 지점 감지
detectBottlenecks() {
const bottlenecks = [];
for (const [colName, col] of Object.entries(this.columns)) {
if (col.wipLimit && col.tasks.length >= col.wipLimit) {
bottlenecks.push(`${colName} 칼럼이 포화 상태입니다`);
}
}
return bottlenecks;
}
}
// 실제 사용 예시
const board = new KanbanBoard();
// 작업 추가
board.addTask('TASK-101', '로그인 API 개발', '김철수');
board.addTask('TASK-102', '프로필 UI 구현', '이영희');
board.addTask('TASK-103', '테스트 코드 작성', '박민수');
// 작업 이동
board.moveTask('TASK-101', 'IN_PROGRESS');
board.moveTask('TASK-102', 'IN_PROGRESS');
board.moveTask('TASK-103', 'IN_PROGRESS');
board.moveTask('TASK-101', 'REVIEW');
// 추가 작업 이동 시도 (WIP 제한 테스트)
board.addTask('TASK-104', '배포 스크립트', '김철수');
board.moveTask('TASK-104', 'IN_PROGRESS'); // 성공
board.addTask('TASK-105', '문서 작성', '이영희');
board.moveTask('TASK-105', 'IN_PROGRESS'); // WIP 제한으로 실패
board.visualize();
const bottlenecks = board.detectBottlenecks();
if (bottlenecks.length > 0) {
console.log('🚨 병목 지점:');
bottlenecks.forEach(b => console.log(` - ${b}`));
}
설명
이것이 하는 일: 이 코드는 칸반 방식의 작업 보드를 구현하고, WIP 제한을 통해 팀의 과부하를 방지하는 시스템입니다. 첫 번째로, KanbanBoard 클래스는 TODO, IN_PROGRESS, REVIEW, DONE의 네 가지 칼럼으로 구성됩니다.
각 칼럼에는 wipLimit(Work In Progress 제한)이 설정되어 있습니다. 예를 들어 IN_PROGRESS는 최대 3개까지만 작업이 들어갈 수 있습니다.
이는 팀원들이 너무 많은 작업을 동시에 시작하여 아무것도 완료하지 못하는 상황을 방지합니다. 그 다음으로, moveTask 메서드가 실행될 때 목표 칼럼의 WIP 제한을 확인합니다.
만약 이미 제한에 도달했다면 작업 이동을 거부하고 경고 메시지를 출력합니다. 이 메커니즘은 팀이 이미 진행 중인 작업을 먼저 완료하도록 강제합니다.
실무에서는 이것이 매우 중요한데, 새 작업을 계속 시작하는 것보다 진행 중인 작업을 완료하는 것이 전체 프로젝트 속도를 높이기 때문입니다. 마지막으로, visualize와 detectBottlenecks 메서드가 보드의 현재 상태를 분석합니다.
visualize는 각 칼럼의 작업 목록과 담당자를 보여주고, detectBottlenecks는 WIP 제한에 도달한 칼럼을 자동으로 감지합니다. 예를 들어 REVIEW 칼럼이 포화 상태라면, 이는 리뷰어가 부족하거나 리뷰 프로세스에 문제가 있다는 신호입니다.
여러분이 이 코드를 사용하면 팀의 작업 흐름을 실시간으로 모니터링할 수 있습니다. 병목 지점을 즉시 발견하여 대응할 수 있고, WIP 제한으로 멀티태스킹을 줄여 생산성을 높이며, 각 작업의 담당자와 상태를 투명하게 공유할 수 있습니다.
실전 팁
💡 WIP 제한 설정: IN_PROGRESS 칼럼은 팀원 수와 같거나 조금 적게 설정하세요. 3명 팀이라면 2-3개가 적당합니다. 너무 많으면 멀티태스킹으로 생산성이 떨어집니다.
💡 매일 보드 확인: 데일리 스크럼 시 보드를 화면에 띄워놓고 진행하세요. 누가 어떤 작업을 하는지 보드를 보며 이야기하면 회의가 더 구체적이고 짧아집니다.
💡 오래된 작업 표시: 한 칼럼에 3일 이상 머무는 작업은 빨간색으로 표시하세요. 이는 문제가 있다는 신호입니다. 팀원이 막혀있거나 작업이 예상보다 복잡할 수 있습니다.
💡 완료 조건 명시: 각 칼럼 간 이동 기준을 명확히 하세요. "REVIEW로 이동하려면 단위 테스트 통과 필수" 같은 규칙을 정하면 품질이 일정하게 유지됩니다.
💡 자동화 활용: GitHub Actions나 Jira 자동화로 PR 생성 시 자동으로 REVIEW로 이동하게 설정하세요. 수동 업데이트를 줄이면 보드가 항상 최신 상태를 유지합니다.
4. 스프린트 리뷰
시작하며
여러분이 2주 동안 열심히 개발했는데, 막상 이해관계자에게 보여줬더니 "이게 아닌데..."라는 반응을 받아본 적 있나요? 혹은 기능은 완성했지만 비즈니스 가치를 제대로 설명하지 못해 인정받지 못한 경험이 있을 겁니다.
이런 문제는 개발팀과 이해관계자 간의 소통 부족에서 발생합니다. 개발자는 기술적 관점에서, 이해관계자는 비즈니스 관점에서 생각하기 때문에 간극이 생깁니다.
결국 만든 제품이 실제 필요와 맞지 않아 재작업하게 되고, 시간과 비용이 낭비됩니다. 바로 이럴 때 필요한 것이 스프린트 리뷰입니다.
정기적으로 작동하는 소프트웨어를 시연하고 피드백을 받으면, 방향을 빠르게 수정하고 이해관계자의 신뢰를 얻을 수 있습니다.
개요
간단히 말해서, 스프린트 리뷰는 스프린트 마지막 날에 완성된 기능을 이해관계자에게 시연하고 피드백을 받는 회의입니다. 실무에서는 실제 작동하는 데모를 보여주는 것이 핵심입니다.
슬라이드가 아닌 실제 프로덕트를 실행하여 "로그인하면 이렇게 대시보드가 나타납니다"처럼 구체적으로 시연합니다. 예를 들어, 결제 기능을 개발했다면 실제로 테스트 카드로 결제해보고 영수증 이메일이 오는 것까지 보여줍니다.
이해관계자는 직접 눈으로 확인하기 때문에 신뢰가 생깁니다. 기존에는 프로젝트 끝에 한 번만 시연했다면, 이제는 매 스프린트마다 시연합니다.
이렇게 하면 잘못된 방향으로 가는 것을 조기에 발견하고, 이해관계자가 개발 과정에 참여하는 느낌을 받아 협업이 원활해집니다. 스프린트 리뷰의 핵심 특징은 세 가지입니다.
첫째, 완료된 것만 시연합니다(반쯤 된 것은 제외). 둘째, 양방향 대화로 진행하여 피드백을 즉시 받습니다.
셋째, 다음 스프린트 우선순위를 조정하는 기회로 활용합니다. 이러한 특징들이 제품의 품질과 고객 만족도를 높입니다.
코드 예제
// Sprint Review 관리 시스템
class SprintReview {
constructor(sprintNumber, sprintGoal) {
this.sprintNumber = sprintNumber;
this.sprintGoal = sprintGoal;
this.completedItems = [];
this.feedback = [];
this.stakeholders = [];
}
// 완료된 작업 추가 (Definition of Done 체크)
addCompletedItem(item, demo, businessValue) {
const completedItem = {
item,
demo, // 시연 방법
businessValue, // 비즈니스 가치
completedAt: new Date()
};
this.completedItems.push(completedItem);
console.log(`✓ 완료 항목 추가: ${item}`);
return completedItem;
}
// 이해관계자 추가
addStakeholder(name, role) {
this.stakeholders.push({ name, role });
}
// 피드백 수집
collectFeedback(stakeholder, comment, priority) {
const feedbackItem = {
from: stakeholder,
comment,
priority, // HIGH, MEDIUM, LOW
receivedAt: new Date()
};
this.feedback.push(feedbackItem);
console.log(`💬 피드백 수신: [${priority}] ${comment}`);
return feedbackItem;
}
// 리뷰 리포트 생성
generateReport() {
console.log('\n📋 === 스프린트 리뷰 리포트 ===\n');
console.log(`스프린트 #${this.sprintNumber}`);
console.log(`목표: ${this.sprintGoal}\n`);
console.log('✅ 완료된 항목:');
this.completedItems.forEach((item, i) => {
console.log(`${i + 1}. ${item.item}`);
console.log(` 💼 비즈니스 가치: ${item.businessValue}`);
console.log(` 🎬 시연: ${item.demo}`);
});
console.log('\n👥 참석자:');
this.stakeholders.forEach(s => {
console.log(` - ${s.name} (${s.role})`);
});
console.log('\n💬 수집된 피드백:');
const highPriorityFeedback = this.feedback.filter(f => f.priority === 'HIGH');
const otherFeedback = this.feedback.filter(f => f.priority !== 'HIGH');
if (highPriorityFeedback.length > 0) {
console.log('\n 🔴 높은 우선순위:');
highPriorityFeedback.forEach(f => {
console.log(` - [${f.from}] ${f.comment}`);
});
}
if (otherFeedback.length > 0) {
console.log('\n 🟡 일반 피드백:');
otherFeedback.forEach(f => {
console.log(` - [${f.priority}] [${f.from}] ${f.comment}`);
});
}
// 통계
console.log('\n📊 통계:');
console.log(` - 완료 항목 수: ${this.completedItems.length}`);
console.log(` - 참석자 수: ${this.stakeholders.length}`);
console.log(` - 피드백 수: ${this.feedback.length}`);
console.log(` - 높은 우선순위 피드백: ${highPriorityFeedback.length}`);
}
}
// 실제 사용 예시
const review = new SprintReview(5, '사용자 인증 시스템 구축');
// 참석자 등록
review.addStakeholder('김대표', 'CEO');
review.addStakeholder('이부장', '제품 책임자');
review.addStakeholder('박과장', '마케팅 팀장');
// 완료 항목 추가
review.addCompletedItem(
'소셜 로그인 (Google, Kakao)',
'실제 계정으로 로그인 → 프로필 정보 표시',
'신규 가입 전환율 30% 향상 예상'
);
review.addCompletedItem(
'비밀번호 재설정 기능',
'이메일 발송 → 링크 클릭 → 새 비밀번호 설정',
'고객 지원 문의 50% 감소 예상'
);
review.addCompletedItem(
'로그인 실패 알림',
'3회 실패 시 이메일 알림 발송',
'계정 보안 강화'
);
// 피드백 수집
review.collectFeedback('김대표', '소셜 로그인이 매우 빠르고 편리합니다', 'LOW');
review.collectFeedback('이부장', 'Apple 로그인도 추가해주세요 (iOS 사용자 많음)', 'HIGH');
review.collectFeedback('박과장', '로그인 완료 후 환영 이메일 발송 기능이 필요합니다', 'MEDIUM');
review.collectFeedback('김대표', '비밀번호 재설정 이메일 디자인 개선 필요', 'MEDIUM');
review.generateReport();
설명
이것이 하는 일: 이 코드는 스프린트 리뷰 회의를 체계적으로 관리하고, 피드백을 우선순위별로 정리하는 시스템입니다. 첫 번째로, SprintReview 클래스는 스프린트 번호와 목표로 초기화되며, 완료된 항목, 피드백, 참석자를 저장할 배열을 준비합니다.
이는 리뷰 회의의 모든 정보를 한곳에서 관리하고 나중에 이력을 추적할 수 있게 합니다. 그 다음으로, addCompletedItem 메서드가 실행될 때 단순히 작업 이름만 기록하는 것이 아니라, 시연 방법과 비즈니스 가치를 함께 저장합니다.
이것이 매우 중요한 이유는, 개발자는 기술적 구현에 집중하지만 이해관계자는 비즈니스 영향에 관심이 있기 때문입니다. "로그인 API 개발"이라고 말하는 것보다 "신규 가입 전환율 30% 향상"이라고 말하는 것이 훨씬 강력합니다.
또한 collectFeedback 메서드는 피드백을 우선순위(HIGH, MEDIUM, LOW)와 함께 저장합니다. 리뷰 회의에서는 다양한 의견이 나오는데, 모든 것을 다음 스프린트에 반영할 수는 없습니다.
우선순위를 명시적으로 기록하면, 회의 후 제품 백로그를 업데이트할 때 어떤 피드백을 먼저 반영할지 명확해집니다. 마지막으로, generateReport 메서드가 전체 리뷰 내용을 구조화된 형식으로 출력합니다.
특히 높은 우선순위 피드백을 별도로 하이라이트하여, 다음 스프린트 계획 시 놓치지 않도록 합니다. 통계 섹션은 스프린트의 생산성을 정량적으로 보여줍니다.
여러분이 이 코드를 사용하면 스프린트 리뷰를 더 효과적으로 진행할 수 있습니다. 회의 전에 완료 항목을 정리하여 준비가 철저해지고, 피드백을 체계적으로 수집하여 놓치는 것이 없으며, 자동 생성된 리포트를 팀과 공유하여 투명성을 높일 수 있습니다.
실전 팁
💡 실제 환경에서 시연: 가능하면 프로덕션과 유사한 환경(스테이징)에서 시연하세요. 로컬 개발 환경은 문제를 숨길 수 있습니다. 실제 데이터와 네트워크 상황에서 테스트해야 진짜 품질을 알 수 있습니다.
💡 스토리텔링 활용: "이 기능은..."이 아니라 "사용자가 로그인할 때..."처럼 사용자 관점의 스토리로 시연하세요. 이해관계자가 더 몰입하고 가치를 쉽게 이해합니다.
💡 미완성 숨기지 말기: 계획했지만 완료 못 한 것도 솔직하게 공유하세요. 이유를 설명하고 다음 계획을 제시하면 신뢰가 쌓입니다. 숨기면 나중에 더 큰 문제가 됩니다.
💡 즉석 피드백 환영: 시연 중 질문과 제안을 환영하는 분위기를 만드세요. "이거 색깔 바꿀 수 있어요?" 같은 작은 피드백도 즉시 메모하고, 가능하면 자리에서 바로 보여주면 협업감이 높아집니다.
💡 다음 스프린트 연결: 리뷰 마지막에 "이번 피드백을 바탕으로 다음 스프린트에서는 X를 할 계획입니다"라고 정리하세요. 피드백이 실제로 반영된다는 것을 보여주면 이해관계자가 더 적극적으로 참여합니다.
5. 스프린트 회고
시작하며
여러분이 프로젝트를 마칠 때마다 "다음엔 더 잘하자"라고 다짐하지만, 정작 다음 프로젝트에서도 같은 실수를 반복한 경험이 있나요? 혹은 팀에 문제가 있는데 아무도 먼저 이야기를 꺼내지 않아서 불편한 분위기가 계속된 적이 있을 겁니다.
이런 문제는 체계적인 개선 프로세스가 없기 때문에 발생합니다. 개인의 기억에만 의존하면 같은 실수가 반복되고, 불만이 쌓여 팀 분위기가 나빠지며, 점점 일하기 힘든 환경이 됩니다.
특히 빠르게 변화하는 IT 환경에서는 지속적인 개선이 없으면 뒤처지게 됩니다. 바로 이럴 때 필요한 것이 스프린트 회고입니다.
정기적으로 팀이 모여 무엇이 잘됐고 무엇을 개선할지 논의하면, 작은 문제를 빠르게 해결하고 팀 문화를 지속적으로 발전시킬 수 있습니다.
개요
간단히 말해서, 스프린트 회고는 스프린트가 끝난 후 팀이 모여 프로세스와 협업 방식을 개선하는 회의입니다. 실무에서는 "잘된 점(Keep)", "개선할 점(Problem)", "실행할 액션(Try)" 세 가지 관점에서 논의합니다.
예를 들어, "코드 리뷰가 너무 늦어서 배포가 지연됐다"는 문제를 발견하면, "리뷰 요청 후 24시간 이내 피드백 규칙"이라는 구체적인 액션 아이템을 정합니다. 중요한 것은 추상적인 반성이 아니라 실행 가능한 개선안을 만드는 것입니다.
기존에는 문제가 생겨도 바쁘다는 이유로 넘어갔다면, 이제는 정기적으로 개선 시간을 확보합니다. 이렇게 하면 작은 불편함이 큰 갈등으로 커지기 전에 해결할 수 있습니다.
스프린트 회고의 핵심 특징은 세 가지입니다. 첫째, 비난이 아닌 개선에 초점을 맞춥니다(사람이 아니라 프로세스를 논의).
둘째, 모든 팀원이 안전하게 의견을 말할 수 있는 환경을 만듭니다. 셋째, 반드시 실행 가능한 액션 아이템을 정하고 다음 스프린트에서 추적합니다.
이러한 특징들이 팀의 자율성과 성장 속도를 높입니다.
코드 예제
// Sprint Retrospective 관리 시스템
class SprintRetrospective {
constructor(sprintNumber) {
this.sprintNumber = sprintNumber;
this.items = {
keep: [], // 잘된 점 (계속할 것)
problem: [], // 문제점 (개선할 것)
try: [] // 시도할 것 (액션 아이템)
};
this.actionItems = [];
}
// 회고 항목 추가
addItem(category, content, author) {
if (!this.items[category]) {
console.log(`❌ 올바른 카테고리: keep, problem, try`);
return false;
}
const item = {
content,
author,
votes: 0,
addedAt: new Date()
};
this.items[category].push(item);
console.log(`✓ [${category.toUpperCase()}] "${content}" 추가됨`);
return item;
}
// 투표 (중요한 항목에 우선순위 부여)
voteItem(category, index) {
if (this.items[category] && this.items[category][index]) {
this.items[category][index].votes++;
console.log(`👍 투표됨: ${this.items[category][index].content} (${this.items[category][index].votes}표)`);
return true;
}
return false;
}
// 액션 아이템 생성 (구체적이고 측정 가능해야 함)
createActionItem(description, owner, dueDate) {
const action = {
id: `ACTION-${this.actionItems.length + 1}`,
description,
owner,
dueDate,
status: 'TODO',
createdAt: new Date()
};
this.actionItems.push(action);
console.log(`🎯 액션 아이템 생성: [${action.id}] ${description} (@${owner})`);
return action;
}
// 액션 아이템 완료 표시
completeAction(actionId) {
const action = this.actionItems.find(a => a.id === actionId);
if (action) {
action.status = 'DONE';
action.completedAt = new Date();
console.log(`✅ 완료: ${action.description}`);
return true;
}
return false;
}
// 회고 리포트 생성
generateReport() {
console.log('\n🔄 === 스프린트 회고 리포트 ===\n');
console.log(`스프린트 #${this.sprintNumber}\n`);
// 각 카테고리별 항목 (투표순 정렬)
['keep', 'problem', 'try'].forEach(category => {
const emoji = { keep: '✅', problem: '⚠️', try: '💡' };
const title = { keep: '잘된 점 (Keep)', problem: '개선할 점 (Problem)', try: '시도할 것 (Try)' };
console.log(`${emoji[category]} ${title[category]}:`);
const sortedItems = [...this.items[category]].sort((a, b) => b.votes - a.votes);
if (sortedItems.length === 0) {
console.log(' (항목 없음)');
} else {
sortedItems.forEach((item, i) => {
const votesDisplay = item.votes > 0 ? ` [${item.votes}표]` : '';
console.log(` ${i + 1}. ${item.content}${votesDisplay}`);
console.log(` by ${item.author}`);
});
}
console.log('');
});
// 액션 아이템
console.log('🎯 액션 아이템:');
if (this.actionItems.length === 0) {
console.log(' ⚠️ 액션 아이템이 없습니다! 구체적인 개선안을 만들어주세요.');
} else {
const todoActions = this.actionItems.filter(a => a.status === 'TODO');
const doneActions = this.actionItems.filter(a => a.status === 'DONE');
console.log(`\n 📋 할 일 (${todoActions.length}):`);
todoActions.forEach(a => {
console.log(` - [${a.id}] ${a.description}`);
console.log(` 담당: ${a.owner} | 기한: ${a.dueDate}`);
});
if (doneActions.length > 0) {
console.log(`\n ✅ 완료 (${doneActions.length}):`);
doneActions.forEach(a => {
console.log(` - [${a.id}] ${a.description}`);
});
}
}
// 통계
console.log('\n📊 통계:');
console.log(` - Keep: ${this.items.keep.length}개`);
console.log(` - Problem: ${this.items.problem.length}개`);
console.log(` - Try: ${this.items.try.length}개`);
console.log(` - 액션 아이템: ${this.actionItems.length}개 (완료: ${this.actionItems.filter(a => a.status === 'DONE').length})`);
}
}
// 실제 사용 예시
const retro = new SprintRetrospective(5);
// 팀원들이 회고 항목 추가
retro.addItem('keep', '페어 프로그래밍으로 버그가 크게 줄었음', '김철수');
retro.addItem('keep', '데일리 스크럼 시간을 10분으로 줄여서 효율적이었음', '이영희');
retro.addItem('problem', '코드 리뷰가 평균 3일 걸려서 배포 지연됨', '박민수');
retro.addItem('problem', '테스트 환경이 자주 다운되어 QA 어려움', '최영수');
retro.addItem('problem', '긴급 요청이 많아서 계획된 작업 못 함', '김철수');
retro.addItem('try', '리뷰 알림 봇 도입해보기', '이영희');
retro.addItem('try', '매주 금요일 오후는 기술 부채 해결 시간으로 고정', '박민수');
// 투표 (중요한 항목에)
retro.voteItem('problem', 0); // 코드 리뷰 지연
retro.voteItem('problem', 0);
retro.voteItem('problem', 0); // 3표
retro.voteItem('problem', 2); // 긴급 요청
retro.voteItem('problem', 2); // 2표
// 액션 아이템 생성 (투표 많은 문제 기반)
retro.createActionItem(
'코드 리뷰 24시간 내 1차 피드백 규칙 도입, Slack 알림 설정',
'이영희',
'다음 스프린트 2일차'
);
retro.createActionItem(
'긴급 요청 처리 프로세스 문서화, 제품 팀과 협의',
'김철수',
'다음 스프린트 5일차'
);
retro.createActionItem(
'테스트 환경 모니터링 도구 도입 (Uptime Robot)',
'최영수',
'다음 스프린트 3일차'
);
retro.generateReport();
// 다음 스프린트에서 액션 아이템 완료 표시
console.log('\n--- 다음 스프린트 진행 중 ---\n');
retro.completeAction('ACTION-1');
설명
이것이 하는 일: 이 코드는 스프린트 회고 회의를 체계적으로 진행하고, 개선안을 추적하는 시스템입니다. 첫 번째로, SprintRetrospective 클래스는 Keep/Problem/Try 세 가지 카테고리로 회고 항목을 관리합니다.
Keep은 잘된 점이므로 계속 유지해야 할 것들, Problem은 개선이 필요한 문제점, Try는 새롭게 시도해볼 아이디어입니다. 이 구조는 팀이 성공 경험도 인정하고(Keep), 문제도 솔직하게 이야기하며(Problem), 실험 정신을 유지하도록(Try) 균형을 잡아줍니다.
그 다음으로, voteItem 메서드가 실행되면서 팀원들이 중요하다고 생각하는 항목에 투표할 수 있습니다. 회고 회의에서는 많은 의견이 나오는데, 시간 제약상 모든 것을 깊이 논의할 수 없습니다.
투표를 통해 팀이 가장 시급하게 생각하는 문제를 파악하고, 그것에 집중할 수 있습니다. 예를 들어 "코드 리뷰 지연"이 3표를 받았다면, 이것이 팀의 최대 관심사임을 알 수 있습니다.
또한 createActionItem 메서드는 추상적인 논의를 구체적인 실행 계획으로 전환합니다. "코드 리뷰를 빨리 하자"는 너무 모호하지만, "24시간 내 1차 피드백 규칙 도입, Slack 알림 설정"은 명확하고 측정 가능합니다.
담당자(owner)와 기한(dueDate)을 명시하여 책임소재를 분명히 하고, 다음 스프린트에서 완료 여부를 확인할 수 있습니다. 마지막으로, generateReport 메서드가 회고 내용을 투표순으로 정렬하여 출력하고, 액션 아이템을 TODO/DONE으로 구분합니다.
이 리포트는 팀의 위키나 공유 문서에 저장하여, 몇 개월 후 "우리가 어떻게 발전해왔는지" 돌아볼 수 있는 귀중한 자료가 됩니다. 여러분이 이 코드를 사용하면 회고 회의를 더 생산적으로 만들 수 있습니다.
모든 의견이 체계적으로 수집되어 누락이 없고, 투표로 우선순위가 명확해지며, 구체적인 액션 아이템으로 실제 개선이 일어나고, 다음 회고에서 "지난번 액션 아이템 다 했나요?"라고 확인하여 지속적인 개선 문화를 만들 수 있습니다.
실전 팁
💡 안전한 분위기 조성: 회고는 비난이 아니라 개선이 목적임을 강조하세요. "누가 잘못했나"가 아니라 "우리가 어떻게 더 잘할 수 있나"에 집중합니다. 필요하면 익명으로 의견을 제출하는 방식도 고려하세요.
💡 구체적인 액션만: "더 열심히 하자" 같은 추상적인 액션은 금지하세요. "매일 오전 10시 코드 리뷰 확인" 처럼 누가, 언제, 무엇을 할지 명확한 것만 액션 아이템으로 만듭니다.
💡 작은 개선부터: 한 번에 많은 것을 바꾸려 하지 마세요. 스프린트당 2-3개의 실행 가능한 액션 아이템이 적당합니다. 작은 성공 경험이 쌓여야 개선 문화가 자리잡습니다.
💡 다음 회고에서 확인: 다음 회고 시작 시 이전 액션 아이템 완료 여부를 확인하세요. 안 지켜지는 액션은 현실적이지 않거나 우선순위가 낮은 것이므로, 솔직하게 인정하고 새로운 방법을 찾습니다.
💡 형식 바꾸기: 매번 같은 형식은 지루합니다. 가끔 "Mad/Sad/Glad", "Start/Stop/Continue", "4L(Liked/Learned/Lacked/Longed for)" 같은 다른 형식을 시도하여 신선함을 유지하세요.
6. 사용자 스토리 작성
시작하며
여러분이 요구사항을 받았을 때 "로그인 기능 만들어줘"처럼 막연한 지시를 받고 뭘 어떻게 만들어야 할지 고민한 경험이 있나요? 혹은 기능을 다 만들었는데 "이게 아닌데, 난 이런 걸 원한 게 아니야"라는 말을 들어본 적이 있을 겁니다.
이런 문제는 요구사항이 불명확하기 때문에 발생합니다. 개발자는 기술적 관점에서, 기획자는 비즈니스 관점에서 생각하기 때문에 서로 다른 것을 상상합니다.
결국 만든 후에야 차이를 발견하고, 재작업으로 시간과 비용이 낭비됩니다. 바로 이럴 때 필요한 것이 사용자 스토리입니다.
"~로서, ~를 원한다, 왜냐하면 ~" 형식으로 요구사항을 구조화하면, 개발자는 맥락을 이해하고 더 나은 해결책을 제안할 수 있습니다.
개요
간단히 말해서, 사용자 스토리는 "누가(Who), 무엇을(What), 왜(Why)" 원하는지를 짧은 문장으로 표현한 요구사항 명세입니다. 실무에서는 "~로서(As a), ~를 원한다(I want), 왜냐하면 ~(So that)" 형식을 사용합니다.
예를 들어, "일반 사용자로서, 소셜 로그인을 원한다, 왜냐하면 비밀번호를 기억하지 않아도 빠르게 접속하고 싶기 때문이다"처럼 작성합니다. 이렇게 하면 개발자는 단순히 "로그인 기능"이 아니라 "편의성"이 핵심 가치임을 이해하고, 사용자 경험에 집중한 구현을 할 수 있습니다.
기존에는 수십 페이지 기획서를 읽어야 했다면, 이제는 한 문장으로 핵심을 파악합니다. 물론 상세한 인수 조건(Acceptance Criteria)도 함께 작성하지만, 먼저 큰 그림을 공유하는 것이 중요합니다.
사용자 스토리의 핵심 특징은 세 가지입니다. 첫째, 사용자 관점에서 작성하여 공감대를 형성합니다.
둘째, 독립적이고 협상 가능하며 가치 있고 추정 가능하고 작고 테스트 가능해야 합니다(INVEST 원칙). 셋째, 완료 조건을 명확히 하여 "Done"의 기준을 합의합니다.
이러한 특징들이 개발팀과 비즈니스팀의 소통을 원활하게 합니다.
코드 예제
// User Story 관리 시스템
class UserStory {
constructor(id) {
this.id = id;
this.asA = ''; // 사용자 역할
this.iWant = ''; // 원하는 기능
this.soThat = ''; // 비즈니스 가치/이유
this.acceptanceCriteria = []; // 인수 조건
this.storyPoints = null; // 스토리 포인트 (복잡도)
this.status = 'DRAFT'; // DRAFT, READY, IN_PROGRESS, DONE
}
// 스토리 정의
define(asA, iWant, soThat) {
this.asA = asA;
this.iWant = iWant;
this.soThat = soThat;
console.log(`✓ 사용자 스토리 ${this.id} 정의됨`);
return this;
}
// 인수 조건 추가 (Given/When/Then 형식)
addAcceptanceCriteria(given, when, then) {
const criterion = {
given, // 전제 조건
when, // 행동
then, // 예상 결과
met: false // 충족 여부
};
this.acceptanceCriteria.push(criterion);
console.log(` ✓ 인수 조건 추가: ${when} → ${then}`);
return this;
}
// 스토리 포인트 추정
estimate(points) {
// 피보나치 수열 사용 (1, 2, 3, 5, 8, 13...)
const validPoints = [1, 2, 3, 5, 8, 13, 21];
if (!validPoints.includes(points)) {
console.log(`⚠️ 유효한 스토리 포인트: ${validPoints.join(', ')}`);
return this;
}
this.storyPoints = points;
if (points > 8) {
console.log(`⚠️ 스토리가 너무 큽니다 (${points}점). 더 작은 스토리로 분할을 고려하세요.`);
} else {
console.log(`✓ 스토리 포인트: ${points}`);
}
this.status = 'READY'; // 추정되면 개발 준비 완료
return this;
}
// 인수 조건 충족 표시
meetCriterion(index) {
if (this.acceptanceCriteria[index]) {
this.acceptanceCriteria[index].met = true;
console.log(`✅ 인수 조건 ${index + 1} 충족됨`);
// 모든 조건 충족 시 완료
if (this.acceptanceCriteria.every(c => c.met)) {
this.status = 'DONE';
console.log(`🎉 스토리 ${this.id} 완료!`);
}
return true;
}
return false;
}
// 스토리 카드 출력
print() {
console.log('\n' + '='.repeat(60));
console.log(`📖 사용자 스토리: ${this.id}`);
console.log('='.repeat(60));
console.log(`\n📝 스토리:`);
console.log(` ${this.asA}로서,`);
console.log(` ${this.iWant}를 원한다,`);
console.log(` 왜냐하면 ${this.soThat}.\n`);
console.log(`🎯 인수 조건 (${this.acceptanceCriteria.filter(c => c.met).length}/${this.acceptanceCriteria.length} 완료):`);
this.acceptanceCriteria.forEach((c, i) => {
const status = c.met ? '✅' : '⬜';
console.log(` ${status} ${i + 1}. Given: ${c.given}`);
console.log(` When: ${c.when}`);
console.log(` Then: ${c.then}\n`);
});
console.log(`📊 스토리 포인트: ${this.storyPoints || '미정'}`);
console.log(`📌 상태: ${this.status}`);
console.log('='.repeat(60) + '\n');
}
// INVEST 원칙 체크
checkINVEST() {
const checks = {
Independent: true, // 다른 스토리와 독립적인가
Negotiable: this.status === 'DRAFT', // 협상 가능한가
Valuable: this.soThat.length > 0, // 가치가 명확한가
Estimable: this.storyPoints !== null, // 추정 가능한가
Small: this.storyPoints <= 8, // 충분히 작은가
Testable: this.acceptanceCriteria.length > 0 // 테스트 가능한가
};
console.log('\n✓ INVEST 원칙 체크:');
Object.entries(checks).forEach(([key, value]) => {
const icon = value ? '✅' : '❌';
console.log(` ${icon} ${key}`);
});
return Object.values(checks).every(v => v);
}
}
// 실제 사용 예시
const story = new UserStory('US-101');
story.define(
'일반 사용자',
'소셜 로그인(Google, Kakao)',
'비밀번호를 기억하지 않아도 빠르고 안전하게 서비스를 이용하고 싶기 때문'
);
story.addAcceptanceCriteria(
'사용자가 로그인 페이지에 있을 때',
'Google 로그인 버튼을 클릭하면',
'Google 인증 페이지로 이동하고, 인증 완료 후 메인 페이지로 리다이렉트된다'
);
story.addAcceptanceCriteria(
'사용자가 Kakao 계정으로 첫 로그인할 때',
'프로필 정보(이름, 이메일) 제공에 동의하면',
'자동으로 회원가입되고 대시보드로 이동한다'
);
story.addAcceptanceCriteria(
'소셜 로그인 중 오류 발생 시',
'사용자가 취소하거나 네트워크 오류가 나면',
'친절한 오류 메시지를 보여주고 다시 시도할 수 있게 한다'
);
story.estimate(5);
story.print();
story.checkINVEST();
// 개발 진행 중 인수 조건 충족
console.log('\n--- 개발 진행 ---\n');
story.status = 'IN_PROGRESS';
story.meetCriterion(0); // Google 로그인 구현 완료
story.meetCriterion(1); // Kakao 로그인 구현 완료
story.meetCriterion(2); // 오류 처리 구현 완료
설명
이것이 하는 일: 이 코드는 사용자 스토리를 구조화하고, INVEST 원칙을 준수하며, 완료 조건을 추적하는 시스템입니다. 첫 번째로, UserStory 클래스는 define 메서드를 통해 "누가(asA), 무엇을(iWant), 왜(soThat)" 세 가지 요소를 저장합니다.
이 구조가 중요한 이유는, 단순히 "기능 목록"이 아니라 "사용자의 목표"를 중심으로 사고하게 만들기 때문입니다. 예를 들어 "관리자 권한 추가"보다 "관리자로서, 사용자를 차단하고 싶다, 왜냐하면 악의적인 행동을 막고 싶기 때문"이 훨씬 많은 맥락을 제공합니다.
그 다음으로, addAcceptanceCriteria 메서드가 Given/When/Then 형식으로 인수 조건을 추가합니다. 이것은 테스트 주도 개발(TDD)과 연결되는 부분으로, 각 조건이 곧 테스트 케이스가 됩니다.
"사용자가 로그인 페이지에 있을 때(Given), Google 버튼을 클릭하면(When), Google 인증 페이지로 이동한다(Then)"는 자동화 테스트로 직접 구현할 수 있을 만큼 구체적입니다. 모든 인수 조건이 충족되면 스토리가 자동으로 DONE 상태로 변경됩니다.
또한 estimate 메서드는 피보나치 수열(1, 2, 3, 5, 8, 13...)로 스토리 포인트를 추정합니다. 이 숫자들은 불확실성을 반영합니다.
1과 2의 차이는 작지만, 13과 21의 차이는 큽니다. 이는 복잡한 작업일수록 정확한 추정이 어렵다는 현실을 반영합니다.
8보다 큰 스토리는 경고를 표시하여 더 작게 나누도록 권장합니다. 마지막으로, checkINVEST 메서드가 좋은 사용자 스토리의 기준을 자동으로 검증합니다.
Independent(독립적), Negotiable(협상 가능), Valuable(가치 있음), Estimable(추정 가능), Small(작음), Testable(테스트 가능)이 모두 충족되는지 확인합니다. 이를 통해 팀은 스토리 작성 품질을 객관적으로 평가할 수 있습니다.
여러분이 이 코드를 사용하면 요구사항을 더 명확하게 정의할 수 있습니다. 개발자는 왜 이 기능이 필요한지 이해하여 더 나은 솔루션을 제안할 수 있고, 인수 조건으로 완료 기준이 명확해져 "아직 안 끝났어요" 논쟁이 사라지며, INVEST 체크로 스토리 품질을 유지할 수 있습니다.
실전 팁
💡 사용자 언어 사용: "데이터베이스에 레코드 삽입"이 아니라 "게시글 작성"처럼 사용자가 이해하는 언어로 작성하세요. 기술 용어는 인수 조건에서 구체화합니다.
💡 하나의 가치, 하나의 스토리: 한 스토리에 여러 기능을 넣지 마세요. "로그인, 회원가입, 비밀번호 재설정"은 세 개의 별도 스토리입니다. 각각 독립적으로 가치를 제공해야 합니다.
💡 테스트 가능하게: "UI가 예뻐야 한다"는 테스트 불가능합니다. "모바일에서 버튼이 최소 44x44px이어야 한다"처럼 측정 가능한 기준을 사용하세요.
💡 대화의 시작점: 스토리는 상세 명세가 아니라 대화의 시작점입니다. 개발자가 "왜 이게 필요하죠?"라고 물어볼 수 있는 환경을 만들고, 함께 더 나은 해결책을 찾으세요.
💡 스파이크 스토리 활용: 기술적 불확실성이 크면 "조사 스토리(Spike)"를 먼저 만드세요. "Redis vs Memcached 성능 비교 (3일)"처럼 시간을 정해 조사하고, 그 후 구현 스토리를 작성합니다.
7. 번다운 차트 활용
시작하며
여러분이 프로젝트를 진행할 때 "우리 지금 제시간에 끝낼 수 있나?"라고 불안해하거나, 마감 일주일 전에야 "이건 못 끝낼 것 같아요"라고 보고한 경험이 있나요? 혹은 일정이 늦어지는데 왜 늦어지는지 명확히 설명하지 못해 곤란했던 적이 있을 겁니다.
이런 문제는 진행 상황을 정량적으로 추적하지 않기 때문에 발생합니다. "거의 다 됐어요"나 "80% 완성"은 너무 주관적이고, 실제로는 남은 작업이 많을 수 있습니다.
객관적인 지표 없이는 문제를 조기에 발견할 수 없고, 이해관계자에게 현실적인 상황을 설명하기 어렵습니다. 바로 이럴 때 필요한 것이 번다운 차트입니다.
매일 남은 작업량을 그래프로 시각화하면, 일정 지연을 즉시 감지하고 대응책을 마련할 수 있으며, 데이터 기반으로 완료 시점을 예측할 수 있습니다.
개요
간단히 말해서, 번다운 차트는 시간에 따라 남은 작업량(스토리 포인트)이 줄어드는 것을 보여주는 그래프입니다. 실무에서는 Y축에 남은 스토리 포인트, X축에 날짜를 표시합니다.
이상적인 선(Ideal Line)과 실제 진행 선(Actual Line)을 함께 그려서 비교합니다. 예를 들어, 스프린트 시작 시 30포인트였는데 5일 차에 아직 25포인트가 남았다면, 이상적인 선보다 뒤처진 것입니다.
이 시점에서 팀은 "왜 느린가?"를 분석하고, 범위를 조정하거나 장애물을 제거하는 액션을 취합니다. 기존에는 "느낌"으로 진행 상황을 판단했다면, 이제는 숫자와 그래프로 객관적으로 평가합니다.
이렇게 하면 "정말 80% 완성됐나?"라는 의심 대신, "남은 5포인트를 2일 안에 끝낼 수 있나?"라는 구체적인 질문을 할 수 있습니다. 번다운 차트의 핵심 특징은 세 가지입니다.
첫째, 매일 업데이트하여 최신 상태를 유지합니다. 둘째, 추세선으로 완료 시점을 예측합니다(남은 속도로 계속하면 언제 끝나는지).
셋째, 팀 회의 시 공유하여 투명성을 높입니다. 이러한 특징들이 프로젝트 관리의 정확성과 예측 가능성을 크게 향상시킵니다.
코드 예제
// Burndown Chart 추적 시스템
class BurndownChart {
constructor(totalPoints, sprintDays) {
this.totalPoints = totalPoints; // 전체 스토리 포인트
this.sprintDays = sprintDays; // 스프린트 기간 (일)
this.dailyData = []; // 매일의 데이터
this.idealBurnRate = totalPoints / sprintDays; // 이상적인 하루 소진량
// 첫날 데이터
this.dailyData.push({
day: 0,
remaining: totalPoints,
completed: 0,
ideal: totalPoints
});
console.log(`📊 번다운 차트 초기화`);
console.log(` 전체: ${totalPoints}포인트, 기간: ${sprintDays}일`);
console.log(` 이상적인 하루 소진: ${this.idealBurnRate.toFixed(1)}포인트\n`);
}
// 하루 작업 기록
recordDay(completedToday) {
const lastDay = this.dailyData[this.dailyData.length - 1];
const currentDay = lastDay.day + 1;
const remaining = lastDay.remaining - completedToday;
const ideal = this.totalPoints - (this.idealBurnRate * currentDay);
const dayData = {
day: currentDay,
remaining: Math.max(0, remaining),
completed: this.totalPoints - remaining,
ideal: Math.max(0, ideal)
};
this.dailyData.push(dayData);
console.log(`📅 Day ${currentDay}: ${completedToday}포인트 완료`);
console.log(` 남은 포인트: ${dayData.remaining} (이상: ${ideal.toFixed(1)})`);
// 진행 상태 분석
if (dayData.remaining > ideal) {
const gap = dayData.remaining - ideal;
console.log(` ⚠️ 계획보다 ${gap.toFixed(1)}포인트 뒤처짐`);
} else if (dayData.remaining < ideal) {
const gap = ideal - dayData.remaining;
console.log(` ✅ 계획보다 ${gap.toFixed(1)}포인트 앞섬`);
} else {
console.log(` 👍 계획대로 진행 중`);
}
console.log('');
return dayData;
}
// 완료 예상일 계산
predictCompletion() {
if (this.dailyData.length < 2) {
return '데이터 부족';
}
// 최근 3일 평균 속도 계산
const recentDays = Math.min(3, this.dailyData.length - 1);
const recentData = this.dailyData.slice(-recentDays - 1);
let totalCompleted = 0;
for (let i = 1; i < recentData.length; i++) {
totalCompleted += recentData[i].completed - recentData[i - 1].completed;
}
const avgDailyVelocity = totalCompleted / recentDays;
const lastDay = this.dailyData[this.dailyData.length - 1];
const daysNeeded = Math.ceil(lastDay.remaining / avgDailyVelocity);
const expectedDay = lastDay.day + daysNeeded;
console.log(`🔮 완료 예측:`);
console.log(` 최근 ${recentDays}일 평균 속도: ${avgDailyVelocity.toFixed(1)}포인트/일`);
console.log(` 남은 포인트: ${lastDay.remaining}`);
console.log(` 예상 완료: Day ${expectedDay} (${daysNeeded}일 소요)`);
if (expectedDay > this.sprintDays) {
console.log(` 🚨 스프린트 마감(Day ${this.sprintDays})을 ${expectedDay - this.sprintDays}일 초과 예상`);
console.log(` 💡 제안: 범위 축소 또는 팀 리소스 추가 고려`);
} else {
console.log(` ✅ 스프린트 내 완료 가능`);
}
return expectedDay;
}
// ASCII 차트 시각화
visualize() {
console.log('\n📈 번다운 차트:\n');
console.log('포인트');
const maxPoint = this.totalPoints;
const height = 10; // 차트 높이
for (let i = height; i >= 0; i--) {
const value = (maxPoint / height) * i;
const label = value.toFixed(0).padStart(3, ' ');
let line = `${label} |`;
for (let day = 0; day < this.dailyData.length; day++) {
const dayData = this.dailyData[day];
const actualLevel = Math.round((dayData.remaining / maxPoint) * height);
const idealLevel = Math.round((dayData.ideal / maxPoint) * height);
if (actualLevel === i) {
line += ' ●'; // 실제 진행
} else if (idealLevel === i) {
line += ' ○'; // 이상적인 진행
} else {
line += ' ';
}
}
console.log(line);
}
// X축 (날짜)
let xAxis = ' +';
for (let day = 0; day < this.dailyData.length; day++) {
xAxis += '--';
}
console.log(xAxis);
let xLabels = ' ';
for (let day = 0; day < this.dailyData.length; day++) {
xLabels += ` ${day}`;
}
console.log(xLabels);
console.log(' 일차\n');
console.log(' ● 실제 진행 ○ 이상적인 진행\n');
}
// 스프린트 요약
getSummary() {
const lastDay = this.dailyData[this.dailyData.length - 1];
const completionRate = ((this.totalPoints - lastDay.remaining) / this.totalPoints * 100).toFixed(1);
const daysElapsed = lastDay.day;
const progressRate = (daysElapsed / this.sprintDays * 100).toFixed(1);
return {
totalPoints: this.totalPoints,
completed: this.totalPoints - lastDay.remaining,
remaining: lastDay.remaining,
completionRate: `${completionRate}%`,
daysElapsed: daysElapsed,
totalDays: this.sprintDays,
progressRate: `${progressRate}%`,
onTrack: lastDay.remaining <= lastDay.ideal
};
}
}
// 실제 사용 예시
const chart = new BurndownChart(30, 10); // 30포인트, 10일 스프린트
// 매일 작업 기록
chart.recordDay(3); // Day 1: 3포인트 완료
chart.recordDay(4); // Day 2: 4포인트 완료
chart.recordDay(2); // Day 3: 2포인트 완료 (느림)
chart.recordDay(5); // Day 4: 5포인트 완료 (따라잡기)
chart.recordDay(4); // Day 5: 4포인트 완료
chart.visualize();
chart.predictCompletion();
console.log('\n📊 스프린트 요약:');
const summary = chart.getSummary();
console.log(JSON.stringify(summary, null, 2));
설명
이것이 하는 일: 이 코드는 스프린트의 진행 상황을 매일 추적하고, 완료 시점을 예측하며, 차트로 시각화하는 시스템입니다. 첫 번째로, BurndownChart 클래스는 전체 스토리 포인트와 스프린트 기간으로 초기화되며, 이상적인 하루 소진량(idealBurnRate)을 자동 계산합니다.
예를 들어 30포인트를 10일에 완료해야 한다면, 매일 3포인트씩 줄어들어야 합니다. 이것이 "이상적인 선"의 기준이 됩니다.
그 다음으로, recordDay 메서드가 실행될 때마다 그날 완료한 포인트를 기록하고, 실제 남은 포인트와 이상적으로 남아야 할 포인트를 비교합니다. 만약 Day 3에 이상적으로는 21포인트가 남아야 하는데 실제로는 24포인트가 남았다면, "3포인트 뒤처짐"이라는 경고를 즉시 표시합니다.
이 조기 경고 시스템이 팀이 빠르게 대응할 수 있게 합니다. 또한 predictCompletion 메서드는 최근 3일의 평균 속도를 계산하여 완료 시점을 예측합니다.
단순히 전체 평균이 아니라 최근 데이터를 사용하는 이유는, 스프린트 초반과 후반의 속도가 다를 수 있기 때문입니다. 예를 들어 초반에 느렸지만 장애물을 제거한 후 속도가 빨라졌다면, 최근 속도가 더 정확한 예측을 제공합니다.
만약 예측 완료일이 스프린트 마감을 넘으면, 범위 축소나 리소스 추가를 제안합니다. 마지막으로, visualize 메서드가 ASCII 아트로 간단한 차트를 그립니다.
실제 프로젝트에서는 Jira, Trello, 또는 Google Sheets로 더 예쁜 차트를 만들겠지만, 이 코드의 핵심은 데이터 추적과 분석 로직입니다. ●는 실제 진행, ○는 이상적인 진행을 나타내며, 두 선의 간격이 벌어지면 문제가 있다는 신호입니다.
여러분이 이 코드를 사용하면 프로젝트 진행을 객관적으로 추적할 수 있습니다. 매일 데이터를 업데이트하여 최신 상태를 유지하고, 일정 지연을 조기에 감지하여 대응책을 마련하며, 이해관계자에게 "느낌"이 아닌 "데이터"로 현황을 보고할 수 있고, 과거 스프린트의 번다운 차트를 분석하여 팀 속도를 개선할 영역을 찾을 수 있습니다.
실전 팁
💡 매일 업데이트: 번다운 차트는 매일 업데이트해야 의미가 있습니다. 데일리 스크럼 시 작업 보드를 보며 완료된 포인트를 기록하는 습관을 만드세요. 자동화가 가능하면 더 좋습니다.
💡 스코프 변경 표시: 스프린트 중간에 작업이 추가되면 차트에 명시하세요. "Day 4에 5포인트 추가"라고 표시하면, 왜 차트가 갑자기 올라갔는지 이해할 수 있습니다.
💡 번업 차트도 활용: 번다운(남은 작업)만큼 번업(완료된 작업)도 유용합니다. 번업 차트는 누적 완료량을 보여주어, 팀이 실제로 일하고 있다는 것을 긍정적으로 보여줍니다.
💡 평평한 구간 분석: 차트가 며칠 동안 평평하다면(남은 포인트가 안 줄어듦), 이는 팀이 막혀있다는 신호입니다. 즉시 데일리 스크럼에서 논의하고 장애물을 제거하세요.
💡 완벽한 선형 아님: 번다운 차트가 완벽한 직선으로 내려가는 경우는 드뭅니다. 어떤 날은 빠르고, 어떤 날은 느립니다. 중요한 것은 전체 추세이므로, 하루 이틀의 변동에 과민반응하지 마세요.
8. Definition of Done
시작하며
여러분이 작업을 "완료했다"고 보고했는데, 팀 리더가 "테스트는 했어?", "문서는 작성했어?", "코드 리뷰는 받았어?"라고 물어보며 다시 작업하라고 한 경험이 있나요? 혹은 "완료"의 기준이 모호해서 서로 다르게 이해하여 혼란을 겪은 적이 있을 겁니다.
이런 문제는 "완료"의 정의가 명확하지 않기 때문에 발생합니다. 개발자는 "코드를 작성했으면 완료"라고 생각하지만, QA는 "테스트를 통과해야 완료", 제품 책임자는 "배포되어야 완료"라고 생각합니다.
이런 불일치는 재작업과 스프린트 목표 미달로 이어집니다. 바로 이럴 때 필요한 것이 Definition of Done(완료 정의)입니다.
팀이 합의한 명확한 체크리스트를 만들면, 모두가 같은 기준으로 "완료"를 판단하고, 품질이 일정하게 유지됩니다.
개요
간단히 말해서, Definition of Done은 작업이 "진짜 완료"되었다고 인정받기 위해 충족해야 할 조건들의 체크리스트입니다. 실무에서는 코드 작성뿐만 아니라 테스트, 리뷰, 문서화, 배포 준비까지 포함합니다.
예를 들어, 어떤 팀의 DoD는 "단위 테스트 커버리지 80% 이상", "2명의 코드 리뷰 승인", "API 문서 업데이트", "스테이징 환경 배포 완료"를 포함할 수 있습니다. 이 모든 조건을 충족해야만 작업 보드에서 "DONE" 칼럼으로 이동할 수 있습니다.
기존에는 각자 주관적으로 "완료"를 판단했다면, 이제는 객관적인 기준을 적용합니다. 이렇게 하면 "완료"에 대한 논쟁이 사라지고, 팀 전체가 같은 품질 기준을 유지할 수 있습니다.
Definition of Done의 핵심 특징은 세 가지입니다. 첫째, 팀이 함께 정의하고 합의합니다(일방적으로 강요되지 않음).
둘째, 측정 가능하고 검증 가능한 조건만 포함합니다. 셋째, 스프린트 회고에서 지속적으로 개선합니다(처음부터 완벽할 필요 없음).
이러한 특징들이 팀의 품질 문화와 투명성을 크게 향상시킵니다.
코드 예제
// Definition of Done 관리 시스템
class DefinitionOfDone {
constructor() {
this.criteria = []; // 완료 조건 목록
}
// 완료 조건 추가
addCriterion(name, description, validator) {
const criterion = {
id: this.criteria.length + 1,
name,
description,
validator, // 검증 함수
enabled: true
};
this.criteria.push(criterion);
console.log(`✓ DoD 조건 추가: ${name}`);
return criterion;
}
// 작업 검증
validate(task) {
console.log(`\n🔍 "${task.title}" DoD 검증 중...\n`);
const results = [];
let allPassed = true;
for (const criterion of this.criteria) {
if (!criterion.enabled) continue;
try {
const passed = criterion.validator(task);
results.push({
criterion: criterion.name,
passed,
message: passed ? '✅ 통과' : '❌ 실패'
});
if (!passed) {
allPassed = false;
console.log(`❌ [${criterion.name}] ${criterion.description}`);
} else {
console.log(`✅ [${criterion.name}] ${criterion.description}`);
}
} catch (error) {
results.push({
criterion: criterion.name,
passed: false,
message: `⚠️ 검증 오류: ${error.message}`
});
allPassed = false;
console.log(`⚠️ [${criterion.name}] 검증 오류: ${error.message}`);
}
}
console.log('\n' + '='.repeat(60));
if (allPassed) {
console.log('🎉 모든 DoD 조건 충족! 작업 완료 인정.');
task.status = 'DONE';
} else {
console.log('⚠️ 일부 DoD 조건 미충족. 작업 계속 필요.');
task.status = 'IN_PROGRESS';
}
console.log('='.repeat(60) + '\n');
return {
passed: allPassed,
results
};
}
// DoD 체크리스트 출력
printChecklist() {
console.log('\n📋 === Definition of Done 체크리스트 ===\n');
this.criteria.forEach(c => {
const status = c.enabled ? '☑️' : '☐';
console.log(`${status} ${c.id}. ${c.name}`);
console.log(` ${c.description}\n`);
});
}
// 조건 활성화/비활성화
toggleCriterion(id, enabled) {
const criterion = this.criteria.find(c => c.id === id);
if (criterion) {
criterion.enabled = enabled;
console.log(`${enabled ? '✓' : '✗'} "${criterion.name}" ${enabled ? '활성화' : '비활성화'}됨`);
return true;
}
return false;
}
}
// 실제 사용 예시
const dod = new DefinitionOfDone();
// 팀의 DoD 정의
dod.addCriterion(
'코드 작성 완료',
'기능 요구사항이 모두 구현됨',
(task) => task.codeComplete === true
);
dod.addCriterion(
'단위 테스트 통과',
'단위 테스트 커버리지 80% 이상, 모든 테스트 통과',
(task) => task.testCoverage >= 80 && task.testsPassed === true
);
dod.addCriterion(
'코드 리뷰 승인',
'최소 2명의 팀원으로부터 승인 받음',
(task) => task.reviewApprovals >= 2
);
dod.addCriterion(
'문서화 완료',
'README 또는 API 문서 업데이트됨',
(task) => task.documentationUpdated === true
);
dod.addCriterion(
'스테이징 배포',
'스테이징 환경에 배포되고 동작 확인됨',
(task) => task.deployedToStaging === true
);
dod.addCriterion(
'보안 점검',
'OWASP Top 10 취약점 점검 완료',
(task) => task.securityChecked === true
);
dod.printChecklist();
// 작업 검증 예시 1: 모든 조건 충족
console.log('\n--- 작업 1 검증 ---');
const task1 = {
title: '사용자 로그인 API',
codeComplete: true,
testCoverage: 85,
testsPassed: true,
reviewApprovals: 2,
documentationUpdated: true,
deployedToStaging: true,
securityChecked: true,
status: 'IN_PROGRESS'
};
dod.validate(task1);
console.log(`최종 상태: ${task1.status}`);
// 작업 검증 예시 2: 일부 조건 미충족
console.log('\n--- 작업 2 검증 ---');
const task2 = {
title: '프로필 페이지 UI',
codeComplete: true,
testCoverage: 60, // 미달
testsPassed: true,
reviewApprovals: 1, // 미달
documentationUpdated: false, // 미완료
deployedToStaging: true,
securityChecked: true,
status: 'IN_PROGRESS'
};
dod.validate(task2);
console.log(`최종 상태: ${task2.status}`);
// DoD 조건 조정 (회고에서 논의 후)
console.log('\n--- 스프린트 회고 후 DoD 조정 ---');
console.log('팀 결정: 초기 단계 프로젝트이므로 보안 점검은 나중 스프린트에 적용\n');
dod.toggleCriterion(6, false); // 보안 점검 비활성화
설명
이것이 하는 일: 이 코드는 팀의 완료 기준을 체계적으로 정의하고, 각 작업이 기준을 충족하는지 자동으로 검증하는 시스템입니다. 첫 번째로, DefinitionOfDone 클래스는 여러 개의 완료 조건(criteria)을 관리합니다.
각 조건은 이름, 설명, 그리고 검증 함수(validator)로 구성됩니다. 검증 함수는 작업 객체를 받아서 해당 조건을 충족하는지 true/false로 반환합니다.
예를 들어 "단위 테스트 통과" 조건은 testCoverage가 80 이상이고 testsPassed가 true인지 확인합니다. 이런 자동화된 검증은 사람의 주관이 개입하지 않아 공정하고 일관성 있습니다.
그 다음으로, validate 메서드가 실행되면서 모든 활성화된 조건을 순차적으로 검증합니다. 하나라도 실패하면 allPassed가 false가 되고, 작업은 여전히 IN_PROGRESS 상태로 유지됩니다.
이것이 중요한 이유는, "거의 다 했는데..."라는 애매한 상황을 방지하기 때문입니다. 모든 조건을 충족해야만 DONE 상태가 되므로, 스프린트 리뷰에서 시연할 때 품질이 보장됩니다.
또한 toggleCriterion 메서드를 통해 특정 조건을 활성화/비활성화할 수 있습니다. 이는 프로젝트 단계에 따라 DoD를 유연하게 조정할 수 있게 합니다.
예를 들어 초기 MVP 단계에서는 보안 점검을 건너뛰고, 나중에 프로덕션에 가까워지면 활성화할 수 있습니다. 또는 특정 유형의 작업(예: 문서 작업)은 단위 테스트 조건을 비활성화할 수 있습니다.
마지막으로, printChecklist 메서드가 팀의 DoD를 읽기 쉬운 형식으로 출력합니다. 이 체크리스트는 팀의 위키, Slack 채널, 또는 작업 보드에 게시하여 모든 팀원이 항상 참고할 수 있도록 합니다.
특히 새로운 팀원이 합류했을 때, "우리 팀의 완료 기준은 이것입니다"라고 명확히 알려줄 수 있어 온보딩이 빨라집니다. 여러분이 이 코드를 사용하면 품질 관리를 체계화할 수 있습니다.
"완료"에 대한 주관적 논쟁이 사라지고, 모든 작업이 일정한 품질 기준을 충족하며, 자동화된 검증으로 사람의 실수를 방지하고, 스프린트 회고에서 DoD를 지속적으로 개선하여 팀의 성숙도를 높일 수 있습니다.
실전 팁
💡 팀이 함께 정의: DoD는 위에서 강요되는 것이 아니라 팀이 함께 만들어야 합니다. 스프린트 회고나 별도 워크숍에서 "완료라고 하려면 뭐가 필요할까?"를 논의하세요. 합의된 기준이어야 지켜집니다.
💡 측정 가능하게: "코드가 깔끔해야 한다"는 주관적이므로 DoD에 적합하지 않습니다. "ESLint 경고 0개", "Complexity 10 이하"처럼 측정 가능한 기준을 사용하세요. 자동화 도구로 검증할 수 있으면 더 좋습니다.
💡 CI/CD 통합: GitHub Actions, GitLab CI 같은 도구로 DoD 조건을 자동 검증하세요. PR을 올리면 자동으로 테스트, 커버리지, 린트, 보안 검사가 실행되어 통과해야만 머지할 수 있게 만듭니다.
💡 점진적 개선: 처음부터 완벽한 DoD를 만들려 하지 마세요. 기본 조건(코드 완료, 테스트, 리뷰)으로 시작하고, 매 회고마다 "이번 스프린트에서 품질 문제가 있었나?"를 돌아보며 조건을 추가합니다.
💡 예외 처리: 간혹 특수한 상황(긴급 핫픽스 등)에서는 DoD 일부를 건너뛸 수 있습니다. 하지만 반드시 팀에 공유하고, 나중에 "기술 부채" 항목으로 추적하여 보완하세요. 예외가 일상이 되면 안 됩니다.
이 카드뉴스가 포함된 코스
댓글 (0)
함께 보면 좋은 카드 뉴스
마이크로서비스 배포 완벽 가이드
Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.
Application Load Balancer 완벽 가이드
AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
고객 상담 AI 시스템 완벽 구축 가이드
AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
AWS Bedrock 인용과 출처 표시 완벽 가이드
AWS Bedrock의 Citation 기능을 활용하여 AI 응답의 신뢰도를 높이는 방법을 배웁니다. 출처 추출부터 UI 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.