AI 개발 · 심화
AI 코딩 도구로 팀 개발하기 - Java 실무 저장소편
비용·권한·속도의 경계 - 병렬과 자동화를 언제 멈추나
AI 호출 비용과 사람 검토 시간, 병렬 작업이 충돌과 검토 부담을 늘리는 경우, 운영 배포·데이터 삭제 같은 되돌릴 수 없는 일은 사람 승인, 완성 코드는 작업마다 예상 비용·위험도를 매겨 자동 처리·사람 승인·중단으로 나누는 결정표를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
작업 기록을 남기면 다음 사람이 변경의 이유를 이어받을 수 있다. 앞 장에서 만든 기록에는 한 가지 정보가 더 필요하다. 이 작업을 계속하는 데 얼마의 자원을 쓸 수 있고, 어느 지점에서 사람이 판단해야 하는가다. AI가 코드를 빠르게 만들어도 검토할 사람이 부족하면 팀의 완료 속도는 빨라지지 않는다. 호출을 더 보내는 선택이 오히려 미검토 변경과 충돌을 쌓을 수도 있다.
이 장에서는 작은 쇼핑몰 주문 서비스의 작업 목록을 받아 예상 비용과 위험을 분류하는 프로그램을 만든다. 실제 AI를 호출하거나 저장소를 수정하지 않는다. 주문·재고·결제 모의 객체를 고치는 작업과 운영 작업을 같은 목록에 놓고, 실행 전에 어떤 경계를 확인해야 하는지 연습한다.
- AI 호출 비용과 사람의 검토 시간을 서로 다른 자원으로 계산한다.
- 병렬 작업의 수보다 변경 영역과 검토 대기량을 먼저 확인한다.
- 운영 배포와 데이터 삭제를 사람 승인 대상으로 분류한다.
- 작업별 한도와 전체 한도를 적용해 자동 처리·사람 승인·중단 결정표를 출력한다.
- 한도와 승인 규칙이 경계값에서도 유지되는지 작은 실행 검사로 확인한다.
문제 상황
팀은 주문 서비스 저장소에서 세 가지 변경을 준비하고 있다. 재고 차감 조건을 정리하고, 결제 모의 객체의 응답을 보완하고, 주문 취소 경로를 수정하는 일이다. 담당자는 서로 다른 AI 작업에 세 일을 나누어 맡기면 빨리 끝날 것이라고 생각한다. 그러나 재고 조건 정리와 주문 취소 수정은 모두 재고 처리 영역을 건드린다. 각각의 변경이 읽기 쉬워도 함께 적용하면 예약 수량과 취소 수량의 계산을 다시 검토해야 한다.
여기에 운영 배포 준비와 오래된 주문 데이터 삭제 요청이 들어온다. 두 작업은 예상 호출 비용이 작다. 그렇다고 코드 정리와 같은 권한으로 실행할 수는 없다. 데이터 삭제는 변경 파일을 되돌리는 것만으로 복구되지 않는다. 운영 배포도 이전 버전을 다시 올리는 절차만으로 이미 발생한 결제나 외부 통신까지 취소할 수 있다고 가정해서는 안 된다.
AI의 제안: “작업을 모두 병렬로 시작하고, 끝난 변경을 한꺼번에 검토하면 대기 시간을 줄일 수 있다.”
이 제안에는 시작 속도만 있다. 누가 결과를 읽는지, 같은 영역의 변경이 겹치는지, 승인 전에 어떤 명령까지 실행할 수 있는지는 빠져 있다. 팀이 필요한 것은 더 많은 실행 버튼이 아니라 시작을 제한하는 규칙이다.
예제에서는 작업을 입력 순서대로 심사한다. 먼저 들어온 작업이 자원을 예약하며, 뒤의 작업은 남은 자원 안에서 판단한다. 이것이 가장 좋은 우선순위라는 뜻은 아니다. 순서를 고정해야 결과를 재현하고 이유를 설명하기 쉽기 때문에 선택한 정책이다. 우선순위를 바꾸려면 작업 목록을 다시 정렬하고 결정표도 다시 검토해야 한다.
비용은 호출량과 검토 시간으로 나누어 본다
예제의 비용 단위는 실제 화폐나 제품 요금이 아니다. 팀이 연습용으로 만든 정수 단위다. 작업별 예상 비용에는 최초 요청뿐 아니라 예상 수정 요청과 결과 확인에 필요한 호출도 포함한다고 가정한다. 제품별 가격이나 처리량을 옮겨 적지 않고도, 한도를 넘는 요청을 어떻게 다룰지 배울 수 있다.
검토 시간도 별도로 입력한다. 호출 비용이 낮은 변경이라도 예외 처리와 동시성 가정이 많으면 사람이 오래 읽는다. 반대로 설명문을 다듬는 일은 호출 비용이 비슷해도 검토 시간이 짧을 수 있다. 두 값을 하나로 합치면 어느 자원이 부족한지 가려진다. 이 프로그램은 비용과 검토 시간 중 하나라도 한도를 넘으면 작업을 중단한다.
| 항목 | 연습용 한도 | 적용 방법 |
|---|---|---|
| 작업 하나의 예상 비용 | 60단위 | 60까지 허용하고 초과하면 중단한다. |
| 작업 하나의 검토 시간 | 30분 | 30분까지 허용하고 초과하면 중단한다. |
| 전체 예약 비용 | 100단위 | 자동 처리와 승인 대기의 비용을 함께 예약한다. |
| 전체 예약 검토 시간 | 60분 | 자동 처리와 승인 대기의 검토 시간을 함께 예약한다. |
사람 승인으로 분류된 작업도 자원을 예약한다. 승인 대기를 공짜로 취급하면 승인 요청을 무제한으로 쌓을 수 있기 때문이다. 다만 예약은 지출이나 실행 완료를 뜻하지 않는다. 사람이 거절하면 예약을 반환할 수 있다. 이 예제는 한 번의 심사만 수행하므로 반환과 재심사 기능은 구현하지 않는다.
추정값은 실제 사용량과 다르다. 실무의 실행기는 작업을 시작한 뒤에도 누적 사용량을 확인해야 한다. 남은 예산이 부족하면 추가 호출과 재시도를 멈추고 현재 결과를 보존한다. 예산을 넘긴 뒤에 보고서를 만드는 것으로는 경계를 지킨 것이 아니다. 특히 실패할 때마다 같은 요청을 다시 보내는 자동화에는 재시도 횟수와 누적 사용량의 상한이 함께 필요하다.
예상값을 모르면 작은 값을 임의로 넣지 않는다. 이 프로그램은 추정이 확인되었는지를 별도 값으로 받는다. 확인되지 않았거나 음수인 입력은 중단한다. 중단은 요구를 폐기한다는 뜻이 아니다. 범위를 줄이거나 근거 있는 추정값을 구한 뒤 다시 심사하라는 뜻이다.
병렬화는 변경 영역과 승인 경계 안에서 한다
병렬 작업은 여러 일을 동시에 진행하는 방식이다. 서로 다른 파일을 수정한다고 해서 서로 독립적이지는 않다. 재고 차감 메서드와 주문 취소 메서드는 파일이 달라도 같은 수량 규칙에 의존한다. 그래서 예제는 파일명 대신 변경 영역을 입력받는다. 같은 영역의 작업이 이미 예약되어 있으면 뒤의 작업을 사람 승인으로 돌린다.
영역 이름 하나로 모든 충돌을 찾을 수는 없다. 공통 모델이나 공통 설정의 변경은 여러 영역에 영향을 줄 수 있다. 여기서는 목록을 사람이 준비하고 영역을 보수적으로 지정했다고 가정한다. 실제 저장소에서는 변경 파일, 공유 인터페이스, 업무 규칙을 함께 확인해야 한다. 영역 판정이 잘못되면 결정표도 잘못된다.
영역이 겹친다는 이유로 승인하는 것은 “충돌이 있어도 동시에 실행하라”는 뜻이 아니다. 담당자가 두 작업을 합치거나 순서를 정한 뒤 실행하라는 뜻이다. 병렬화를 줄이면 생성 속도는 낮아질 수 있지만, 서로의 전제를 다시 읽는 시간을 줄일 수 있다. 검토자가 같은 사람이면 서로 다른 영역의 작업도 검토 단계에서는 줄을 서게 된다.
승인은 위험을 없애는 계산 결과가 아니라 권한을 가진 사람의 결정이다. 운영 배포나 데이터 삭제는 예상 비용과 무관하게 승인 대상으로 둔다. 프로그램의 해당 표시는 “승인을 받아야 한다”는 요구만 나타낸다. 승인이 도착했다는 증거가 아니며, 이 프로그램에는 실행 기능 자체가 없다.
| 결정 | 판정 의미 | 다음 행동 |
|---|---|---|
| 자동 처리 | 현재 정책의 자동화 조건을 만족한다. | 격리된 작업 공간에서 허용된 변경을 진행한다. |
| 사람 승인 | 예산은 예약했지만 사람의 판단이 필요하다. | 대상과 범위를 제시하고 실행을 보류한다. |
| 중단 | 입력이 불충분하거나 자원 한도를 넘는다. | 추정 보완, 작업 축소 또는 재계획을 한다. |
자동 처리도 운영 환경까지 자동으로 바꾼다는 뜻은 아니다. 여기서 허용하는 범위는 저장소의 격리된 변경 작업이다. 운영 자격 증명이나 데이터 삭제 권한을 작업 실행기에 미리 주면 결정표의 의도를 벗어나기 쉽다. 판단 프로그램과 실제 실행 권한을 함께 설계해야 승인 대기가 실제 대기로 작동한다.
승인 요청에는 실행 대상, 변경 범위, 예상 영향, 복구 수단을 적는다. 예를 들어 주문 데이터 삭제라면 대상 조건과 예상 건수, 보존해야 할 기록, 복원 가능성을 확인해야 한다. 대상을 확인할 수 없는 요청은 승인 대상으로 포장하지 말고 중단해야 한다. 완성 코드는 이러한 상세 운영 검증을 대신하지 않으며, 작업 목록이 준비된 이후의 자원·위험 심사만 다룬다.
완성 코드
다음 프로그램은 파일과 네트워크를 사용하지 않는다. 비용·검토 시간·위험·되돌리기 어려운 작업 여부를 읽어 결정표를 출력하고, 같은 정책을 새 객체로 검사한다. 위험 수준은 통계 점수가 아니라 이 연습의 분류값이다. 낮음은 독립된 작은 변경, 보통은 업무 동작에 영향을 주는 변경, 높음은 추가 판단이 필요한 변경을 뜻한다.
Main.java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
enum Risk {
LOW("낮음"), MEDIUM("보통"), HIGH("높음");
final String label;
Risk(String label) {
this.label = label;
}
}
enum Decision {
AUTO("자동 처리"), APPROVAL("사람 승인"), STOP("중단");
final String label;
Decision(String label) {
this.label = label;
}
}
record Task(String name, String area, int cost, int reviewMinutes,
Risk risk, boolean irreversible, boolean estimateKnown) {
}
record Result(Task task, Decision decision, String reason) {
}
static final int TASK_COST_LIMIT = 60;
static final int TASK_REVIEW_LIMIT = 30;
static final int TOTAL_COST_LIMIT = 100;
static final int TOTAL_REVIEW_LIMIT = 60;
static int failures;
static final class Planner {
private int reservedCost;
private int reservedReview;
private final Set<String> reservedAreas = new HashSet<>();
Result assess(Task task) {
if (!task.estimateKnown()
|| task.cost() < 0 || task.reviewMinutes() < 0) {
return new Result(task, Decision.STOP, "추정 확인 필요");
}
if (!withinTaskLimits(task)) {
return new Result(task, Decision.STOP, "작업별 한도 초과");
}
if ((long) reservedCost + task.cost() > TOTAL_COST_LIMIT
|| (long) reservedReview + task.reviewMinutes()
> TOTAL_REVIEW_LIMIT) {
return new Result(task, Decision.STOP, "전체 한도 초과");
}
Decision decision;
String reason;
if (task.irreversible()) {
decision = Decision.APPROVAL;
reason = "되돌리기 어려운 작업";
} else if (requiresRiskApproval(task)) {
decision = Decision.APPROVAL;
reason = "위험 높음";
} else if (reservedAreas.contains(task.area())) {
decision = Decision.APPROVAL;
reason = "영역 겹침: 순서 조정";
} else {
decision = Decision.AUTO;
reason = "자동화 조건 충족";
}
reservedCost += task.cost();
reservedReview += task.reviewMinutes();
reservedAreas.add(task.area());
return new Result(task, decision, reason);
}
int reservedCost() {
return reservedCost;
}
int reservedReview() {
return reservedReview;
}
}
// 비교 실습을 위한 잘못된 판단 함수다.
static boolean wrongCostOnly(Task task) {
return task.cost() <= TASK_COST_LIMIT;
}
static boolean withinTaskLimits(Task task) {
return task.cost() <= TASK_COST_LIMIT
&& task.reviewMinutes() <= TASK_REVIEW_LIMIT;
}
static boolean wrongAtLimit(int total) {
return total >= TOTAL_COST_LIMIT;
}
static boolean exceedsTotalCost(int total) {
return total > TOTAL_COST_LIMIT;
}
static boolean wrongRiskOnly(Task task) {
return task.risk() == Risk.HIGH;
}
static boolean requiresRiskApproval(Task task) {
return task.irreversible() || task.risk() == Risk.HIGH;
}
static void check(boolean condition, String name) {
if (condition) {
System.out.println("PASS " + name);
} else {
System.out.println("FAIL " + name);
failures++;
}
}
static void runChecks(List<Result> results, Planner planner) {
check(results.get(0).decision() == Decision.AUTO,
"독립 작업 자동 처리");
check(results.get(2).decision() == Decision.APPROVAL,
"같은 영역 승인 대기");
check(results.get(3).decision() == Decision.APPROVAL
&& results.get(4).decision() == Decision.APPROVAL,
"운영 배포와 삭제 승인");
check(planner.reservedCost() == 78
&& planner.reservedReview() == 45,
"중단 작업은 예약 제외");
Task slow = new Task("긴 검토", "별도", 10, 31,
Risk.LOW, false, true);
check(wrongCostOnly(slow) && !withinTaskLimits(slow),
"검토 시간 누락 검출");
check(wrongAtLimit(100) && !exceedsTotalCost(100),
"전체 비용 경계 비교");
Task deletion = new Task("삭제", "데이터", 1, 1,
Risk.LOW, true, true);
check(!wrongRiskOnly(deletion) && requiresRiskApproval(deletion),
"낮은 위험 표시여도 삭제 승인");
Planner boundary = new Planner();
Result first = boundary.assess(new Task("경계 A", "A", 60, 30,
Risk.LOW, false, true));
Result second = boundary.assess(new Task("경계 B", "B", 40, 30,
Risk.LOW, false, true));
check(first.decision() == Decision.AUTO
&& second.decision() == Decision.AUTO
&& boundary.reservedCost() == 100
&& boundary.reservedReview() == 60,
"작업별 한도와 전체 한도 일치 허용");
Planner reviewPlanner = new Planner();
reviewPlanner.assess(new Task("검토 A", "A", 1, 30,
Risk.LOW, false, true));
reviewPlanner.assess(new Task("검토 B", "B", 1, 30,
Risk.LOW, false, true));
Result overReview = reviewPlanner.assess(
new Task("검토 C", "C", 1, 1, Risk.LOW, false, true));
check(overReview.decision() == Decision.STOP
&& reviewPlanner.reservedCost() == 2,
"전체 검토 시간 초과 중단");
Planner other = new Planner();
Result negative = other.assess(new Task("음수", "A", -1, 1,
Risk.LOW, false, true));
Result unknown = other.assess(new Task("미확인", "B", 1, 1,
Risk.LOW, false, false));
Result high = other.assess(new Task("고위험", "C", 1, 1,
Risk.HIGH, false, true));
check(negative.decision() == Decision.STOP
&& unknown.decision() == Decision.STOP
&& high.decision() == Decision.APPROVAL,
"입력 확인과 위험 분기");
Task ordinary = new Task("일반", "독립", 20, 10,
Risk.LOW, false, true);
check(wrongCostOnly(ordinary) == withinTaskLimits(ordinary)
&& wrongAtLimit(99) == exceedsTotalCost(99)
&& wrongRiskOnly(ordinary)
== requiresRiskApproval(ordinary),
"기존 일반 입력의 결과 유지");
System.out.println("검사 실패: " + failures);
if (failures != 0) {
throw new IllegalStateException("검사 실패");
}
}
public static void main(String[] args) {
List<Task> tasks = List.of(
new Task("재고 조건 정리", "재고", 20, 10,
Risk.LOW, false, true),
new Task("결제 모의 응답", "결제", 25, 15,
Risk.MEDIUM, false, true),
new Task("취소 재고 복구", "재고", 15, 10,
Risk.MEDIUM, false, true),
new Task("운영 배포", "운영", 10, 5,
Risk.HIGH, true, true),
new Task("주문 데이터 삭제", "데이터", 8, 5,
Risk.HIGH, true, true),
new Task("범위 미정 수정", "미정", 5, 5,
Risk.MEDIUM, false, false),
new Task("주문 흐름 재작성", "주문", 70, 10,
Risk.HIGH, false, true),
new Task("추가 결제 정리", "결제", 25, 20,
Risk.LOW, false, true)
);
Planner planner = new Planner();
List<Result> results = new ArrayList<>();
System.out.println("결정표");
System.out.println("작업 | 비용 | 검토(분) | 위험 | 결정 | 이유");
for (Task task : tasks) {
Result result = planner.assess(task);
results.add(result);
System.out.println(task.name() + " | " + task.cost()
+ " | " + task.reviewMinutes() + " | " + task.risk().label
+ " | " + result.decision().label + " | " + result.reason());
}
System.out.println("예약 비용: " + planner.reservedCost() + "/100");
System.out.println("예약 검토: " + planner.reservedReview() + "/60분");
System.out.println("승인 대상은 실행하지 않음");
runChecks(results, planner);
}
}
줄별 해설
Risk와 Decision 선언은 입력의 위험 수준과 출력의 결정을 구분한다. 위험이 높다는 사실과 작업을 중단한다는 결정은 같지 않다. 높은 위험이라도 한도 안이면 사람 승인을 요청할 수 있고, 낮은 위험이라도 전체 예산이 부족하면 중단한다. 두 개념을 하나의 숫자로 합치지 않은 이유다.
Task 선언은 이름, 영역, 예상 비용, 검토 시간, 위험, 되돌리기 어려운 작업 여부, 추정 확인 여부를 묶는다. 레코드(record)는 생성한 뒤 구성 요소를 바꾸지 않으므로 심사 도중 입력이 달라지는 일을 줄여 준다. 다만 이 선언만으로 값이 유효해지는 것은 아니다. 외부 입력을 받는 프로그램이라면 이름과 영역의 누락, 위험 값의 누락도 검사해야 한다. 여기서는 코드 안에 준비한 입력만 사용한다.
Planner의 세 필드는 지금까지 예약한 비용, 검토 시간, 영역을 보관한다. 영역 집합은 포함 여부만 확인하며 순회해서 출력하지 않는다. 따라서 집합의 내부 순서가 출력 순서에 영향을 주지 않는다. 실제 심사는 한 스레드에서 순서대로 실행된다. 여러 스레드가 같은 객체를 동시에 호출할 수 있도록 만든 코드는 아니다.
assess의 첫 번째 조건은 추정이 미확인이거나 수치가 음수이면 즉시 중단한다. 다음 조건은 작업별 한도를 확인한다. 확인되지 않은 추정과 너무 큰 작업을 먼저 걸러야 이후에 승인 사유가 있다고 해서 실행 후보로 오해하지 않는다.
전체 한도 검사는 현재 예약량에 새 작업의 예상량을 더한다. 덧셈 전에 long으로 변환하므로 정수 덧셈의 범위도 넓어진다. 초과 비교에는 >를 쓴다. 한도와 같은 값은 허용한다는 정책을 코드로 옮긴 것이다. 비용과 검토 시간 가운데 어느 하나라도 초과하면 두 자원을 모두 예약하지 않는다.
승인 분기에서는 되돌리기 어려운 작업을 먼저 표시하고, 높은 위험, 영역 겹침 순으로 이유를 정한다. 한 작업에 여러 사유가 있어도 이 예제는 대표 이유 하나만 출력한다. 운영 배포의 위험이 높더라도 출력 이유가 “되돌리기 어려운 작업”인 것은 이 순서 때문이다. 한도 검사는 이미 끝났으므로 승인 대상이 한도를 우회하지 않는다.
예약 갱신은 중단 분기 아래에 있다. 자동 처리와 사람 승인만 비용·시간·영역을 예약한다. 마지막 추가 결제 정리는 영역도 겹치지만, 전체 한도 검사에서 먼저 중단되므로 이유가 “전체 한도 초과”다. 정책의 검사 순서가 설명문에도 영향을 준다는 점을 확인할 수 있다.
runChecks는 결정표의 핵심 행과 경계 입력을 확인한다. 일반 입력은 고치기 전 함수와 고친 함수가 같은 결과를 내는지 비교한다. 누락된 조건을 드러내는 입력에서는 결과가 달라야 검사에 통과한다. 오류 수정에서 모든 입력의 출력을 유지할 수는 없다. 유지해야 할 동작과 바뀌어야 할 잘못된 동작을 나누어 실행으로 확인한다.
main의 목록 순서는 심사 순서이자 출력 순서다. 난수나 현재 시각을 사용하지 않아 실행할 때마다 같은 결과가 나온다. 검사 실패가 있으면 FAIL을 출력한 뒤 예외를 던진다. PASS가 찍혔는지만 눈으로 찾는 대신 프로세스 실패 여부도 확인할 수 있는 구조다.
실행 결과
Main.java를 저장한 디렉터리에서 다음 명령을 순서대로 실행한다. 명령은 셸 입력이며 Java 소스에 넣지 않는다.
javac -d out *.java
java -cp out Main
예상 출력은 다음과 같다. 표의 세로 구분선은 출력 문자열의 일부다.
결정표
작업 | 비용 | 검토(분) | 위험 | 결정 | 이유
재고 조건 정리 | 20 | 10 | 낮음 | 자동 처리 | 자동화 조건 충족
결제 모의 응답 | 25 | 15 | 보통 | 자동 처리 | 자동화 조건 충족
취소 재고 복구 | 15 | 10 | 보통 | 사람 승인 | 영역 겹침: 순서 조정
운영 배포 | 10 | 5 | 높음 | 사람 승인 | 되돌리기 어려운 작업
주문 데이터 삭제 | 8 | 5 | 높음 | 사람 승인 | 되돌리기 어려운 작업
범위 미정 수정 | 5 | 5 | 보통 | 중단 | 추정 확인 필요
주문 흐름 재작성 | 70 | 10 | 높음 | 중단 | 작업별 한도 초과
추가 결제 정리 | 25 | 20 | 낮음 | 중단 | 전체 한도 초과
예약 비용: 78/100
예약 검토: 45/60분
승인 대상은 실행하지 않음
PASS 독립 작업 자동 처리
PASS 같은 영역 승인 대기
PASS 운영 배포와 삭제 승인
PASS 중단 작업은 예약 제외
PASS 검토 시간 누락 검출
PASS 전체 비용 경계 비교
PASS 낮은 위험 표시여도 삭제 승인
PASS 작업별 한도와 전체 한도 일치 허용
PASS 전체 검토 시간 초과 중단
PASS 입력 확인과 위험 분기
PASS 기존 일반 입력의 결과 유지
검사 실패: 0
예약 비용 78은 앞의 다섯 작업 비용을 더한 값이다. 검토 시간도 같은 다섯 작업의 합인 45분이다. 승인 대기 작업까지 예약하므로 “실제로 자동 실행 가능한 작업의 비용”과는 다르다. 마지막 작업을 추가하면 비용은 103단위, 검토 시간은 65분이 된다. 두 한도를 모두 넘지만 프로그램은 대표 이유를 한 줄만 출력한다.
이 출력은 AI 작업의 실제 완료 시간을 예측하지 않는다. 동시에 실행할 작업 수를 계산하지도 않는다. 팀이 승인 요청을 검토하기 전에, 현재 계획이 자원 한도 안에 있는지 확인하는 자료다. 실제 실행을 붙일 때는 승인 상태와 실행 권한을 별도로 관리해야 한다.
실무에서 자주 틀리는 것
호출 비용만 확인하고 검토 시간을 빠뜨린다
다음 두 함수는 완성 코드에 들어 있으며 실제 검사에서 호출된다. 앞 함수는 잘못된 판단을 재현한다. 뒤 함수는 작업별 자원 검사에 사용한다.
static boolean wrongCostOnly(Task task) {
return task.cost() <= TASK_COST_LIMIT;
}
static boolean withinTaskLimits(Task task) {
return task.cost() <= TASK_COST_LIMIT
&& task.reviewMinutes() <= TASK_REVIEW_LIMIT;
}
예상 비용 10단위, 검토 31분을 넣으면 앞 함수는 true, 뒤 함수는 false다. 검토 시간 상한을 넘는 작업을 거절하는 것이 수정 목적이다. 비용 20단위, 검토 10분인 일반 입력에서는 두 함수가 모두 true다. 완성 코드의 마지막 검사에서 이 기존 입력의 결과가 유지되는지도 함께 확인한다.
리뷰 요청이 쌓이면 검토 시간을 줄여 적어서 통과시키고 싶어질 수 있다. 이때 바꿔야 할 것은 숫자가 아니라 작업 범위나 담당자 배정이다. 추정값을 작게 쓰면 결정표는 깔끔해져도 대기 시간은 줄지 않는다.
한도와 같은 값까지 초과로 처리한다
한도를 포함하는지 팀이 정하지 않으면 비교 연산자가 작성자마다 달라진다. 이 장의 정책에서는 100단위까지 허용한다. 따라서 앞 함수의 비교는 정책과 맞지 않는다.
static boolean wrongAtLimit(int total) {
return total >= TOTAL_COST_LIMIT;
}
static boolean exceedsTotalCost(int total) {
return total > TOTAL_COST_LIMIT;
}
같은 입력 100에 대해 앞 함수는 초과라고 판단하고 뒤 함수는 초과가 아니라고 판단한다. 입력 99에서는 두 함수 모두 false다. 완성 코드는 함수 비교와 별도로 작업 두 개를 실제 Planner에 넣는다. 작업별 한도 안에서 전체 비용 100단위와 검토 60분을 채우면 둘 다 자동 처리되는지 검사한다. 함수 하나의 비교 결과뿐 아니라 예약 동작까지 확인하는 이유다.
이 장의 한도는 예제 상수다. 실무에서 상수를 바꿀 때는 경계 검사도 정책에 맞게 검토해야 한다. 숫자만 올려서 중단을 없애는 변경에는 실제 자원 확보라는 근거가 필요하다.
위험 표지만 보고 삭제 승인을 생략한다
입력의 위험 분류는 틀릴 수 있다. 데이터 삭제 작업에 낮음이 붙었다고 해서 자동으로 진행하면 안 된다. 되돌리기 어려운 동작이라는 속성은 별도로 확인해야 한다.
static boolean wrongRiskOnly(Task task) {
return task.risk() == Risk.HIGH;
}
static boolean requiresRiskApproval(Task task) {
return task.irreversible() || task.risk() == Risk.HIGH;
}
위험이 낮음이고 삭제 속성이 true인 같은 입력에서 앞 함수는 false, 뒤 함수는 true다. 일반 코드 정리 입력에서는 두 함수가 모두 false이므로 기존 동작을 유지한다. 완성 코드의 검사는 이 두 경우를 모두 실행한다.
물론 삭제 속성을 작성자가 빠뜨릴 수도 있다. 실제 실행기에서는 작업 설명만 믿지 말고 요청된 명령과 접근 대상도 검사해야 한다. 승인 표가 출력되었다는 사실을 승인 완료로 해석하는 것도 같은 종류의 오류다. 승인 완료에는 누가 어떤 대상과 범위를 허용했는지 확인할 수 있는 기록이 필요하다. 승인 뒤 대상이 달라지면 다시 판단해야 한다.
한눈에 보기
| 확인 대상 | 계속할 조건 | 멈추거나 넘길 조건 | 예제의 처리 |
|---|---|---|---|
| 추정 근거 | 예상이 확인되고 음수가 아니다. | 범위가 불명확하거나 값이 잘못되었다. | 중단 |
| 자원 | 작업별·전체 한도 안이다. | 비용이나 검토 시간 중 하나가 초과한다. | 중단 |
| 변경 영역 | 예약한 영역과 겹치지 않는다. | 같은 업무 규칙을 함께 수정한다. | 사람 승인 후 순서 조정 |
| 실행 영향 | 허용된 저장소 변경 범위다. | 위험이 높거나 되돌리기 어렵다. | 사람 승인 전 실행 보류 |
자동화 범위는 코드 생성 능력만으로 정하지 않는다. 사람이 읽을 수 있는 양, 변경끼리의 관계, 실행 권한을 함께 본다. 다음 장의 종합 실습에서도 작업을 시작하기 전에 이 결정표를 적용할 수 있다. 중단된 작업을 더 작은 변경으로 나눌지, 승인 자료를 보완할지 정한 뒤 진행한다.
연습 문제
- 추가 결제 정리 작업의 예상 비용을 20단위, 검토 시간을 10분으로 바꾼다. 결정, 이유, 최종 예약 비용과 검토 시간을 예상한 뒤 실행 결과와 비교한다.
- 검토 시간이 31분인 작업을 새 Planner에 넣는 검사를 추가한다. 결정이 중단이고 예약 비용과 검토 시간이 모두 0인지 확인한다. 기존 함수 비교 검사만으로 이 동작을 보장할 수 없는 이유도 설명한다.
- 서로 다른 영역의 작업 세 개를 비용 10단위, 검토 25분씩 준비한다. 모두 위험은 낮음이며 추정이 확인되었다. 입력 순서대로 심사하면 몇 개가 자동 처리되는지, 남는 검토 시간은 얼마인지 구한다.
- 사람이 삭제 작업을 승인한 뒤 삭제 대상 조건이 바뀌었다. 기존 승인을 그대로 사용해도 되는지 설명하고, 실제 실행기에 필요한 승인 기록 항목을 네 가지 이상 적는다.
정답과 해설
- 사람 승인이고 이유는 “영역 겹침: 순서 조정”이다. 기존 예약 78단위와 45분에 각각 20단위와 10분을 더하면 98단위와 55분이다. 전체 한도는 통과하지만 결제 영역이 이미 예약되어 있다. 기존 검사 중 예약 합계를 확인하는 조건도 새 예상값에 맞게 수정해야 한다.
- 작업별 검토 한도 30분을 넘으므로 중단이며 두 예약량은 0이다. withinTaskLimits의 반환값을 비교하는 검사는 조건 함수만 확인한다. Planner가 그 함수를 호출하는지, 중단한 뒤에도 예약을 잘못 갱신하지 않는지까지는 확인하지 못한다. 별도 Planner로 결과와 상태를 함께 검사해야 한다.
- 앞의 두 개가 자동 처리된다. 예약 비용은 20단위, 예약 검토 시간은 50분이다. 세 번째를 추가하면 검토 시간이 75분이 되어 전체 한도를 넘는다. 남는 검토 시간은 10분이다. 비용 여유가 있어도 검토 자원이 부족하면 시작하지 않는다.
- 대상 조건이 바뀌었으므로 다시 승인받아야 한다. 기록에는 승인자, 승인한 작업 내용, 실행 대상 환경, 삭제 대상 조건, 승인 시점, 복구 수단 등을 포함할 수 있다. 실행 직전에 현재 요청이 승인 범위와 같은지도 확인해야 한다. 표에 “사람 승인”이라고 적힌 것만으로 실행 권한이 생기지는 않는다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.