범위에서 기능점수까지
공개된 SISP 3.0 적용 지침의 표를 따라 측정 목적과 애플리케이션 경계를 정하고, 기본 처리와 논리 데이터를 식별해 DET, RET, FTR을 셉니다. 복잡도와 가중치를 적용해 조정 전 기능점수를 계산하는 코드를 실행하고, 요구가 바뀔 때 점수가 어떻게 달라지는지 추적합니다.
같은 취소 기능을 웹과 모바일 앱에서 함께 제공한다고 기능의 크기가 두 배가 되지는 않습니다. 기능점수는 구현한 코드의 줄 수가 아니라 사용자가 요구하는 논리적인 기능을 세기 때문입니다. 규모·비용 추정의 가정에서 종류만 나누어 보았던 여섯 기능을, 이 글에서는 데이터 항목과 참조 관계까지 세어 실제로 계산해 봅니다.
어떤 규칙으로 계산하는가
이 글의 복잡도 구간과 가중치는 브라질 정부가 공개한 SISP 「Roteiro de Métricas de Software」 3.0의 물리 PDF 30–40쪽, 특히 39쪽의 표 3–5와 40쪽의 표 6을 따릅니다. 한국어 설명과 모임 사례는 이 교재에서 새로 구성했습니다. 이 지침은 IFPUG가 기능점수를 세는 규칙을 모아 둔 측정 매뉴얼 CPM(Counting Practices Manual) 4.3.1을 참조하며, 지침 스스로 매뉴얼 전체를 대신하지 않는다고 밝힙니다. 여기서 CPM은 앞 글에서 본 임계 경로 기법과는 다른 말입니다.
그래서 이 글의 계산은 공개된 정부 지침을 적용해 보는 연습이며, 현행 IFPUG 표준 전체를 지켰다는 인증된 측정이 아닙니다. 이 교재는 CPM 원문 전체나 유료인 ISO 표준 본문을 확인하지 않았으므로, 실제 발주나 측정 업무에 쓰려면 어떤 판본의 규칙과 적용 지침을 쓸지부터 합의해야 합니다. 특히 이 SISP 판본은 EO와 EQ의 복잡도 구간을 표 하나로 제시하므로, 다른 방법의 표와 섞어 쓰지 않습니다.
도움말 기능을 세는 방법을 다룬 IFPUG의 iTip #1 「Help!」(2011년 1.0판)도 논리적으로 같은 기능을 중복해 세지 않는 판단과 EQ의 사례를 설명합니다. 이 자료의 3–4쪽은 DET가 3개나 4개이고 참조하는 파일이 1개인 도움말 조회를 복잡도가 낮은 EQ로 분류합니다. 다만 iTip도 전체 규칙집이 아니라 해석의 사례입니다.
1단계: 측정 목적과 애플리케이션 경계
이번 측정의 목적은 주어진 모임 기능 묶음의 조정 전 기능 크기를 구하는 것이며, 개발 비용이나 필요한 인원을 계산하려는 것이 아닙니다. 범위에는 아래의 여섯 논리 기능만 넣고, 관리자 화면이나 바깥 회원 시스템 전체가 구현되어 있다고 가정하지 않습니다.
측정하는 애플리케이션과 바깥 세계를 나누는 선을 애플리케이션 경계(application boundary)라고 합니다. 경계 안에는 모임과 참가 내역을 관리하는 모임 앱이 있고, 경계 밖의 회원 시스템은 참가 자격 정보를 관리하며 모임 앱은 그 정보를 읽기만 합니다. 웹 화면과 모바일 화면, 저장 라이브러리, 서버의 배치가 나뉘어 있어도 그것만으로 경계를 더 긋지 않으며, 사용자가 별개의 애플리케이션으로 인식하는지와 어떤 데이터를 누가 관리하는지로 경계를 판단합니다.
기본 처리(elementary process)는 사용자에게 의미 있는 가장 작은 활동으로, 그 활동을 마치면 업무가 일관된 상태로 남아야 합니다. 신청 버튼을 누르는 일, 안에서 입력을 검사하는 일, 저장 함수를 부르는 일은 하나의 신청을 끝내기 위한 부분이므로 기능 세 개로 나누어 세지 않습니다. 웹과 모바일의 취소가 같은 목적, 같은 데이터, 같은 처리 논리를 쓴다면 하나의 기본 처리로 세고, 화면의 이름이 같더라도 처리 논리가 다르다면 다시 비교합니다.
| 종류 | 식별하는 기준 | 이 사례의 판단 |
|---|---|---|
| ILF | 경계 안의 애플리케이션이 관리하는, 사용자가 인식하는 논리 데이터 | 모임과 참가 내역 한 묶음 |
| EIF | 이 애플리케이션은 참조만 하고 다른 애플리케이션이 관리하는 논리 데이터 | 회원 시스템의 참가 자격 한 묶음 |
| EI | 입력의 주된 목적이 ILF를 관리하거나 동작을 바꾸는 것 | 참가 신청과 취소, 각각 하나의 기본 처리 |
| EO | 단순한 조회 외에 계산이나 새로 만든 값을 포함한 출력 | 참가율을 계산한 요약 |
| EQ | 계산, 새로 만든 값, ILF 관리, 동작 변경 없이 데이터를 가져오는 조회 | 모임 제목 조회 |
EIF는 바깥 시스템에 보내는 HTTP 요청의 횟수가 아닙니다. 같은 논리 데이터를 두 번 불러도 EIF는 하나이고, 반대로 사용자가 별개로 인식하는 논리 데이터 두 묶음을 JSON 파일 하나에 담았다고 하나로 합쳐 세지도 않습니다.
2단계: 데이터 기능의 DET와 RET를 센다
DET(data element type)는 사용자가 인식하는, 서로 겹치지 않는 데이터 항목의 종류입니다. RET(record element type)는 논리 데이터 안에서 사용자가 인식하는 하위 묶음으로, 행의 수나 회원의 수를 세는 말이 아닙니다. SISP 지침은 이 둘을 포르투갈어 약어로 각각 DER, RLR이라고 씁니다.
계산을 위해 측정 사례의 요구를 다음과 같이 정합니다. 이는 앞 장들의 작은 core.mjs에 날짜나 출처 기능이 이미 있다는 뜻이 아니라, 계산을 연습하려고 넓힌 요구 명세입니다.
| 논리 데이터 | 사용자가 인식하는 하위 묶음과 항목 | 겹치지 않는 DET | RET | 관리하는 쪽 |
|---|---|---|---|---|
| 모임과 참가 내역 | 모임: id, title, capacity, status / 선택적인 참가 내역: memberId, joinedAt, cancelledAt, source | 8 | 2 | 모임 앱 |
| 참가 자격 | memberId, eligible, validUntil | 3 | 1 | 바깥 회원 시스템 |
참가 내역을 모임에 딸린 선택적인 하위 묶음으로 사용자가 인식한다고 요구했기 때문에 모임 데이터의 RET를 2로 둡니다. 만약 업무에서 참가 내역을 독립된 논리 데이터로 인식한다면, 논리 데이터를 식별하는 단계부터 달라집니다. 데이터베이스의 테이블 수만 보고 RET를 정하지 않으며, 참가 내역에 관계를 나타내려고 모임 id가 다시 나오더라도 이 논리 데이터 안에서 같은 항목을 두 번 세지 않는다는 전제도 함께 적어 둡니다.
다른 시스템과 함께 쓰는 데이터에서는 이 범위가 실제로 쓰는 항목만 셉니다. 바깥 회원 시스템의 모든 필드나 데이터베이스 내부의 키를 자동으로 DET에 넣지 않으며, 주소를 항목 하나로 쓰는지 우편번호와 시도를 따로 처리하는지도 사용자 요구에 따라 달라집니다.
3단계: 거래 기능의 DET와 FTR을 센다
FTR(file type referenced)은 기본 처리가 읽거나 관리하는 ILF와, 읽는 EIF의 개수입니다. 같은 ILF를 읽고 쓰더라도 FTR은 하나이며, SISP 지침은 이를 ALR이라고 씁니다. 참조하는 파일의 모든 항목이 경계를 넘나드는 것은 아니므로 FTR과 DET는 따로 셉니다.
이 사례의 각 거래에는 실행을 시작하는 제어 수단 하나와, 끝났는지 실패했는지를 사용자에게 알리는 응답 수단 하나가 있으며, SISP 지침 37쪽의 규칙에 따라 각각을 DET 하나로 셉니다. 같은 처리를 여러 버튼으로 시작하거나 오류 문구가 여러 가지여도 그 수만큼 늘리지 않고, 화면 제목이나 고정된 문구, 페이지 넘김 같은 탐색 요소도 DET에 더하지 않습니다.
| 기본 처리 | 경계를 넘나드는 DET | DET | 참조하는 논리 데이터 | FTR |
|---|---|---|---|---|
| 신청 EI | sessionId, memberId, 시작 수단, 응답 수단 | 4 | 모임 데이터 읽기와 관리, 바깥 참가 자격 읽기 | 2 |
| 취소 EI | sessionId, memberId, 시작 수단, 응답 수단 | 4 | 모임 데이터 읽기와 관리 | 1 |
| 참가율 요약 EO | sessionId, registeredCount, capacity, ratio, 시작 수단, 응답 수단 | 6 | 모임 데이터 읽기 | 1 |
| 제목 조회 EQ | sessionId, title, 시작 수단, 응답 수단 | 4 | 모임 데이터 읽기 | 1 |
요약에서 sessionId를 입력받고 다시 출력하더라도 같은 항목이므로 한 번만 셉니다. 요약은 ratio = registeredCount / capacity를 계산하므로 EO이고, 제목 조회는 새 값을 만들거나 저장을 바꾸지 않으므로 EQ입니다. "조회 버튼"이라는 이름만으로 EQ가 되는 것은 아닙니다. 신청할 때 안에서 읽는 eligible과 validUntil은 사용자에게 입력받거나 보여 주지 않는다고 요구했으므로 거래의 DET에 넣지 않습니다.
4단계: 복잡도와 가중치를 적용한다
아래 구간은 SISP 3.0 물리 39쪽 표 3–5의 규칙을 옮겨 정리한 것입니다. 여기서 낮음, 보통, 높음은 기능점수의 분류일 뿐, 구현이 쉽거나 어렵다는 주관적인 평가가 아닙니다.
| ILF·EIF의 RET | DET 1–19 | DET 20–50 | DET 51 이상 |
|---|---|---|---|
| 1 | 낮음 | 낮음 | 보통 |
| 2–5 | 낮음 | 보통 | 높음 |
| 6 이상 | 보통 | 높음 | 높음 |
| EI의 FTR | DET 1–4 | DET 5–15 | DET 16 이상 |
|---|---|---|---|
| 0–1 | 낮음 | 낮음 | 보통 |
| 2 | 낮음 | 보통 | 높음 |
| 3 이상 | 보통 | 높음 | 높음 |
| 이 지침의 EO·EQ FTR | DET 1–5 | DET 6–19 | DET 20 이상 |
|---|---|---|---|
| 0–1 | 낮음 | 낮음 | 보통 |
| 2–3 | 낮음 | 보통 | 높음 |
| 4 이상 | 보통 | 높음 | 높음 |
EO와 EQ는 지침에서 같은 표를 쓰지만, EQ는 FTR이 적어도 1개여야 한다는 표 5의 주석도 함께 적용합니다. 이 글에서 계산하는 EQ는 FTR이 1개이며, 앞에서 본 IFPUG 도움말 사례의 낮은 복잡도 판단과도 조건이 같습니다. 복잡도에 따른 가중치는 SISP 3.0 물리 40쪽의 표 6에 나와 있습니다.
| 종류 | 낮음 | 보통 | 높음 |
|---|---|---|---|
| ILF | 7 | 10 | 15 |
| EIF | 5 | 7 | 10 |
| EI | 3 | 4 | 6 |
| EO | 4 | 5 | 7 |
| EQ | 3 | 4 | 6 |
모임 ILF는 DET 8, RET 2이므로 낮음 7점이고, 참가 자격 EIF는 DET 3, RET 1이므로 낮음 5점입니다. 신청 EI는 파일 두 개를 참조하지만 DET가 4로 첫 구간에 있으므로 보통이 아니라 낮음 3점이고, 요약 EO는 DET가 6이어도 FTR이 1인 행에서는 둘째 칸도 낮음이므로 4점입니다.
| 기능 | 종류 | 판단한 수량 | 복잡도 | 가중치 |
|---|---|---|---|---|
| 모임과 참가 내역 | ILF | DET 8, RET 2 | 낮음 | 7 |
| 참가 자격 | EIF | DET 3, RET 1 | 낮음 | 5 |
| 신청 | EI | DET 4, FTR 2 | 낮음 | 3 |
| 취소 | EI | DET 4, FTR 1 | 낮음 | 3 |
| 참가율 요약 | EO | DET 6, FTR 1 | 낮음 | 4 |
| 제목 조회 | EQ | DET 4, FTR 1 | 낮음 | 3 |
| 합계 | 25 |
이렇게 범위 안의 기능별 가중치를 더한 조정 전의 크기를 UFP(unadjusted function points)라고 하며, 이 사례는 7 + 5 + 3 + 3 + 4 + 3 = 25 UFP입니다. 기능이 여섯 개라서 6점이 되는 것이 아닙니다. 시스템의 일반적인 특성에 따른 예전 방식의 보정, 데이터 전환 기능, 변경 프로젝트에 쓰는 별도의 식, 발주 단가는 이 합계에 넣지 않았습니다. 이 값을 사람·일이나 완료 날짜로 바꾸려면 생산성에 대한 별도의 자료와 조건이 필요합니다.
5단계: 계산을 코드로 확인한다
표를 적용하는 계산은 코드로 옮겨 확인할 수 있습니다. 다음 코드를 reading-lab/examples/ch8-4.mjs로 저장해 실행합니다. 아래 clubFunctions의 수량은 앞의 표에서 사람이 판단해 넣은 값입니다.
const weights = {
ILF: [7, 10, 15],
EIF: [5, 7, 10],
EI: [3, 4, 6],
EO: [4, 5, 7],
EQ: [3, 4, 6],
};
const matrix = [
[0, 0, 1],
[0, 1, 2],
[1, 2, 2],
];
const levels = ["낮음", "보통", "높음"];
const band = (value, firstMax, secondMax) => (value <= firstMax ? 0 : value <= secondMax ? 1 : 2);
function requireCount(value, minimum, name) {
if (!Number.isSafeInteger(value) || value < minimum) {
throw new TypeError(`${name} must be an integer >= ${minimum}`);
}
}
function classify(fn) {
if (!Object.hasOwn(weights, fn.type)) throw new TypeError("unknown function type");
requireCount(fn.det, 1, "DET");
let row;
let column;
if (fn.type === "ILF" || fn.type === "EIF") {
requireCount(fn.ret, 1, "RET");
row = band(fn.ret, 1, 5);
column = band(fn.det, 19, 50);
} else if (fn.type === "EI") {
requireCount(fn.ftr, 0, "FTR");
row = band(fn.ftr, 1, 2);
column = band(fn.det, 4, 15);
} else {
requireCount(fn.ftr, fn.type === "EQ" ? 1 : 0, "FTR");
row = band(fn.ftr, 1, 3);
column = band(fn.det, 5, 19);
}
const index = matrix[row][column];
return { level: levels[index], weight: weights[fn.type][index] };
}
function measure(functions) {
return functions.reduce((sum, fn) => sum + classify(fn).weight, 0);
}
const clubFunctions = [
{ id: "club-record", type: "ILF", det: 8, ret: 2 },
{ id: "eligibility", type: "EIF", det: 3, ret: 1 },
{ id: "register", type: "EI", det: 4, ftr: 2 },
{ id: "cancel", type: "EI", det: 4, ftr: 1 },
{ id: "summary-ratio", type: "EO", det: 6, ftr: 1 },
{ id: "title-lookup", type: "EQ", det: 4, ftr: 1 },
];
for (const fn of clubFunctions) {
const { level, weight } = classify(fn);
console.log(fn.id, fn.type, level, weight);
}
console.log("UFP:", measure(clubFunctions));
const changed = clubFunctions.map((fn) =>
fn.id === "register" ? { ...fn, det: 5 } : fn.id === "club-record" ? { ...fn, det: 9 } : fn,
);
console.log("phone 추가 뒤 UFP:", measure(changed));club-record ILF 낮음 7
eligibility EIF 낮음 5
register EI 낮음 3
cancel EI 낮음 3
summary-ratio EO 낮음 4
title-lookup EQ 낮음 3
UFP: 25
phone 추가 뒤 UFP: 26band는 값이 첫 구간의 끝 이하이면 0, 둘째 구간의 끝 이하이면 1, 그보다 크면 2를 돌려주고, matrix[row][column]이 그 행과 열에 해당하는 복잡도의 번호를 알려 줍니다. 세 표는 구간의 끝값만 다르고 낮음, 보통, 높음이 놓인 모양은 같으므로 matrix 하나를 함께 씁니다. requireCount는 수량이 정수가 아니거나 너무 작으면 거절하며, FTR이 0인 EQ도 여기서 거절됩니다.
마지막 줄은 다음 절에서 다룰 요구 변경을 계산한 것입니다. 이 코드는 사람이 판단해 넣은 수량에 표를 적용해 확인할 뿐, 요구 문장을 읽고 애플리케이션 경계나 기본 처리를 찾아 주지는 않습니다. 기능을 잘못 식별해 넣으면 합계의 계산이 정확해도 측정은 틀립니다.
6단계: 요구가 바뀔 때 달라지는 것을 추적한다
신청할 때 사용자가 전화번호 phone을 하나 더 입력하고, 그 값을 모임 데이터에도 보관하기로 합의했다고 해 봅시다. 신청 EI의 DET는 4에서 5가 되고 FTR은 2 그대로라서, EI 표에서 낮음 3점이 보통 4점으로 바뀝니다. 모임 ILF의 DET도 8에서 9로 늘지만 같은 낮음 구간에 머뭅니다. 그래서 여섯 기능의 합계는 26 UFP가 되며, 필드 하나를 더했다고 모든 기능의 점수가 오르지는 않습니다.
바깥의 참가 자격 정보를 모임 앱이 직접 관리하게 된다면, 그 데이터를 EIF로 볼지 ILF로 볼지 경계의 판단부터 바뀝니다. 같은 DET와 RET라면 ILF의 가중치는 7, EIF는 5이므로 2점 차이가 나지만, 자격 정보를 관리하는 새 기본 처리가 생기는지도 따로 식별해야 하므로 이것만으로 바뀐 뒤의 크기를 27이라고 단정할 수 없습니다. 웹과 모바일에서 같은 취소 처리를 하나 더 보여 주는 것만으로는 취소 EI를 더하지 않으며, 측정 기록에는 숫자와 함께 이런 판단의 근거를 남깁니다.
정리
- 기능점수는 코드가 아니라 사용자가 요구하는 논리적인 기능을 세며, 어떤 판본의 규칙과 적용 지침을 썼는지 함께 밝혀야 합니다.
- 측정 목적과 애플리케이션 경계를 먼저 정하고, 사용자에게 의미 있는 가장 작은 활동인 기본 처리와 논리 데이터를 식별합니다.
- 데이터 기능은 DET와 RET로, 거래 기능은 DET와 FTR로 복잡도를 정하며, 같은 항목이나 같은 파일을 두 번 세지 않습니다.
- 복잡도에 따른 가중치를 더한 것이 조정 전 기능점수 UFP이고, 이 사례는 25 UFP입니다.
- 요구가 바뀌면 어느 기능의 수량이 어느 구간을 넘는지 추적하며, 코드는 사람이 판단한 수량에 표를 적용하는 것만 확인해 줍니다.
연습 문제
1. 같은 한 항목이라도 점수가 다를까
본문의 처음 사례에서 제목 조회 EQ에 사용자에게 보여 줄 startAt 항목 하나만 더합니다. 다른 요구는 그대로일 때 DET, FTR, 복잡도와 여섯 기능의 합계를 차례로 구하고, 신청에 phone을 더한 경우와 비교하세요.
해설 보기
제목 조회의 DET는 4에서 5가 되고 FTR은 1입니다. EO와 EQ 표에서 DET 5는 아직 첫 구간이므로 낮음 3점 그대로이고, 합계도 25입니다. 반면 신청은 DET가 4에서 5로 늘면서 EI 표의 구간 끝을 넘었고 FTR이 2였으므로 보통 4점이 되어 합계가 26이 되었습니다. 같은 항목 하나를 더해도 기능의 종류와 참조하는 파일의 수에 따라 결과가 달라집니다. startAt을 모임 데이터에도 보관하기로 했다면 데이터 기능의 DET도 따로 확인해야 합니다.
2. 웹과 모바일의 취소
웹과 모바일 앱이 같은 취소 처리를 부르는데, 누군가 "화면이 두 개이니 취소 EI를 두 개로 세자"고 했습니다. 이 판단을 평가하고, 둘을 따로 세야 하는 경우는 언제인지 설명하세요.
해설 보기
두 화면이 같은 목적, 같은 데이터, 같은 처리 논리를 쓴다면 하나의 기본 처리이므로 취소 EI는 하나로 셉니다. 화면의 수를 따라 세면 같은 사용자 기능을 두 번 세어 크기를 부풀리게 됩니다. 다만 모바일의 취소만 위약금을 계산하거나 다른 데이터를 참조하는 식으로 처리 논리가 다르다면, 서로 다른 기본 처리인지 다시 판단해야 합니다.
3. 참조 파일이 없는 조회
어떤 개발자가 기능 목록에 { id: "help", type: "EQ", det: 3, ftr: 0 }을 넣었더니 본문 코드가 오류를 냈습니다. 왜 거절되었는지 설명하고, 이 기능을 어떻게 다시 판단해야 할지 적으세요.
해설 보기
이 지침에서 EQ는 적어도 하나의 논리 파일을 참조해 데이터를 가져오는 조회이므로 FTR이 1 이상이어야 하며, 코드는 이 규칙에 따라 FTR must be an integer >= 1 오류를 냅니다. 도움말 문구가 화면에 고정된 글일 뿐이라면 데이터를 가져오는 조회가 아니므로 EQ로 세지 않을 수 있습니다. 반대로 도움말이 사용자가 인식하는 논리 데이터로 관리되고 그 데이터를 읽어 보여 준다면 FTR을 1로 바로잡아 셉니다. 어느 쪽인지는 코드가 아니라 요구를 보고 판단합니다.
크기, 구현의 어려움, 학습자의 이해도는 서로 다른 측정 대상이며, 어떤 숫자든 그 정의와 범위, 판본을 함께 남겨야 합니다. 다음 장의 품질 요구와 평가 모델에서는 소프트웨어의 품질을 무엇으로 정의하고 어떻게 평가하는지 다룹니다.
원자료와 더 읽기
복잡도 구간과 가중치는 SISP 「Roteiro de Métricas de Software」 3.0의 물리 39–40쪽 표 3–6을, 중복 판단과 EQ 사례는 IFPUG iTip #1을 참고했습니다. 모임 사례와 계산 코드는 이 교재에서 새로 구성했으며, 인증된 측정 결과가 아닙니다.