리뷰를 받는 쪽 - PR 을 리뷰하기 쉽게 만들기
이 장에서 배우는 것
리뷰어가 변경 의도를 알아내는 데 시간을 모두 쓰면 정작 코드를 살펴볼 시간이 줄어든다. 작성자는 코드를 제출하는 사람인 동시에, 다른 사람이 그 코드를 판단할 수 있도록 자료를 준비하는 사람이다. 앞 장에서 의견의 근거와 제안을 분명하게 쓰는 법을 살펴보았다. 이번에는 그 의견을 받을 준비를 하고, 받은 의견을 확인 가능한 변경으로 돌려주는 방법을 다룬다.
대상은 도서관 대출 서비스의 변경 요청이다. 풀 리퀘스트(pull request, 이하 PR)를 작게 나누고, 설명을 채우고, 스스로 변경 내용을 읽은 뒤 리뷰에 응답하는 과정을 따라간다. 마지막에는 PR 정보로 리뷰 준비 점수를 계산하고 빠진 항목을 알려 주는 프로그램을 만든다. 점수는 이 장에서 정한 준비 기준의 합계이며, 코드 품질이나 승인 여부를 대신하지 않는다.
- 한 가지 목적을 중심으로 PR 범위를 정하고, 함께 바뀌어야 하는 코드를 구별한다.
- 목적·변경·검증·위험을 읽는 사람이 판단할 수 있는 정보로 작성한다.
- 리뷰 요청 전에 변경 내용을 다시 읽고 설명과 코드의 일치를 확인한다.
- 리뷰 의견에 처리 결과를 답하고, 반영 커밋을 확인하기 쉬운 단위로 나눈다.
- 실행 가능한 준비 점검 프로그램으로 빠진 설명 항목을 찾는다.
문제 상황
도서관 대출 서비스에서 회원이 자신의 대출 상태를 조회하는 기능을 고치고 있다. 이번 요청의 목적은 대출 중인 도서가 없을 때 안내 문구를 더 분명하게 보여 주는 것이다. 그런데 작성자는 관련 파일을 읽다가 메서드 이름을 바꾸고, 날짜 표시 형식을 고치고, 반납 처리의 중복 코드도 정리했다. 모두 필요한 일처럼 보였으므로 하나의 PR에 넣었다.
설명에는 “대출 기능 개선”만 적혀 있다. 리뷰어는 반환 문구가 달라지는 이유를 찾다가 날짜 처리 변경을 발견한다. 날짜 변경이 이번 요청에 필요한지, 반납 처리까지 확인해야 하는지 알 수 없다. 작성자는 실행해 보았다고 답하지만 어떤 입력으로 무엇을 확인했는지는 남아 있지 않다. 코드가 짧아도 판단에 필요한 정보가 흩어져 있으면 리뷰는 오래 걸린다.
“이번 요청은 대출 내역이 없는 회원에게 안내 문구를 바꾸는 일로 이해했다. 날짜 형식 변경도 같은 요구에 포함되는가. 별도 목적이라면 분리하고, 이번 PR에서 확인한 입력과 결과를 설명에 남겨 주기 바란다.”
이 의견에 “네, 수정했다”라고만 답하면 리뷰어는 모든 변경을 다시 찾아야 한다. 작성자는 범위를 정리하고, 무엇을 옮겼으며 무엇을 유지했는지 알려야 한다. 설명을 보완하는 일도 작업의 일부다. 코드를 잘 읽히게 하는 일과 PR을 잘 읽히게 하는 일은 함께 진행된다.
팀은 같은 문제가 반복되지 않도록 간단한 준비 점검기를 만들어 보기로 했다. 점검기는 PR 서비스에 접속하지 않는다. 작성자가 입력한 목적, 변경, 검증, 위험 설명과 두 가지 확인 표시를 받아 빠진 항목을 출력한다. 실제 내용을 이해하는 일은 사람이 맡고, 기계는 비어 있는 칸과 확인되지 않은 표시를 찾는다.
작은 PR은 하나의 판단으로 묶는다
작은 PR은 줄 수만 적은 PR이 아니다. 리뷰어가 한 문장으로 목적을 설명할 수 있고, 그 목적을 기준으로 변경의 필요성을 판단할 수 있는 PR이다. 대출 내역이 없을 때의 안내를 고치는 작업이라면 안내를 반환하는 코드와 그 동작을 확인하는 내용이 함께 있어야 한다. 확인 코드를 떼어 다음 PR로 보내는 것은 크기를 줄여도 판단을 쉽게 만들지 못한다.
반대로 날짜 표시 형식은 별도 요구라면 다른 PR로 옮길 수 있다. 두 작업이 같은 파일을 수정한다는 사실만으로 같은 목적이 되지는 않는다. 파일 단위보다 변경 이유를 기준으로 묶는다. “이 변경을 빼도 이번 목적을 달성하고 검증할 수 있는가”를 물으면 분리 후보를 찾기 쉽다.
동작을 유지하는 구조 정리가 본래 변경을 이해하는 데 필요할 때도 있다. 이때 구조 정리와 동작 변경을 커밋으로 구분하면 읽는 순서를 제공할 수 있다. 다만 앞선 구조 정리가 아주 크다면 먼저 별도 PR로 검토하는 편이 나을 수 있다. 뒤 PR이 앞 PR에 의존한다면 설명에 그 관계와 검토 순서를 적는다. 아직 반영되지 않은 선행 변경을 이미 사용 가능한 코드처럼 설명해서는 안 된다.
이번 장의 완성 코드에는 고치기 전 점검 방식과 고친 뒤 점검 방식이 함께 들어 있다. 전자는 조건마다 누락 항목을 추가한다. 후자는 반복되는 추가 절차를 작은 메서드로 모은다. 이 변경의 목적은 점수 기준을 바꾸는 것이 아니라 같은 기준을 읽기 쉽게 표현하는 것이다. 따라서 같은 입력에 같은 점수와 같은 순서의 누락 목록이 나와야 한다.
이렇게 목적을 먼저 정하면 리뷰 범위도 정해진다. 새 점수 기준을 제안하는 의견은 별도 논의로 남기고, 이번에는 기존 판정이 유지되는지 확인한다. 관련 의견을 무시하라는 뜻은 아니다. 지금 반영할 일인지, 다음 작업으로 다룰 일인지 구분해 답하라는 뜻이다.
설명과 자기 점검으로 읽는 순서를 만든다
PR 설명은 작업 일지가 아니다. 어떤 파일을 몇 번 고쳤는지보다 왜 이 변경을 판단해야 하는지 알려 주는 자료다. 목적·변경·검증·위험의 네 항목을 고정하면 작성할 때 빠뜨린 내용을 찾기 쉽다. 각 항목에는 독자가 다음 행동을 정할 수 있는 정보를 넣는다.
| 항목 | 답해야 할 질문 | 작성 예 |
|---|---|---|
| 목적 | 어떤 불편을 해결하는가 | 대출 내역이 없는 회원이 조회 실패로 오해하지 않도록 안내를 바꾼다. |
| 변경 | 무엇을 바꾸었는가 | 빈 대출 목록에 사용하는 안내 문구를 바꾸었다. |
| 검증 | 어떤 입력에서 무엇을 확인했는가 | 대출 0권과 1권으로 조회하여 빈 목록 안내와 기존 목록 표시를 확인했다. |
| 위험 | 어떤 영향과 미확인 사항이 있는가 | 화면 안내가 바뀐다. 대출 저장 처리는 변경하지 않았고, 좁은 화면의 줄바꿈은 확인하지 못했다. |
목적에는 기대하는 사용자의 변화를 적는다. 변경에는 그 목적을 위해 손댄 동작이나 구조를 적는다. 두 항목을 모두 “대출 기능 개선”으로 채우면 제목을 반복하는 데 그친다. 검증에는 실행 명령만 적지 말고 대표 입력과 기대 결과, 실제 확인 결과를 남긴다. 실행하지 못했다면 그 사실과 남은 확인을 구체적으로 쓴다.
위험 항목도 빈칸으로 두지 않는다. 영향이 작다고 판단했다면 어떤 범위를 살펴보고 그렇게 판단했는지 적는다. “위험 없음”이라는 문장을 자동으로 넣으면 검토할 단서를 잃는다. 알려진 영향과 아직 확인하지 못한 범위를 함께 기록하면 리뷰어가 추가 확인의 필요성을 판단할 수 있다.
리뷰를 요청하기 전에는 작성자도 변경 전후의 차이를 다시 읽는다. 작업 중에는 자신이 의도한 코드를 떠올리기 쉽다. 제출할 차이만 따로 읽으면 의도와 무관한 이름 변경, 임시 출력, 설명에서 빠진 동작 변경을 찾을 수 있다. 이때 전체 파일을 다시 쓰는 것이 아니라 이번 PR에 실제로 포함된 변경을 확인한다.
- 각 변경을 목적 문장과 연결해 보고, 연결되지 않는 변경은 분리할지 판단한다.
- 설명에 적은 검증을 실제 제출 상태에서 수행했는지 확인한다.
- 고치기 전후에 동작을 유지한다고 했다면 같은 입력의 결과를 비교한다.
- 위험 항목에 남긴 미확인 사항이 코드와 모순되지 않는지 확인한다.
완성 코드의 준비 기준은 여섯 개다. 네 설명이 비어 있지 않으면 각각 1점, 자기 점검을 마쳤으면 1점, 한 가지 목적의 범위를 확인했으면 1점이다. 합계는 6점이다. 이 숫자는 설명용으로 정한 고정 규칙이며 팀 성과 지표가 아니다. 특히 문장이 존재한다는 사실과 설명이 충분하다는 판단은 다르다. “나중에 확인”이라는 문장도 이 프로그램에서는 검증 항목을 채운 것으로 계산된다. 그래서 결과는 검토 시작을 돕는 자료로만 사용한다.
의견에는 처리 결과를 답하고 변경은 추적 가능하게 남긴다
리뷰 의견을 받으면 먼저 요청을 이해했는지 확인한다. 수정 요구인지, 이유를 묻는 질문인지, 다음 작업에 대한 제안인지 구분한다. 의미가 불분명하면 자신이 이해한 내용을 짧게 적고 확인한다. 곧바로 코드를 바꾸면 질문에 대한 설명만으로 충분했던 상황에서도 불필요한 변경을 만들 수 있다.
의견을 반영했다면 바꾼 내용과 확인 결과를 답한다. 관련 커밋을 가리키면 재확인이 쉬워진다. 이번 범위에서 반영하지 않는다면 이유와 처리 방향을 적는다. 합의하지 못한 내용을 작성자가 임의로 완료 처리하면 논의가 끝났는지 알기 어렵다. 대화의 완료 표시와 코드 반영 여부를 별개로 확인한다.
“날짜 표시 변경은 안내 문구 개선과 목적이 달라 이번 PR에서 제외했다. 안내 문구 변경과 조회 확인만 남겼다. 대출 0권과 1권으로 다시 실행했고, 각각 새 안내와 기존 목록 표시를 확인했다.”
리뷰 반영 커밋도 의견의 개수에 기계적으로 맞출 필요는 없다. 같은 이유로 고친 코드와 그 확인은 함께 묶는다. 서로 다른 판단이 필요한 수정은 나눈다. 예를 들어 “빈 설명 판정 수정”과 “출력 문구 정리”를 한 커밋에 섞으면 리뷰어가 어느 수정이 판정 결과에 영향을 주는지 다시 찾아야 한다.
리뷰 도중에는 기존 커밋을 계속 합쳐 버리기보다 추가 커밋으로 반영 내용을 보여 주는 방법이 유용하다. 이미 읽은 상태 이후 무엇이 달라졌는지 찾을 수 있기 때문이다. 최종 병합 전에 이력을 정리할지는 팀 방식에 따른다. 정리했다면 이전 의견과 대응하는 변경을 찾을 수 있도록 설명을 갱신하고 재검토가 필요한 범위를 알린다.
이제 준비 점검기를 완성한다. 입력 객체에는 설명 네 개와 확인 표시 두 개를 둔다. 프로그램은 같은 두 입력을 변경 전후 방식으로 계산하고 결과의 동등성을 확인한다. 이어서 공백 설명, null 설명, 누락 항목 순서를 작은 확인 함수로 검사한다. 외부 서비스나 파일을 사용하지 않아 실행할 때마다 같은 결과가 나온다.
완성 코드
Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
private static final int MAX_SCORE = 6;
record PrInfo(
String purpose,
String changes,
String verification,
String risk,
boolean selfReviewed,
boolean singlePurpose) {
}
record Report(int score, List<String> missing) {
Report {
missing = List.copyOf(missing);
}
}
private static boolean filled(String value) {
return value != null && !value.isBlank();
}
// 고치기 전: 기준마다 누락 추가 절차를 직접 쓴다.
private static Report assessBefore(PrInfo pr) {
List<String> missing = new ArrayList<>();
if (!filled(pr.purpose())) {
missing.add("목적");
}
if (!filled(pr.changes())) {
missing.add("변경");
}
if (!filled(pr.verification())) {
missing.add("검증");
}
if (!filled(pr.risk())) {
missing.add("위험");
}
if (!pr.selfReviewed()) {
missing.add("자기 점검");
}
if (!pr.singlePurpose()) {
missing.add("한 가지 목적");
}
return new Report(MAX_SCORE - missing.size(), missing);
}
private static void addMissing(
List<String> missing, boolean satisfied, String label) {
if (!satisfied) {
missing.add(label);
}
}
// 고친 후: 기준과 반복 절차를 나누되 판정 순서를 유지한다.
private static Report assessAfter(PrInfo pr) {
List<String> missing = new ArrayList<>();
addMissing(missing, filled(pr.purpose()), "목적");
addMissing(missing, filled(pr.changes()), "변경");
addMissing(missing, filled(pr.verification()), "검증");
addMissing(missing, filled(pr.risk()), "위험");
addMissing(missing, pr.selfReviewed(), "자기 점검");
addMissing(missing, pr.singlePurpose(), "한 가지 목적");
return new Report(MAX_SCORE - missing.size(), missing);
}
private static String format(Report report) {
String items = report.missing().isEmpty()
? "없음"
: String.join(", ", report.missing());
return report.score() + "/" + MAX_SCORE + " | 빠진 항목: " + items;
}
private static int failures = 0;
private static void check(boolean condition, String name) {
System.out.println((condition ? "PASS: " : "FAIL: ") + name);
if (!condition) {
failures++;
}
}
public static void main(String[] args) {
PrInfo draft = new PrInfo(
"빈 대출 목록을 조회 실패로 오해하지 않게 한다.",
"빈 목록의 안내 문구를 바꾸었다.",
" ",
null,
false,
true);
PrInfo ready = new PrInfo(
"빈 대출 목록을 조회 실패로 오해하지 않게 한다.",
"빈 목록의 안내 문구를 바꾸었다.",
"대출 0권과 1권에서 안내와 목록 표시를 확인했다.",
"화면 안내가 바뀐다. 좁은 화면 줄바꿈은 미확인이다.",
true,
true);
Report draftBefore = assessBefore(draft);
Report draftAfter = assessAfter(draft);
Report readyBefore = assessBefore(ready);
Report readyAfter = assessAfter(ready);
System.out.println("초안 변경 전: " + format(draftBefore));
System.out.println("초안 변경 후: " + format(draftAfter));
System.out.println("준비본 변경 전: " + format(readyBefore));
System.out.println("준비본 변경 후: " + format(readyAfter));
check(draftBefore.equals(draftAfter), "초안 전후 결과 동일");
check(readyBefore.equals(readyAfter), "준비본 전후 결과 동일");
check(draftAfter.score() == 3, "초안 점수 3");
check(draftAfter.missing().equals(
List.of("검증", "위험", "자기 점검")), "누락 순서 유지");
check(readyAfter.score() == MAX_SCORE
&& readyAfter.missing().isEmpty(), "준비본 누락 없음");
PrInfo empty = new PrInfo(null, " ", "\t", "", false, false);
Report emptyReport = assessAfter(empty);
check(emptyReport.score() == 0
&& emptyReport.missing().size() == MAX_SCORE,
"빈 설명과 미확인 표시 모두 누락");
check(assessBefore(empty).equals(emptyReport), "빈 입력 전후 결과 동일");
PrInfo padded = new PrInfo(" 목적 ", " 변경 ", " 검증 ", " 위험 ",
true, true);
check(assessAfter(padded).score() == MAX_SCORE,
"앞뒤 공백이 있는 설명 인정");
System.out.println("실패 수: " + failures);
if (failures != 0) {
throw new IllegalStateException("준비 점검 확인 실패");
}
}
}
줄별 해설
파일 위에서부터 의미 있는 줄을 묶어 읽는다. 먼저 가져온 두 컬렉션은 역할이 다르다. ArrayList는 판정 중 누락 항목을 순서대로 모으는 데 사용하고, List는 결과와 메서드의 자료형을 표현하는 데 사용한다. MAX_SCORE는 여섯 기준의 합계를 나타낸다. 이 예제에서는 기준 수를 고정했으므로 기준을 추가할 때 이 값과 확인 코드도 함께 검토해야 한다.
PrInfo 레코드(record)는 PR 설명과 작성자의 확인 표시를 담는다. 네 문자열은 설명의 존재를 확인하는 입력이다. 두 논리값은 실제 자기 점검과 범위 확인을 했다는 작성자의 표시다. 프로그램이 코드를 읽고 이 값을 추론하지 않는다. 따라서 true를 넣는 책임은 입력을 준비하는 사람에게 있다.
Report는 점수와 누락 목록을 함께 반환한다. 생성자 안의 List.copyOf는 전달받은 목록의 수정 불가능한 복사본을 저장한다. 판정에 사용한 임시 목록과 반환한 결과를 분리하려는 선택이다. 레코드의 equals는 구성 요소를 비교하고, 목록의 비교에는 항목 순서도 포함된다. 이 성질을 이용해 전후 결과의 점수와 누락 순서를 함께 확인한다.
filled는 null이 아니고 공백만으로 이루어진 문자열도 아닌지 확인한다. 설명의 앞뒤에 공백이 있어도 내용이 있으면 인정한다. 여기서는 자연어의 적절성을 판정하지 않는다. 문장의 길이를 늘리거나 특정 단어를 넣는 방법으로 설명의 충실함을 대신 측정하지도 않는다.
assessBefore는 여섯 조건을 순서대로 확인한다. 충족하지 못한 항목을 목록에 넣고, 전체 기준 수에서 누락 개수를 빼 점수를 계산한다. 같은 목록을 점수 계산과 누락 안내에 사용하므로 두 결과가 서로 다른 판정에서 나오지 않는다. 목록 순서는 목적, 변경, 검증, 위험, 자기 점검, 한 가지 목적이다.
addMissing은 조건이 충족되지 않았을 때 이름을 넣는 공통 절차다. assessAfter에서는 조건과 이름을 나란히 읽을 수 있다. 단순히 줄 수가 줄었다는 이유로 더 좋다고 결론 내릴 수는 없다. 팀이 이 표현을 더 쉽게 읽는지 판단해야 한다. 이번 PR에서 실행으로 확인하는 것은 기존 판정과 반환 순서가 유지된다는 사실이다.
format은 점수와 누락 목록을 출력 문장으로 바꾼다. 누락이 없을 때는 “없음”을 사용한다. 판정 메서드에서 화면 문구를 직접 출력하지 않으므로, main에서 같은 결과를 비교하고 출력할 수 있다. 반환 결과를 비교하는 확인과 사용자가 보는 문장을 확인하는 절차도 구별할 수 있다.
check는 조건이 참이면 PASS, 거짓이면 FAIL을 출력한다. 실패 개수를 누적한 뒤 마지막에 예외를 던지므로, 하나가 실패해도 뒤의 확인을 실행할 수 있다. 이 프로그램에서는 조건을 확인하는 데 부수 효과가 없으며 출력 순서도 고정되어 있다.
main의 초안은 목적, 변경, 한 가지 목적을 충족해 3점이다. 준비본은 여섯 기준을 모두 충족한다. 먼저 각 입력의 변경 전후 출력 네 줄을 보여 주고, 이어서 두 결과 객체를 비교한다. 사람이 화면에서 같은지 살펴보는 데 그치지 않고 프로그램도 점수와 목록이 같은지 확인하는 구성이다.
마지막 두 입력은 대표 입력만으로 빠뜨리기 쉬운 판정을 확인한다. empty는 null, 일반 공백, 탭, 빈 문자열을 포함한다. padded는 설명 앞뒤에 공백이 있지만 내용도 있다. 이 확인으로 모든 입력의 동등성이 증명되는 것은 아니다. 다만 이번 변경에서 유지하려는 중요한 사례와 누락 순서를 실행 가능한 근거로 남긴다.
실행 결과
Main.java를 같은 디렉터리에 저장한 뒤 다음 명령을 실행한다. JDK 17에서 외부 라이브러리 없이 컴파일되며 컴파일 경고는 발생하지 않는다. 첫 명령은 경고까지 확인하는 용도이며, 두 번째 컴파일 명령은 기본 실행 절차다. 프로그램은 파일을 만들거나 시각을 읽지 않는다.
javac -Xlint:all -d out *.java
javac -d out *.java
java -cp out Main
예상 출력은 다음과 같다. 초안과 준비본 각각의 변경 전후 문장이 같고, 결과 객체 비교도 통과한다.
초안 변경 전: 3/6 | 빠진 항목: 검증, 위험, 자기 점검
초안 변경 후: 3/6 | 빠진 항목: 검증, 위험, 자기 점검
준비본 변경 전: 6/6 | 빠진 항목: 없음
준비본 변경 후: 6/6 | 빠진 항목: 없음
PASS: 초안 전후 결과 동일
PASS: 준비본 전후 결과 동일
PASS: 초안 점수 3
PASS: 누락 순서 유지
PASS: 준비본 누락 없음
PASS: 빈 설명과 미확인 표시 모두 누락
PASS: 빈 입력 전후 결과 동일
PASS: 앞뒤 공백이 있는 설명 인정
실패 수: 0
이 결과를 PR 검증 항목에 남길 때는 “실행 성공”만 적지 않는다. 초안·준비본·빈 설명 입력에서 전후 점수와 누락 목록이 같았고, 공백이 포함된 설명 판정도 확인했다고 적는다. 준비본의 6점은 설명 칸과 확인 표시가 갖춰졌다는 뜻이다. 좁은 화면 줄바꿈이 미확인이라는 입력 내용까지 해결되었다는 뜻은 아니다.
실무에서 자주 틀리는 것
다음 코드는 각각 독립적으로 실행할 수 있는 짧은 예다. 완성 프로그램에 추가할 파일은 아니다. 실행 가능한 형태로 PR 제목이나 응답 문구를 출력하여 차이를 보여 준다. 문장 예시를 실제 도구 호출처럼 보이게 작성하지 않는다.
서로 다른 목적을 제목 하나로 덮는다
다음 제목은 검토할 작업의 경계를 알려 주지 못한다. 안내 문구, 날짜 표시, 반납 구조 정리를 함께 올린 뒤 제목만 짧게 만드는 것으로 범위가 작아지지는 않는다.
class BroadTitle {
public static void main(String[] args) {
System.out.println("대출 기능 전반 개선");
}
}
고친 예는 이번 PR의 목적을 구체적으로 드러낸다. 실제 변경에서도 날짜 표시와 반납 정리를 분리해야 제목과 내용이 맞는다. 제목을 고치는 일만으로 분리가 끝나는 것은 아니다.
class FocusedTitle {
public static void main(String[] args) {
System.out.println("빈 대출 목록의 안내 문구 변경");
}
}
검증 항목을 실행 여부 한마디로 끝낸다
“확인했다”는 말에는 입력과 결과가 없다. 리뷰어는 어떤 상황을 다시 살펴봐야 하는지 판단하기 어렵다. 확인을 더 많이 했더라도 설명에 남기지 않으면 다른 사람이 이용할 근거가 되지 못한다.
class VagueVerification {
public static void main(String[] args) {
System.out.println("검증: 실행해서 확인했다.");
}
}
고친 예는 입력과 관찰 결과를 연결한다. 실제로 실행한 내용만 적어야 한다. 실행하지 않은 화면이나 입력을 확인했다고 채워 넣으면 설명은 길어져도 신뢰할 근거는 줄어든다.
class ConcreteVerification {
public static void main(String[] args) {
System.out.println(
"검증: 대출 0권에서 새 안내, 1권에서 기존 목록 표시를 확인했다.");
}
}
리뷰 의견에 완료 여부만 답한다
다음 응답은 어떤 요청을 어떤 방식으로 처리했는지 알려 주지 않는다. 코드 변경이 있었는지, 설명만 보완했는지조차 알 수 없어 리뷰어가 전체 차이를 다시 읽게 된다.
class ShortReply {
public static void main(String[] args) {
System.out.println("수정했다.");
}
}
고친 응답에는 처리 내용과 재확인 결과가 있다. 실제 PR에서는 대응 커밋도 가리킬 수 있다. 의견을 반영하지 않았다면 같은 자리에 판단 이유와 다음 처리 방향을 적는다.
class TraceableReply {
public static void main(String[] args) {
System.out.println(
"날짜 변경을 제외했다. 대출 0권과 1권 조회 결과를 다시 확인했다.");
}
}
준비 점수를 승인으로 바꿔 읽는다
점검기가 모든 항목을 찾았다고 해서 변경 동작이 요구에 맞는 것은 아니다. 다음 문장은 입력의 존재 확인을 승인 판단으로 확대한다. 이런 출력을 사용하면 작성자와 리뷰어가 점수의 의미를 다르게 이해할 수 있다.
class MisleadingScore {
public static void main(String[] args) {
int score = 6;
if (score == 6) {
System.out.println("승인 가능");
}
}
}
고친 문장은 점검 범위를 그대로 드러낸다. 준비 점검을 통과한 뒤에도 설명의 근거와 실제 변경을 사람이 읽어야 한다. 이 프로그램의 자기 점검 표시도 실행 기록이나 검토 내용을 자동으로 보증하지 않는다.
class BoundedScore {
public static void main(String[] args) {
int score = 6;
if (score == 6) {
System.out.println("준비 항목 누락 없음. 내용 검토가 필요하다.");
}
}
}
한눈에 보기
| 준비할 일 | 남길 정보 | 확인 기준 |
|---|---|---|
| 범위 줄이기 | 한 문장 목적과 제외한 작업 | 각 변경이 같은 목적에 필요한가 |
| 설명 채우기 | 목적·변경·검증·위험 | 입력과 결과, 미확인 범위를 알 수 있는가 |
| 스스로 읽기 | 제출 상태에서 확인한 내용 | 설명과 실제 차이가 일치하는가 |
| 의견에 답하기 | 반영 결과 또는 미반영 이유 | 의견의 처리 상태를 알 수 있는가 |
| 커밋 나누기 | 같은 이유의 수정과 확인 | 리뷰 이후 달라진 부분을 찾을 수 있는가 |
| 점수 해석하기 | 6개 기준과 누락 목록 | 준비 여부와 승인 판단을 구별하는가 |
작성자가 만드는 것은 코드와 설명의 연결이다. 목적이 범위를 정하고, 검증이 변경의 근거를 제공하며, 응답과 커밋이 재검토할 위치를 알려 준다. 다음 장에서는 이 준비 자료를 바탕으로 자동 리뷰 도구와 사람이 각각 맡을 일을 종합 실습에서 살펴본다.
연습 문제
- 빈 대출 목록 안내를 바꾸는 PR에 메서드 이름 정리, 확인 코드, 날짜 형식 변경이 함께 들어 있다. 어떤 기준으로 남길 변경과 분리할 변경을 정할지 설명하라. 이름 정리가 안내 변경을 이해하는 데 필요한 경우도 고려하라.
- “검증: 실행 성공. 위험: 없음.”을 실제 확인한 정보가 드러나게 다시 작성하라. 대출 0권과 1권 조회는 확인했고, 좁은 화면의 줄바꿈은 확인하지 못했다고 가정한다.
- 완성 코드에 위험 설명만 비어 있고 다른 다섯 기준은 충족하는 입력을 추가하라. 점수가 5이고 누락 목록이 위험 하나이며 전후 결과가 같은지 check로 확인하라.
- 리뷰어가 날짜 변경을 분리해 달라고 요청했다. 작성자는 분리했고 안내 조회를 다시 확인했다. 답변 문장과 반영 커밋의 범위를 작성하라. 준비 점수가 6점일 때 승인되었다고 답해도 되는지도 설명하라.
정답과 해설
첫 번째 문제는 같은 목적을 달성하고 판단하는 데 필요한지를 기준으로 나눈다. 안내 변경과 그 동작을 확인하는 코드는 함께 둔다. 날짜 형식 변경이 독립 요구라면 분리한다. 이름 정리가 안내 변경을 읽는 데 필요하다면 이유를 설명하고 구조 정리 커밋으로 구분할 수 있다. 없어도 이해에 지장이 없다면 별도 작업으로 옮기는 편이 범위를 분명하게 만든다.
두 번째 문제의 예는 “검증: 대출 0권 조회에서 새 안내를, 1권 조회에서 기존 목록 표시를 확인했다. 위험: 화면 안내 문구가 바뀐다. 좁은 화면의 줄바꿈은 확인하지 못했다.”다. 확인한 결과와 남은 확인을 구별한다. 실행하지 않은 항목까지 완료된 것처럼 쓰지 않는 것이 핵심이다.
세 번째 문제는 다음 완전한 프로그램으로 확인할 수 있다. Main.java와 같은 디렉터리에 ExerciseCheck.java로 저장하면 완성 코드의 자료형을 이용한다. 판정 메서드는 private이므로 메서드를 공개하는 대신 Main 안에 아래와 같은 입력과 check를 추가하는 것이 본래 과제의 방법이다. 여기서는 별도 실행 예를 위해 동일한 입력의 누락 조건을 직접 확인한다.
class ExerciseCheck {
public static void main(String[] args) {
Main.PrInfo pr = new Main.PrInfo(
"빈 대출 목록의 안내 개선",
"안내 문구 변경",
"대출 0권과 1권 조회 확인",
" ",
true,
true);
boolean riskMissing = pr.risk() == null || pr.risk().isBlank();
check(riskMissing, "위험 설명 누락");
}
private static void check(boolean condition, String name) {
System.out.println((condition ? "PASS: " : "FAIL: ") + name);
}
}
Main 안에서는 이 입력으로 assessBefore와 assessAfter를 각각 호출한다. 고친 뒤 결과의 score가 5인지, missing이 List.of("위험")과 같은지, 두 Report가 같은지를 별도의 check 세 번으로 확인한다. 위의 별도 실행 예는 입력 조건만 확인하므로 점수와 전후 동등성 확인을 대체하지 않는다. 과제의 완료 기준은 세 확인을 모두 추가하는 것이다.
네 번째 문제의 답변 예는 “날짜 형식 변경을 별도 작업으로 분리했다. 이번 반영 커밋에는 해당 변경을 제외한 내용과 관련 설명 갱신을 담았다. 대출 0권과 1권 조회를 다시 실행해 안내와 목록 표시를 확인했다.”다. 날짜 변경 제외와 관련 확인은 같은 목적의 반영으로 묶고, 무관한 출력 정리를 함께 넣지 않는다. 준비 점수 6점은 항목 누락이 없다는 뜻이므로 승인되었다고 답할 수 없다. 승인은 설명의 내용과 실제 변경을 검토한 뒤 팀의 절차에 따라 판단한다.