AI 개발 · 심화
AI 코딩 도구로 팀 개발하기 - Java 실무 저장소편
작업을 PR 하나 크기로 자르기
큰 요청을 리뷰 가능한 단위로 쪼개기, 순서와 의존, 각 조각의 완료 기준, 한 번에 시키면 생기는 섞인 diff, 완성 코드는 작업 목록과 변경 파일 수를 받아 PR 크기 기준을 넘는 작업을 찾아 나누는 계획을 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 주문 서비스 저장소의 입구와 주요 파일을 찾았다. 이제 팀은 그 저장소에 변경을 넣어야 한다. “주문 취소 기능을 만들어 달라”는 요청은 한 문장이지만, 실제 변경에는 주문 상태, 재고 복구, 결제 취소 모의 객체가 함께 등장한다. 요청이 짧다고 작업도 작은 것은 아니다.
이 장에서는 큰 요청을 풀 리퀘스트(Pull Request, 이하 PR) 하나로 검토할 수 있는 작업으로 나눈다. 기준은 코드 줄 수만이 아니다. 검토자가 어떤 행동의 변화를 확인해야 하는지, 먼저 들어와야 하는 변경이 무엇인지, 어디까지 하면 끝인지가 함께 드러나야 한다. 마지막에는 작업 목록과 예상 변경 파일 수를 받아 분할 계획을 출력하는 Java 프로그램을 만든다.
- 하나의 요청에 섞인 서로 다른 변경 이유를 구분한다.
- 작업 사이의 선행 관계를 정하고 병합 가능한 순서로 배치한다.
- 각 조각에 관찰 가능한 완료 기준을 붙인다.
- 변경 파일 수를 검토 신호로 사용하되, 숫자만으로 작업 경계를 결정하지 않는다.
- 작은 검사 함수로 계획의 크기 제한과 의존 관계를 확인한다.
문제 상황
작은 쇼핑몰의 주문 서비스에는 주문을 생성하는 코드, 수량을 차감하는 재고 객체, 결제 성공과 실패를 흉내 내는 모의 객체가 있다. 팀은 고객 문의를 받아 취소 기능을 추가하려 한다. 개발자는 AI에게 다음과 같이 요청한다.
주문 취소 기능을 추가해라. 이미 취소한 주문은 다시 취소하지 못하게 하고, 재고를 돌려놓고, 결제 취소 모의 객체도 연결해라. 관련 코드가 지저분하면 함께 정리해라.
잠시 뒤 변경이 도착한다. 주문 상태가 늘었고, 재고 메서드의 이름이 바뀌었으며, 결제 결과를 담는 클래스도 교체되어 있다. 생성자 매개변수 순서까지 달라졌다. 취소 요청을 처리하는 데 필요한 변경과 코드 정리 목적의 변경이 같은 변경 내역(diff)에 들어 있다.
검토자는 재고 복구가 맞는지 보다가 생성자 변경을 따라가고, 다시 결제 실패 분기로 돌아온다. 코드 한 줄이 어려워서가 아니다. 서로 다른 질문에 답해야 하는 변경들이 한꺼번에 나타나기 때문이다. 주문 취소가 잘못된 것인지, 이름 변경 과정에서 호출이 빠진 것인지도 분리하기 어렵다.
| 변경 묶음 | 검토할 질문 | 이번 요청과의 관계 |
|---|---|---|
| 주문 취소 상태와 전이 | 어떤 주문을 취소할 수 있는가 | 직접 필요한 행동 변경 |
| 재고 복구 연결 | 취소 시 수량이 한 번만 돌아오는가 | 직접 필요한 협력 객체 변경 |
| 결제 취소 모의 객체 연결 | 실패 결과를 호출자에게 어떻게 전달하는가 | 정책 결정이 필요한 연결 변경 |
| 이름과 생성자 일괄 정리 | 기존 호출의 의미가 유지되는가 | 별도 이유와 검증이 필요한 정리 |
여기서 할 일은 AI에게 “더 작게 해라”라고만 말하는 것이 아니다. 작은 작업의 경계를 사람이 먼저 정해야 한다. 이 예제에서는 결제 실패 시 주문과 재고를 어떻게 처리할지 요구가 아직 정해지지 않았다. 이 상태에서 세 객체를 모두 연결하면 AI가 빈 정책을 임의로 채울 수 있다. 팀은 그 결정을 작업 설명에 남긴 다음 연결 작업을 시작해야 한다.
이 장의 프로그램은 실제 주문 서비스를 구현하지 않는다. 같은 저장소에서 진행할 작업을 계획하는 보조 도구다. 프로그램이 내놓는 것은 파일 수정이나 자동 병합 결과가 아니라, 검토할 PR 목록과 그 순서다.
PR 하나에는 설명할 수 있는 변화 하나를 담는다
작업을 나눌 때 먼저 물을 것은 “파일이 몇 개인가”보다 “이 PR을 받아들이면 무엇이 가능해지는가”다. “주문 관련 파일 수정”은 위치를 말할 뿐이다. “생성된 주문을 취소 상태로 바꾸고 두 번째 취소를 거부한다”는 문장은 행동과 경계를 함께 말한다.
변경 이유가 같고 함께 검증해야 하는 파일들은 하나의 조각에 둘 수 있다. 반대로 같은 파일을 수정하더라도 변경 이유가 다르면 분리 후보가 된다. 주문 서비스 클래스에서 취소 조건을 추가하는 일과 변수 이름을 정리하는 일은 같은 파일에서 일어나지만, 검토 질문은 다르다.
파일마다 PR을 하나씩 만드는 방법도 적절하지 않다. 인터페이스의 메서드를 바꾸고 구현체와 호출부를 다른 PR로 미루면, 먼저 병합한 저장소가 컴파일되지 않을 수 있다. 함께 바뀌어야만 의미가 성립하는 파일들은 묶어야 한다. 작은 PR은 파일을 적게 담는 것과 함께, 병합한 상태가 설명 가능해야 한다는 조건을 갖는다.
팀이 이 작업에서 사용할 파일 수 기준을 4개로 정했다고 하자. 이 값은 업계 공통 기준이나 생산성 점수가 아니다. 예제를 위한 팀의 임시 기준이다. 파일 5개를 바꾼다고 잘못된 PR이 되는 것도 아니다. 기준을 넘으면 “더 나눌 수 있는가”를 확인하도록 만드는 신호다.
파일 수가 적어도 검토가 어려울 수 있다. 하나의 긴 메서드에 취소 가능 조건, 재고 복구, 결제 실패 처리를 모두 넣으면 파일 수는 1개다. 반대로 같은 형식의 작은 자료 클래스를 여러 개 추가하는 변경은 파일 수가 많아도 질문은 단순할 수 있다. 그러므로 파일 수는 의미를 대신하는 판정기가 아니라, 사람이 살펴볼 지점을 찾는 보조 값으로 사용한다.
이번 프로그램에서는 사람이 먼저 의미 있는 조각을 입력한다. 각 조각에는 이름, 예상 변경 파일 수, 완료 기준을 붙인다. 프로그램은 같은 작업의 조각들을 입력 순서대로 묶되, 파일 수 합계가 기준을 넘기 전에 새 PR을 연다. 조각 하나가 기준보다 크면 억지로 숫자를 잘라 넣지 않고 입력을 거부한다. 그때는 사람이 행동의 경계를 다시 찾아야 한다.
순서와 완료 기준을 작업의 일부로 적는다
작업 이름만 나열하면 목록은 생기지만 계획은 생기지 않는다. 재고 복구 연결은 주문 취소 상태가 준비된 뒤에 진행해야 하는지, 결제 모의 객체는 별도로 준비할 수 있는지 적어야 한다. 선행 관계를 빼면 여러 사람이 동시에 착수한 뒤 서로의 미완성 코드를 기다리는 상황이 생긴다.
여기서는 설명을 단순하게 유지하기 위해 작업마다 선행 작업을 최대 하나만 지정한다. 작업 목록은 선행 작업이 앞에 오는 순서로 입력한다. 프로그램은 잘못된 순서를 자동으로 고치지 않고 오류로 알려 준다. 실제 업무에서는 여러 선행 작업이 있을 수 있지만, 그 구조까지 이번 예제에 넣지는 않는다.
| 작업 | 조각과 예상 파일 수 | 완료 기준 | 선행 작업 |
|---|---|---|---|
| 주문 취소 기반 | 상태 규칙 2, 서비스 연결 3, 호출 진입점 2 | 취소 상태, 중복 거부, 거부 이유 전달을 각각 확인한다 | 없음 |
| 재고 복구 연결 | 재고 복구 3 | 취소 한 번에 재고가 한 번 복구됨을 확인한다 | 주문 취소 기반 |
| 결제 모의 객체 준비 | 취소 결과 추가 2 | 성공과 실패 결과를 고정 입력으로 확인한다 | 없음 |
표의 결제 작업은 주문 서비스에 결제를 연결하는 작업이 아니다. 모의 객체에 취소 결과를 준비하는 범위만 담았다. 결제 실패 시 처리 정책이 정해지지 않았기 때문이다. 준비 작업의 PR 설명에 “주문 서비스 연결은 포함하지 않는다”라고 적으면, 작게 나눈 이유와 남은 범위가 함께 드러난다.
완료 기준은 “구현 완료”, “정상 동작”처럼 작성자의 판단만 남기는 표현을 피한다. 입력이나 조건, 관찰할 결과를 적는다. 예를 들어 “이미 취소한 주문을 다시 취소하면 거부 이유가 반환된다”는 문장은 검토자가 확인할 대상을 가리킨다. 구체적인 테스트를 어떻게 설계할지는 다음 장에서 다룬다. 여기서는 어떤 증거가 필요한지 이름 붙이는 데 집중한다.
한 작업이 여러 PR로 나뉘면 뒤 조각은 앞 조각을 선행 PR로 가진다. 다른 작업이 그 작업에 의존하면 마지막 PR을 기다린다. 첫 PR만 병합되었다고 전체 작업이 준비된 것은 아니기 때문이다. 이 예제는 안전하게 순서를 보존하는 계획이며, 각 조각이 서로 독립적인지까지 분석해 병렬 실행을 찾아내지는 않는다.
선행 PR을 기준으로 개발하더라도 각 PR은 자기 기준 브랜치에서 컴파일되고 검토 가능해야 한다. 특히 호출 진입점보다 먼저 병합되는 상태 규칙과 서비스 연결은 아직 외부에서 사용하지 않는 기반 코드로 성립해야 한다. 이 조건이 맞지 않으면 계획표의 파일 수가 작아도 분할 경계가 잘못된 것이다.
AI에게는 전체 요청을 버리는 대신, 이번 조각과 보류 범위를 구분해 전달한다. 예를 들면 다음과 같다.
이번 PR은 주문 취소 상태 규칙만 다룬다. 생성 상태에서 취소 상태로 바뀌며, 이미 취소된 상태에서는 거부 이유를 반환해야 한다. 재고와 결제 연결, 기존 이름 정리는 보류한다. 선행 PR은 없다. 예상 변경 파일은 2개이며, 인터페이스 변경으로 호출부 수정이 더 필요하면 변경 전에 계획을 다시 제시해라.
이 요청은 파일 수만 제한하지 않는다. 무엇을 증명해야 하는지와 어디서 멈춰야 하는지를 함께 준다. AI가 예상보다 넓은 수정을 제안하면 계획을 수정할 근거가 생긴다.
완성 코드
다음 프로그램은 고정된 작업 목록을 입력으로 받는다. 각 작업의 전체 예상 파일 수와 조각별 파일 수 합계가 같은지 확인하고, 기준을 넘는 작업을 찾아 PR 계획을 출력한다. 외부 입력 없이 실행을 마치므로 매번 같은 결과가 나온다. 파일은 생성하지 않는다.
여기서 파일 수는 조각별 예상 변경 파일 수를 더한 값이다. 여러 조각이 같은 파일을 고치더라도 중복을 제거하지 않는다. 따라서 실제 PR의 고유 파일 수보다 크게 계산될 수 있다. 이 보수적인 추정과 실제 변경 파일 수의 차이는 구현 후 다시 확인해야 한다.
Main.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
private static int failures;
record Slice(String name, int files, String done) {
}
record Task(String id, String name, int files,
String after, List<Slice> slices) {
Task {
slices = List.copyOf(slices);
}
}
record Pr(String id, String taskId, String after,
List<Slice> slices) {
Pr {
slices = List.copyOf(slices);
}
int files() {
return slices.stream().mapToInt(Slice::files).sum();
}
}
static List<Pr> plan(List<Task> tasks, int limit) {
if (limit <= 0) {
throw new IllegalArgumentException("基準は正の数にする");
}
List<Pr> result = new ArrayList<>();
Map<String, String> lastPr = new HashMap<>();
for (Task task : tasks) {
validate(task, limit, lastPr);
String previous = task.after().isEmpty()
? "なし" : lastPr.get(task.after());
List<Slice> bucket = new ArrayList<>();
int count = 0;
for (Slice slice : task.slices()) {
if (!bucket.isEmpty() && slice.files() > limit - count) {
String id = "PR-" + (result.size() + 1);
result.add(new Pr(id, task.id(), previous, bucket));
previous = id;
bucket = new ArrayList<>();
count = 0;
}
bucket.add(slice);
count += slice.files();
}
String id = "PR-" + (result.size() + 1);
result.add(new Pr(id, task.id(), previous, bucket));
lastPr.put(task.id(), id);
}
return List.copyOf(result);
}
static void validate(Task task, int limit,
Map<String, String> lastPr) {
if (task.id().isBlank() || task.name().isBlank()
|| lastPr.containsKey(task.id())) {
throw new IllegalArgumentException("작업 식별자와 이름을 확인한다");
}
if (!task.after().isEmpty() && !lastPr.containsKey(task.after())) {
throw new IllegalArgumentException("선행 작업을 먼저 입력한다");
}
if (task.files() <= 0 || task.slices().isEmpty()) {
throw new IllegalArgumentException("작업에는 양수 파일 수와 조각이 필요하다");
}
long sum = 0;
for (Slice slice : task.slices()) {
if (slice.name().isBlank() || slice.done().isBlank()
|| slice.files() <= 0) {
throw new IllegalArgumentException("조각의 이름, 파일 수, 완료 기준을 확인한다");
}
if (slice.files() > limit) {
throw new IllegalArgumentException("큰 조각을 의미에 따라 다시 나눈다");
}
sum += slice.files();
}
if (sum != task.files()) {
throw new IllegalArgumentException("작업과 조각의 파일 수 합계가 다르다");
}
}
static List<Task> sample() {
return List.of(
new Task("order", "주문 취소 기반", 7, "", List.of(
new Slice("취소 상태 규칙", 2,
"생성 주문이 취소 상태로 바뀐다"),
new Slice("취소 서비스 연결", 3,
"이미 취소한 주문은 다시 취소하지 못한다"),
new Slice("취소 호출 진입점", 2,
"거부 이유가 호출자에게 전달된다"))),
new Task("stock", "재고 복구 연결", 3, "order", List.of(
new Slice("재고 복구", 3,
"취소 한 번에 재고가 한 번 복구된다"))),
new Task("payment", "결제 모의 객체 준비", 2, "", List.of(
new Slice("결제 취소 결과", 2,
"성공과 실패 결과를 고정 입력으로 확인한다")))
);
}
static void printPlan(List<Task> tasks, int limit, List<Pr> prs) {
System.out.println("PR 파일 수 기준: " + limit);
for (Task task : tasks) {
String decision = task.files() > limit ? "분할" : "유지";
System.out.println("작업 " + task.id() + ": " + task.name()
+ " / " + task.files() + "개 / " + decision);
}
System.out.println("계획");
for (Pr pr : prs) {
System.out.println(pr.id() + " | " + pr.taskId()
+ " | 파일 " + pr.files() + "개 | 선행 " + pr.after());
for (Slice slice : pr.slices()) {
System.out.println(" - " + slice.name() + " / 완료: " + slice.done());
}
}
}
static boolean rejected(List<Task> tasks, int limit) {
try {
plan(tasks, limit);
return false;
} catch (IllegalArgumentException expected) {
return true;
}
}
static void check(boolean condition, String name) {
System.out.println((condition ? "PASS " : "FAIL ") + name);
if (!condition) {
failures++;
}
}
public static void main(String[] args) {
int limit = 4;
List<Task> tasks = sample();
List<Pr> prs = plan(tasks, limit);
printPlan(tasks, limit, prs);
check(prs.size() == 5, "예제는 PR 다섯 개");
check(prs.stream().allMatch(pr -> pr.files() <= limit),
"모든 PR이 파일 수 기준 이하");
check(prs.get(1).after().equals("PR-1")
&& prs.get(2).after().equals("PR-2"),
"분할 조각의 순서 유지");
check(prs.get(3).after().equals("PR-3"),
"후속 작업은 마지막 조각에 의존");
check(prs.get(4).after().equals("なし"),
"독립 작업에는 선행 PR 없음");
Task exact = new Task("exact", "경계", 4, "", List.of(
new Slice("첫 조각", 2, "첫 결과 확인"),
new Slice("둘째 조각", 2, "둘째 결과 확인")));
check(plan(List.of(exact), limit).size() == 1,
"기준과 같은 합계는 한 PR");
Task large = new Task("large", "큰 조각", 5, "", List.of(
new Slice("아직 나누지 않은 조각", 5, "결과 확인")));
check(rejected(List.of(large), limit), "큰 조각은 재분할 요청");
Task mismatch = new Task("bad", "합계 오류", 3, "", List.of(
new Slice("조각", 2, "결과 확인")));
check(rejected(List.of(mismatch), limit), "파일 수 합계 오류 거부");
check(rejected(List.of(tasks.get(1), tasks.get(0)), limit),
"선행 작업보다 앞선 입력 거부");
check(rejected(List.of(tasks.get(0), tasks.get(0)), limit),
"중복 작업 식별자 거부");
check(rejected(tasks, 0), "잘못된 기준 거부");
if (failures > 0) {
throw new IllegalStateException("실패한 검사: " + failures);
}
}
}
줄별 해설
Slice는 더 이상 자동으로 쪼개지 않을 의미 단위다. 이름은 변경 이유를, files는 예상 크기를, done은 완료 증거를 나타낸다. 프로그램은 완료 기준이 빈 문자열인지는 검사하지만, 내용이 충분히 구체적인지까지 판단하지는 못한다. “확인 완료”라는 문장도 통과하므로 사람이 읽어야 한다.
Task는 원래 요청에서 뽑은 작업이다. id는 의존 관계를 연결하는 식별자이며, 표시용 이름과 분리한다. 이름을 고쳐도 식별자가 같으면 연결은 유지된다. after가 빈 문자열이면 선행 작업이 없다. 이 예제는 작업 목록을 작성하는 사람이 필수 값을 제공한다고 가정하며, 모든 외부 입력 형식을 다루는 범용 입력기는 만들지 않는다.
두 레코드의 생성자에 있는 List.copyOf는 전달받은 목록의 변경이 계획에 흘러들지 않도록 복사한다. 특히 Pr가 현재 묶음 목록을 그대로 보관하면, 이후 묶음을 수정하는 코드와 계획이 서로 영향을 줄 수 있다. 조각 목록을 복사해 두면 한 번 만든 PR의 내용이 고정된다.
plan은 먼저 기준이 양수인지 검사한다. 그다음 결과 목록과 작업별 마지막 PR을 담을 맵을 만든다. 결과의 순서는 목록에 저장하고, 맵은 식별자로 마지막 PR을 찾는 용도로만 사용한다. 맵을 순회해 출력하지 않으므로 출력 순서는 입력 목록과 처리 순서로 결정된다.
validate를 통과한 작업은 선행 작업의 마지막 PR을 찾을 수 있다. 같은 작업을 처리하는 동안에는 previous가 직전에 만든 PR로 바뀐다. 이 두 동작을 합치면 작업 사이의 의존과 작업 내부의 순서를 같은 출력 형식으로 표현할 수 있다.
조각을 담기 전의 조건문이 분할 경계다. 현재 묶음이 비어 있지 않고, 새 조각의 파일 수가 남은 용량보다 클 때만 묶음을 닫는다. 합계가 기준과 같은 경우는 그대로 담는다. slice.files() > limit - count라는 비교는 양수 두 개를 먼저 더하지 않아도 초과 여부를 판단하게 한다.
반복문이 끝난 뒤 마지막 묶음도 결과에 추가한다. 작업에 조각이 하나 이상 있다는 검사를 이미 마쳤으므로 여기서 빈 PR은 생기지 않는다. 마지막으로 lastPr에 저장하는 값은 첫 PR이 아니라 방금 만든 마지막 PR이다. 후속 작업은 이 값을 사용한다.
검증 과정의 합계는 long으로 계산한다. 각 파일 수는 int지만, 잘못된 대량 입력을 더하는 과정에서 합계가 작은 음수로 바뀌는 일을 피하려는 선택이다. 반면 PR 내부의 합계는 양수인 int 기준 이하로 묶였으므로 int 범위 안에 있다.
sample이 실제 입력 지점이다. 작업을 바꾸려면 이 목록과 조각을 바꾼다. 주문 작업의 조각은 2, 3, 2개다. 기준이 4개일 때 인접한 조각 둘을 합칠 수 없으므로 세 PR이 된다. 뒤의 2개짜리 조각을 앞으로 옮겨 첫 조각과 합치지는 않는다. 입력한 구현 순서를 지키는 편이 이 프로그램의 목적에 맞기 때문이다.
rejected는 잘못된 계획이 거부되는지 확인하는 보조 함수다. 예상한 입력 오류만 잡으며, 다른 종류의 예외를 성공으로 바꾸지 않는다. check는 검사 결과를 출력하고 실패 수를 기록한다. 마지막에 실패가 있으면 예외를 던지므로, 출력에 실패가 나타났는데도 정상 종료한 것처럼 끝나지 않는다.
실행 결과
Main.java를 저장한 디렉터리에서 다음 명령을 실행한다. JDK 17 표준 라이브러리만 사용하며 패키지 선언은 없다.
javac -d out *.java
java -cp out Main
컴파일이 성공하면 별도 메시지 없이 실행 단계로 넘어간다. 예상 출력은 다음과 같다. 선행 PR이 없는 경우에는 코드에 지정한 표기인 なし가 출력된다.
PR 파일 수 기준: 4
작업 order: 주문 취소 기반 / 7개 / 분할
작업 stock: 재고 복구 연결 / 3개 / 유지
작업 payment: 결제 모의 객체 준비 / 2개 / 유지
계획
PR-1 | order | 파일 2개 | 선행 なし
- 취소 상태 규칙 / 완료: 생성 주문이 취소 상태로 바뀐다
PR-2 | order | 파일 3개 | 선행 PR-1
- 취소 서비스 연결 / 완료: 이미 취소한 주문은 다시 취소하지 못한다
PR-3 | order | 파일 2개 | 선행 PR-2
- 취소 호출 진입점 / 완료: 거부 이유가 호출자에게 전달된다
PR-4 | stock | 파일 3개 | 선행 PR-3
- 재고 복구 / 완료: 취소 한 번에 재고가 한 번 복구된다
PR-5 | payment | 파일 2개 | 선행 なし
- 결제 취소 결과 / 완료: 성공과 실패 결과를 고정 입력으로 확인한다
PASS 예제는 PR 다섯 개
PASS 모든 PR이 파일 수 기준 이하
PASS 분할 조각의 순서 유지
PASS 후속 작업은 마지막 조각에 의존
PASS 독립 작업에는 선행 PR 없음
PASS 기준과 같은 합계는 한 PR
PASS 큰 조각은 재분할 요청
PASS 파일 수 합계 오류 거부
PASS 선행 작업보다 앞선 입력 거부
PASS 중복 작업 식별자 거부
PASS 잘못된 기준 거부
PR-5가 마지막에 출력되었다고 PR-4의 병합을 기다려야 하는 것은 아니다. 출력 위치와 선행 관계는 다른 정보다. 이 계획에서 결제 모의 객체 준비는 독립 작업이다. 다만 동시에 수정해도 충돌하지 않는지, 팀에 검토 여력이 있는지는 이 프로그램의 출력만으로 알 수 없다.
이 결과가 보여 주는 범위도 구분해야 한다. 검사들은 계획의 크기와 연결을 확인한다. 주문 취소가 실제로 동작하거나 재고가 올바르게 복구된다는 증거는 아니다. 계획에 적힌 완료 기준은 이후 구현 PR에서 별도로 확인해야 한다.
실무에서 자주 틀리는 것
합계가 기준과 같은데도 나눈다
초과 조건을 이상 조건으로 작성하면 기준을 채운 묶음까지 분할된다. 다음은 틀린 비교와 고친 비교를 같은 입력으로 실행하는 독립 예제다. 각 예제는 별도 디렉터리의 Main.java로 실행한다.
public class Main {
public static void main(String[] args) {
int used = 2;
int next = 2;
int limit = 4;
boolean before = used + next >= limit;
boolean after = next > limit - used;
System.out.println("수정 전 분할: " + before);
System.out.println("수정 후 분할: " + after);
System.out.println("같은 판단: " + (before == after));
}
}
같은 입력의 출력은 수정 전 분할: true, 수정 후 분할: false, 같은 판단: false다. 이 수정은 동작을 보존하는 정리가 아니라 잘못된 경계 판단을 바꾸는 수정이다. 기준이 “4개 이하”라면 수정 후 판단이 요구와 일치한다. 파일 수가 작은 입력에서는 두 비교가 같아 보일 수 있으므로 경계 입력이 필요하다.
후속 작업을 첫 PR에 연결한다
원래 작업이 세 PR로 나뉘었는데 첫 PR만 선행으로 기록하면, 기반 작업이 덜 끝난 상태에서 후속 작업을 시작하게 된다. 다음 예제의 앞 표현은 첫 원소를, 뒤 표현은 마지막 원소를 사용한다.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> pieces = List.of("PR-1", "PR-2", "PR-3");
String before = pieces.get(0);
String after = pieces.get(pieces.size() - 1);
System.out.println("수정 전 선행: " + before);
System.out.println("수정 후 선행: " + after);
System.out.println("같은 판단: " + before.equals(after));
}
}
출력은 수정 전 선행: PR-1, 수정 후 선행: PR-3, 같은 판단: false다. 여기서도 결과가 달라지는 것이 수정 목적이다. 한 PR로 끝나는 입력만 검사하면 앞뒤 결과가 같아서 오류를 발견하지 못한다. 완성 코드가 여러 조각으로 나뉘는 작업을 검사하는 이유다.
반복 계산을 정리하면서 입력을 바꾼다
파일 수 계산을 정리하는 PR에서 조각 목록까지 재배열하면, 계산식 정리와 작업 순서 변경이 섞인다. 수정 전후에 같은 입력을 넣어 같은 결과가 나오는지 먼저 확인해야 한다. 다음의 앞 계산은 예제 데이터 개수에 묶여 있어 유지보수하기 어렵다. 뒤 계산은 목록 전체를 순회한다.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> files = List.of(2, 3, 2);
int before = files.get(0) + files.get(1) + files.get(2);
int after = 0;
for (int count : files) {
after += count;
}
System.out.println("수정 전 합계: " + before);
System.out.println("수정 후 합계: " + after);
System.out.println("같은 결과: " + (before == after));
}
}
출력은 수정 전 합계: 7, 수정 후 합계: 7, 같은 결과: true다. 앞의 두 오류 수정과 달리 이 변경은 기존 결과를 유지하는 정리다. 한 입력의 일치가 모든 입력의 동등성을 증명하지는 않지만, 변경 목적과 확인해야 할 관계를 드러낸다. 조각 순서나 완료 기준을 고치는 작업은 별도의 이유로 설명한다.
완료 기준을 비워 둔 채 파일 수만 맞추는 실수도 이 문제와 연결된다. 숫자 계산이 맞아도 무엇을 검토할지 알 수 없으면 계획은 아직 부족하다. 파일 수 조정, 행동 변경, 표현 정리를 한 PR에 함께 넣기 전에 각각 같은 검토 질문에 속하는지 확인한다.
한눈에 보기
| 항목 | 좋은 상태 | 다시 나눌 신호 |
|---|---|---|
| 변경 이유 | 한 문장으로 행동 변화를 설명한다 | 기능 추가와 일괄 정리가 섞인다 |
| 크기 | 의미 있는 조각을 기준 안에서 묶는다 | 조각 하나가 기준보다 크다 |
| 완료 기준 | 조건과 관찰할 결과가 드러난다 | “정상 동작”만 적혀 있다 |
| 선행 관계 | 필요한 작업의 마지막 PR을 가리킨다 | 첫 PR 병합을 전체 완료로 취급한다 |
| 병합 가능성 | 선행 변경 위에서 컴파일되고 검토된다 | 나중 PR이 없으면 현재 PR이 깨진다 |
| 자동화의 범위 | 크기와 연결을 기계로 확인한다 | 파일 수만 보고 설계까지 승인한다 |
분할 계획은 구현 전에 한 번 작성하고 버리는 문서가 아니다. 예상 파일 수가 크게 달라지거나, 한 조각에 다른 정책 결정이 들어오면 경계를 다시 본다. 기준을 맞추려고 필요한 호출부를 빼는 대신, 함께 바뀌어야 하는 이유를 밝히고 예외를 검토하거나 더 작은 행동 단위를 찾는다.
다음 장에서는 이렇게 잘라 낸 조각 하나를 받아 어떤 테스트를 먼저 요구할지 살펴본다. 조각의 완료 기준이 구체적일수록 AI가 낸 테스트에서 빠진 조건을 찾기도 쉬워진다.
연습 문제
- 완성 코드의 파일 수 기준을 4에서 5로 바꾸면 주문 취소 기반 작업은 몇 PR이 되는가. 각 PR의 파일 수와 재고 복구 작업의 선행 PR을 적어라. 기준 변경에 따라 기존 검사 중 무엇을 고쳐야 하는지도 설명해라.
- 이름 정리 작업을 주문 취소 기반 작업의 마지막 조각으로 추가하자는 제안이 들어왔다. 변경 파일 수는 1개이며 완료 기준은 “이름 정리 완료”다. 파일 수 외에 확인할 문제 두 가지를 적고, 별도 작업으로 옮길 때 필요한 완료 기준을 제안해라.
- 기준이 4일 때 조각별 파일 수가 2, 3, 2인 작업을 두 PR로 만들기 위해 첫 조각과 마지막 조각을 합치자는 제안이 있다. 프로그램이 이를 하지 않는 이유를 설명해라. 마지막 조각이 독립적이라는 사실이 확인되면 입력을 어떻게 바꿀지도 적어라.
- 예상 파일 수가 6인 조각 하나를 입력했더니 프로그램이 거부했다. 같은 행동을 파일 수 3과 3으로만 나누어 이름에 “앞부분”, “뒷부분”을 붙이면 충분한가. 주문 취소 사례에서 다시 확인해야 할 조건을 설명해라.
정답과 해설
주문 작업은 두 PR이 된다. 첫 PR은 앞의 두 조각을 합쳐 5개, 둘째 PR은 마지막 조각으로 2개다. 재고 복구는 PR-3이 되며 선행은 PR-2다. 결제 준비는 PR-4가 된다. 전체 개수를 5로 예상한 검사, 주문 내부의 세 PR 연결을 예상한 검사, 재고와 결제의 목록 위치를 직접 사용한 검사를 고쳐야 한다. 기준이 바뀌어도 항상 유지되어야 하는 조건과 예제에만 맞춘 예상값을 구분하는 연습이다.
첫째, 취소 행동 변경과 이름 정리가 같은 검토 이유인지 확인해야 한다. 둘째, “완료”만으로는 기존 호출과 결과가 유지되는지 판단할 수 없다. 별도 정리 작업의 완료 기준은 “같은 주문 입력에서 취소 결과와 거부 이유가 수정 전후에 같고, 이전 이름의 호출이 남아 있지 않다”처럼 정할 수 있다. 정리 과정에서 공개된 호출 방식까지 바뀐다면 그 호환 범위도 따로 적어야 한다.
프로그램은 입력 순서가 구현 의존을 반영한다고 가정한다. 뒤 조각을 앞으로 옮겨 파일 수를 채우면, 아직 없는 서비스 연결을 호출하는 진입점이 먼저 등장할 수 있다. 마지막 조각이 실제로 독립적이라면 별도 작업으로 옮기고 선행 관계를 명시한다. 단순 재배열만으로 독립성이 생기지는 않는다. 파일 수 합계와 작업별 조각 합계도 함께 수정해야 한다.
충분하지 않다. 숫자 두 개로 나누어도 각 PR의 행동과 완료 증거가 없으면 검토할 단위가 생기지 않는다. 상태 규칙을 먼저 추가할 수 있는지, 서비스 연결을 따로 병합해도 컴파일되는지, 호출 진입점을 연결하기 전 기반 코드가 일관된 상태인지 확인해야 한다. 함께 바뀌어야 하는 인터페이스와 구현체라면 같은 조각에 둘 수 있으며, 기준 초과 이유를 팀이 검토할 수도 있다. 이 프로그램은 그런 예외를 자동 승인하지 않으므로, 입력 기준을 바꾸기 전에 사람이 이유와 범위를 판단해야 한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.