소프트웨어 공학 · 심화
코드 리뷰 실무 - 버그를 잡고 팀을 키우는 읽기
리뷰 의견 쓰기 - 말투·근거·제안
질문형·제안형 의견, 필수와 선택 구분 표시, 근거(재현 입력·문서) 붙이기, 사람 대신 코드를 말하기, 칭찬, 완성 코드는 리뷰 의견 데이터에서 근거 없는 지적·모호한 지적을 찾아 표시한다
개발자KR · 원고 갱신
이 장에서 배우는 것
리뷰 의견은 코드를 읽고 떠올린 생각을 그대로 적는 메모가 아니다. 작성자가 문제를 확인하고 다음 행동을 결정할 수 있도록 전달하는 작업이다. “이상하다”는 감상과 “이 입력에서 대출 안내가 규칙과 다르게 나온다”는 설명은 서로 다른 일을 한다. 앞의 말은 해석을 요구하고, 뒤의 말은 확인할 출발점을 제공한다.
앞 장에서 테스트가 무엇을 확인하는지 살펴보았다. 이 장에서는 그 확인 결과를 동료에게 어떻게 전달할지 다룬다. 같은 결함을 발견해도 사람을 평가하는 말로 전달하면 코드에 관한 대화가 어려워진다. 반대로 말투만 부드럽게 만들고 수정 필요성을 숨기면 중요한 문제가 선택 사항처럼 보인다. 의견에는 존중과 명확함이 함께 필요하다.
- 질문형과 제안형을 상황에 맞게 사용하고, 필요한 변경을 분명하게 전달한다.
- 필수 의견과 선택 의견을 표시하고 그 판단의 근거를 붙인다.
- 재현 입력과 팀 문서를 이용해 다른 사람이 확인할 수 있는 의견을 쓴다.
- 사람 대신 코드의 동작을 말하고, 유지할 만한 판단을 구체적으로 칭찬한다.
- 리뷰 의견 데이터에서 근거 누락과 모호함의 후보를 찾아 표시하는 프로그램을 실행한다.
문제 상황
도서관 대출 서비스 팀이 대출 가능 여부를 안내하는 메서드를 정리하고 있다. 이번 변경의 목적은 중복된 반환 구문을 줄이는 것이다. 대출 중인 책이 세 권 이상이면 “대출 불가”, 그보다 적으면 “대출 가능”이라는 기존 안내는 유지하기로 했다. 팀 문서에는 이 조건이 대출 규칙 L-1로 기록되어 있다. 이 문서는 예제를 위해 만든 팀 내부 규칙이다.
리뷰 화면에는 다음과 같은 의견이 달렸다.
이렇게 짜면 곤란하다. 다시 정리해 달라.
[선택] 이 코드는 느리니 문자열 캐시를 추가하면 좋겠다.
첫 의견은 어느 부분을 어떻게 바꿔야 하는지 알기 어렵다. 두 번째 의견은 구체적인 대안을 말하지만, 느리다는 판단을 확인할 자료가 없다. 선택 표시를 붙였다고 해서 근거가 필요 없어지는 것은 아니다. 작성자가 캐시를 추가하면 코드의 책임과 관리할 상태가 늘어나는데, 그 변경을 시작할 이유가 빠져 있다.
팀은 의견을 정리하면서 두 가지를 확인하기로 했다. 먼저 코드 정리가 기존 안내를 유지했는지 같은 입력으로 비교한다. 이어서 의견 데이터에 대상, 근거, 요청을 나누어 담고, 비어 있는 항목을 표시한다. 프로그램은 의견을 채점하지 않는다. 사람이 다시 읽어야 할 후보를 찾는 역할을 맡는다.
질문과 제안에 판단의 무게를 담는다
질문은 모르는 사실을 확인할 때 쓴다
질문형 의견은 작성자만 알고 있을 수 있는 조건을 확인할 때 유용하다. 다만 물음표를 붙였다고 모두 좋은 질문은 아니다. “왜 이렇게 했나요?”는 질문의 범위가 넓다. 작성자는 이름, 분기, 자료구조 중 무엇을 설명해야 하는지 추측해야 한다.
[질문] LoanService.message의 대출 한도는 회원 종류와 관계없이 세 권으로 적용하는가. 별도 예외 규칙이 있다면 문서 위치를 알려 달라.
이 질문에는 대상과 필요한 답변이 들어 있다. 아직 결함이라고 단정하지 않으므로 필수 수정으로 표시하지 않는다. 답변을 받은 뒤 현재 코드가 규칙을 어기는 것으로 확인되면, 그때 근거를 붙인 필수 의견으로 바꿀 수 있다.
이미 규칙 위반을 확인했다면 질문으로 책임을 감추지 않는다. “세 권도 막아야 하지 않을까요?”보다 “세 권인 입력에서 안내가 규칙과 다르므로 조건을 수정해 달라”가 필요한 행동을 분명하게 전달한다. 다만 실행하지 않은 결과를 실행한 것처럼 쓰거나, 읽지 않은 문서를 근거로 적어서는 안 된다.
제안은 이유와 허용 범위를 함께 쓴다
제안형 의견은 대안을 제시하되 그 대안만 정답이라고 가정하지 않는다. “이름을 바꿔 달라”에 그치지 않고 현재 이름이 무엇을 읽기 어렵게 하는지 설명한다. 작성자가 같은 문제를 더 적절한 방법으로 해결할 여지도 남긴다.
[선택] count가 무엇을 세는지 메서드 선언만으로는 알기 어렵다. 현재 대출 권수를 뜻하므로 activeLoanCount로 바꾸는 것을 제안한다. 같은 뜻이 드러나는 다른 이름도 가능하다.
필수와 선택은 말투의 강도가 아니라 변경을 끝내기 위한 조건이다. 팀이 합의한 규칙 위반이나 확인된 잘못된 동작은 필수 의견의 근거가 될 수 있다. 취향이나 여러 대안 중 하나의 선호는 보통 선택 의견으로 전달한다. 모든 의견을 필수로 만들면 중요한 문제의 구분이 흐려진다. 반대로 필요한 수정을 선택으로 표시하면 작성자는 그대로 두어도 된다고 이해할 수 있다.
| 표시 | 쓰는 상황 | 함께 적을 내용 |
|---|---|---|
| 필수 | 확인된 동작이나 합의된 규칙을 바로잡아야 한다 | 대상, 근거, 필요한 결과 |
| 선택 | 현재 변경을 끝낼 수 있지만 개선 여지가 있다 | 개선 이유, 제안, 다른 대안의 여지 |
| 질문 | 판단에 필요한 조건을 아직 모른다 | 확인할 대상과 필요한 정보 |
| 칭찬 | 유지하거나 공유할 만한 판단이 있다 | 좋았던 코드와 그 효과 |
질문과 칭찬은 필수·선택과 같은 종류의 구분이 아니다. 질문은 정보를 얻는 형식이고, 칭찬은 유지할 판단을 알리는 형식이다. 뒤의 프로그램에서도 의견의 종류와 수정 필요도를 별도 값으로 표현한다. 질문이나 칭찬에는 수정 필요도를 지정하지 않는다.
근거와 요청을 연결하고 사람 대신 코드를 말한다
근거는 다른 사람이 다시 확인할 수 있어야 한다
재현 입력은 입력값만 적는 것으로 끝나지 않는다. 어떤 대상을 실행했고, 어떤 결과를 기대했으며, 실제로 무엇이 나왔는지 연결해야 한다. 예를 들어 “세 권에서 문제”보다 “LoanService.message에 3을 전달하면 대출 가능이 나온다. L-1은 세 권부터 대출 불가로 정한다”가 확인하기 쉽다. 이는 조건을 잘못 바꾼 변경을 발견했을 때 쓸 수 있는 의견이다. 이번 장의 완성 코드에 있는 변경 전후 메서드는 모두 L-1을 지킨다.
문서를 근거로 삼을 때는 “규칙에 있다”라고만 적지 않는다. 문서 이름과 항목, 적용 조건을 남긴다. 문서가 저장소에 있다면 해당 위치를 연결하고, 내용이 바뀔 수 있다면 검토한 버전도 남긴다. 문서의 존재만으로 현재 코드에 적용된다고 단정하지 말고, 회원 종류나 호출 경로 같은 조건도 맞는지 확인한다.
이름이나 구조에 관한 선택 의견에는 실행 결과 대신 읽기 어려운 지점을 근거로 적을 수 있다. “변수 이름이 별로다”는 선호의 표현이지만 “두 권수 변수가 같은 범위에 있어 count만으로는 현재 대출 권수와 신청 권수를 구별하기 어렵다”는 코드에서 확인할 수 있는 설명이다. 모든 의견에 숫자나 측정 자료가 필요한 것은 아니다. 주장에 맞는 근거가 필요하다.
코드의 관찰 결과와 다음 행동을 적는다
“작성자가 규칙을 이해하지 못했다”는 말은 코드만 보고 확인할 수 없다. “이 조건은 세 권인 입력을 허용한다”는 말은 실행하거나 조건식을 읽어 확인할 수 있다. 리뷰 의견의 주어를 사람에서 메서드, 조건식, 반환값으로 옮기면 대화의 대상도 선명해진다.
요청은 반드시 완성된 수정 코드를 제공해야 한다는 뜻이 아니다. “L-1에 맞게 세 권부터 거절하도록 조건을 수정해 달라”처럼 필요한 결과를 적어도 충분하다. 구현 방법까지 지정해야 한다면 그 제약의 이유를 덧붙인다. 작성자가 다른 방법을 택했을 때도 같은 결과를 얻는지 함께 검토할 수 있어야 한다.
[필수] LoanService.message의 조건이 대출 규칙 L-1과 다르다. 입력 3에서 현재 결과는 “대출 가능”이고 기대 결과는 “대출 불가”다. 세 권부터 거절하도록 조건을 수정해 달라.
좋은 점도 같은 방식으로 적는다. “잘했다”는 반응은 격려가 될 수 있지만, 어떤 판단을 다음에도 유지하면 좋을지는 알려 주지 않는다. “대출 한도 판단을 한 메서드에 모아 안내 문구를 바꿀 위치가 분명하다”라고 쓰면 칭찬이 코드에 관한 정보가 된다. 칭찬을 수정 요청 앞에 의무적으로 붙일 필요는 없다. 실제로 관찰한 장점을 구체적으로 남긴다.
의견 데이터에서 찾을 수 있는 것과 없는 것
문장의 의미를 전부 판별하려 하면 간단한 규칙으로는 다루기 어렵다. 여기서는 의견을 기록할 때부터 대상, 메시지, 근거, 요청을 나누어 받는다. 변경 의견은 근거가 비어 있으면 “근거 누락”으로 표시한다. 변경 의견과 질문은 대상이나 요청이 비어 있으면 “모호함”으로 표시한다. 칭찬은 수정 요청을 요구하지 않는다.
이 규칙은 근거 없는 지적과 모호한 지적을 놓치지 않도록 돕는 작은 장치다. 비어 있지 않은 근거가 사실인지, 요청이 실제로 구체적인지는 판단하지 못한다. 근거에 “그냥 그렇다”라고 적으면 검사에 걸리지 않는다. 질문 형식을 빌려 결함을 단정하는 문장도 찾아내지 못한다. 표시가 없다는 결과를 좋은 의견이라는 판정으로 읽어서는 안 된다.
예제의 R1은 대상·근거·요청이 빠진 변경 의견이다. R2는 캐시 추가라는 요청은 있으나 느리다는 주장의 근거가 없다. R3은 규칙을 확인하는 질문이고, R4는 팀 규칙을 근거로 한 이름 변경 제안이다. R5는 유지할 만한 판단을 설명하는 칭찬이다. 실제 재현을 하지 않은 R2를 고칠 때는 그럴듯한 실행 수치를 만들어 넣지 않는다. 확인된 주장으로 범위를 좁히거나, 필요한 정보를 묻는 질문으로 바꾼다.
완성 코드
다음 내용을 Main.java로 저장한다. 대출 안내 메서드의 변경 전후를 비교하고, 다섯 개 의견의 표시 결과를 검사한다. 이어서 R1은 근거 있는 이름 변경 제안으로, R2는 실행 기록을 묻는 질문으로 고친다. 의견 데이터만 사용하며 파일이나 네트워크에 접근하지 않는다.
Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
enum Kind {
CHANGE, QUESTION, PRAISE
}
enum Priority {
REQUIRED, OPTIONAL, NONE
}
enum Flag {
MISSING_EVIDENCE, VAGUE
}
record Comment(
String id,
Kind kind,
Priority priority,
String target,
String message,
String evidence,
String request) {
}
private static int failures = 0;
// 변경 전: 두 분기에서 각각 반환한다.
static String beforeMessage(int count) {
if (count >= 3) {
return "대출 불가";
} else {
return "대출 가능";
}
}
// 변경 후: 같은 조건과 안내 문구를 유지한다.
static String afterMessage(int count) {
return count >= 3 ? "대출 불가" : "대출 가능";
}
static boolean blank(String value) {
return value == null || value.isBlank();
}
static List<Flag> inspect(Comment comment) {
List<Flag> flags = new ArrayList<>();
if (comment.kind() == Kind.CHANGE
&& blank(comment.evidence())) {
flags.add(Flag.MISSING_EVIDENCE);
}
if (comment.kind() != Kind.PRAISE
&& (blank(comment.target())
|| blank(comment.request()))) {
flags.add(Flag.VAGUE);
}
return List.copyOf(flags);
}
static String label(Flag flag) {
return switch (flag) {
case MISSING_EVIDENCE -> "근거 누락";
case VAGUE -> "모호함";
};
}
static String describe(List<Flag> flags) {
if (flags.isEmpty()) {
return "표시 없음";
}
List<String> labels = new ArrayList<>();
for (Flag flag : flags) {
labels.add(label(flag));
}
return String.join(", ", labels);
}
static void check(boolean condition, String name) {
System.out.println((condition ? "PASS " : "FAIL ") + name);
if (!condition) {
failures++;
}
}
public static void main(String[] args) {
System.out.println("대출 안내 변경 전후");
int[] inputs = {0, 2, 3, 4};
String[] expected = {
"대출 가능", "대출 가능", "대출 불가", "대출 불가"
};
for (int i = 0; i < inputs.length; i++) {
int count = inputs[i];
String before = beforeMessage(count);
String after = afterMessage(count);
System.out.println(
"입력 " + count + ": " + before + " / " + after);
check(before.equals(after)
&& expected[i].equals(after),
"동작 유지 " + count);
}
List<Comment> comments = List.of(
new Comment(
"R1", Kind.CHANGE, Priority.REQUIRED,
"", "이렇게 짜면 곤란하다. 다시 정리해 달라.",
"", ""),
new Comment(
"R2", Kind.CHANGE, Priority.OPTIONAL,
"LoanService.message", "이 코드는 느리다.",
"", "문자열 캐시를 추가해 달라."),
new Comment(
"R3", Kind.QUESTION, Priority.NONE,
"LoanService.message의 대출 한도",
"회원 종류에 따른 예외가 있는가.",
"", "예외 규칙이 있다면 문서 위치를 알려 달라."),
new Comment(
"R4", Kind.CHANGE, Priority.REQUIRED,
"LoanService.message의 매개변수 count",
"대출 권수를 나타내는 이름으로 바꿔 달라.",
"팀 명명 규칙 N-2: 공개 메서드의 권수 인자는 "
+ "무엇을 세는지 이름에 나타낸다.",
"activeLoanCount처럼 현재 대출 권수가 드러나게 바꿔 달라."),
new Comment(
"R5", Kind.PRAISE, Priority.NONE,
"LoanService.message",
"대출 한도 판단을 한곳에 모아 "
+ "안내 문구를 바꿀 위치가 분명하다.",
"", "")
);
System.out.println("리뷰 의견 점검");
List<List<Flag>> expectedFlags = List.of(
List.of(Flag.MISSING_EVIDENCE, Flag.VAGUE),
List.of(Flag.MISSING_EVIDENCE),
List.<Flag>of(),
List.<Flag>of(),
List.<Flag>of()
);
for (int i = 0; i < comments.size(); i++) {
Comment comment = comments.get(i);
List<Flag> flags = inspect(comment);
System.out.println(comment.id() + ": " + describe(flags));
check(flags.equals(expectedFlags.get(i)),
comment.id() + " 표시");
}
Comment revisedR1 = new Comment(
"R1", Kind.CHANGE, Priority.REQUIRED,
"LoanService.message의 매개변수 count",
"매개변수 이름에 대출 권수라는 뜻을 나타내 달라.",
"팀 명명 규칙 N-2는 공개 메서드의 권수 인자에 "
+ "대상을 나타내도록 정한다. 현재 이름은 count다.",
"activeLoanCount처럼 현재 대출 권수가 드러나게 바꿔 달라."
);
Comment revisedR2 = new Comment(
"R2", Kind.QUESTION, Priority.NONE,
"LoanService.message의 호출 기록",
"이 메서드가 지연 원인인지 확인할 실행 기록이 있는가.",
"",
"관련 기록이 있다면 입력 조건과 함께 공유해 달라."
);
System.out.println("의견 수정 후");
for (Comment comment : List.of(revisedR1, revisedR2)) {
List<Flag> flags = inspect(comment);
System.out.println(comment.id() + ": " + describe(flags));
check(flags.isEmpty(), comment.id() + " 수정 후 표시");
}
check(blank(null) && blank(" \t") && !blank("근거"),
"빈 항목 판별");
Comment vagueQuestion = new Comment(
"Q", Kind.QUESTION, Priority.NONE,
"", "왜 그런가.", "", ""
);
check(inspect(vagueQuestion).equals(List.of(Flag.VAGUE)),
"질문의 대상과 요청 확인");
Comment unsupportedButFilled = new Comment(
"X", Kind.CHANGE, Priority.OPTIONAL,
"LoanService.message", "구조를 바꾸자.",
"그냥 그렇다.", "메서드를 나누자."
);
check(inspect(unsupportedButFilled).isEmpty(),
"내용의 타당성은 자동 판정하지 않음");
System.out.println("실패 수: " + failures);
if (failures != 0) {
throw new IllegalStateException("검사 실패");
}
}
}
줄별 해설
Kind는 변경, 질문, 칭찬을 구분한다. Priority는 필수, 선택, 지정 없음을 표현한다. 변경 의견인 R1과 R4는 필수이고 R2는 선택이다. R3과 R5는 수정 요구가 없으므로 NONE이다. 이 프로그램은 필요도에 따라 검사 기준을 완화하지 않는다. 선택 의견에도 근거가 필요하기 때문이다.
Comment는 레코드(record)로 선언했다. 생성자의 인자 순서대로 식별자, 종류, 필요도, 대상, 메시지, 근거, 요청을 저장한다. comment.target()처럼 각 항목을 읽는다. 독자가 프로그램을 바꿀 때는 메시지 문장만 보지 말고 함께 저장한 항목도 확인해야 한다. 실제 리뷰 화면으로 옮긴다면 대상·근거·요청도 의견 본문에 드러나야 한다.
beforeMessage와 afterMessage는 이번 리뷰 대상 변경을 나타낸다. 앞 메서드는 두 분기에서 반환하고, 뒤 메서드는 조건 연산자로 반환한다. 비교 연산자와 문자열은 같다. 이름 변경 제안은 이 동작 정리와 별개의 의견이며, 이 프로그램에서 추가로 적용하지 않는다.
blank는 값이 없거나 공백으로만 이루어진 문자열을 빈 항목으로 본다. 논리합의 왼쪽이 참이면 오른쪽을 평가하지 않으므로 null에서 isBlank()를 호출하지 않는다. 이 처리는 문자열 항목에만 적용한다. 의견 객체 자체와 종류 값은 예제에서 정상적으로 만들어 전달한다는 전제가 있다.
inspect의 첫 조건은 변경 의견의 근거 누락을 찾는다. 두 번째 조건은 칭찬을 제외한 의견에서 대상 또는 요청의 누락을 찾는다. 두 조건을 독립적으로 실행하므로 R1에는 두 표시가 함께 붙는다. 반환할 때 목록을 복사하므로 호출자가 반환 목록에 표시를 추가하거나 제거할 수 없다.
label과 describe는 검사 결과를 출력 문구로 바꾼다. 표시를 추가하는 순서가 정해져 있어 R1은 항상 “근거 누락, 모호함”으로 출력된다. 의견 목록도 생성한 순서대로 순회한다. 따라서 실행 시각이나 외부 상태에 따라 출력 순서가 달라지지 않는다.
main의 첫 반복문은 변경 전후에 같은 입력을 전달한다. 두 결과가 같은지뿐 아니라 L-1에 맞는 기대 결과인지도 확인한다. 둘 다 같은 방식으로 틀렸을 때 단순 동등 비교만 통과하는 일을 피하기 위해서다. 다만 네 입력의 통과가 모든 입력에 관한 증명은 아니다. 여기서는 대출 권수의 정상 범위와 한도 주변을 표본으로 확인한다.
의견 검사에서는 expectedFlags와 실제 목록을 비교한다. “표시가 하나라도 있다”는 검사보다 어떤 표시가 붙었는지 분명하게 확인할 수 있다. 수정 후 R1은 팀의 명명 규칙을 근거로 제공한다. N-2 역시 이 예제를 위해 만든 합의다. 이런 합의가 없는 팀에서 이름에 관한 선호를 그대로 필수로 표시해서는 안 된다.
수정 후 R2는 느리다는 단정을 철회하고 확인할 자료를 묻는다. 근거 없는 주장을 그대로 둔 채 항목만 채운 것이 아니다. 마지막 unsupportedButFilled 검사는 이 구분을 다시 보여 준다. “그냥 그렇다”라는 내용도 문자열이 있으므로 검사를 통과한다. 검사 이름은 이 한계를 독자가 실행 결과에서 확인하도록 적었다.
check는 조건에 따라 PASS 또는 FAIL을 출력하고 실패를 누적한다. 끝에서 실패 수가 0이 아니면 예외를 발생시킨다. 예상대로 실행되면 모든 검사가 PASS이고 프로그램이 바로 끝난다.
실행 결과
JDK 17 환경에서 Main.java가 있는 디렉터리에서 다음 명령을 실행한다. 컴파일 결과는 out 디렉터리에 만들어진다. 예제 프로그램 자체는 파일을 쓰지 않는다.
javac -d out *.java
java -cp out Main
예상 출력은 다음과 같다. 같은 줄의 왼쪽 안내가 변경 전, 오른쪽 안내가 변경 후 결과다. 네 입력에서 안내가 같고 기대 결과와도 일치한다.
대출 안내 변경 전후
입력 0: 대출 가능 / 대출 가능
PASS 동작 유지 0
입력 2: 대출 가능 / 대출 가능
PASS 동작 유지 2
입력 3: 대출 불가 / 대출 불가
PASS 동작 유지 3
입력 4: 대출 불가 / 대출 불가
PASS 동작 유지 4
리뷰 의견 점검
R1: 근거 누락, 모호함
PASS R1 표시
R2: 근거 누락
PASS R2 표시
R3: 표시 없음
PASS R3 표시
R4: 표시 없음
PASS R4 표시
R5: 표시 없음
PASS R5 표시
의견 수정 후
R1: 표시 없음
PASS R1 수정 후 표시
R2: 표시 없음
PASS R2 수정 후 표시
PASS 빈 항목 판별
PASS 질문의 대상과 요청 확인
PASS 내용의 타당성은 자동 판정하지 않음
실패 수: 0
R4와 R5의 “표시 없음”은 같은 뜻의 평가가 아니다. R4는 변경 의견에 필요한 항목이 채워져 있고, R5는 칭찬이라 수정 요청 검사의 대상이 아니다. 결과를 읽을 때 의견의 종류를 함께 보아야 한다.
실무에서 자주 틀리는 것
다음 코드는 의견 문구를 출력하는 독립 예제다. 각 코드를 OpinionExample.java로 저장해 실행할 수 있다. 여기서 고치는 것은 의견의 전달 내용이므로 출력 문장은 의도적으로 달라진다. 앞서 확인한 대출 안내 메서드의 동작 비교와는 구별한다.
사람을 주어로 삼는다
틀린 코드는 작성자의 이해 수준을 추측한다. 이 말만으로는 어느 조건이 문제인지 확인할 수 없다.
class OpinionExample {
public static void main(String[] args) {
System.out.println("대출 규칙을 제대로 이해하지 못한 것 같다.");
}
}
고친 코드는 규칙을 어긴 변경을 확인했다는 상황에서 대상과 관찰 결과를 적는다. 실제로 아래 결과를 확인했을 때만 이 문구를 사용한다.
class OpinionExample {
public static void main(String[] args) {
System.out.println(
"[필수] LoanService.message에 3을 전달하면 "
+ "대출 가능이 나온다. L-1은 세 권부터 대출 불가로 정한다. "
+ "조건을 규칙에 맞게 수정해 달라.");
}
}
작성자의 성향이나 의도를 설명하지 않아도 필요한 수정은 전달할 수 있다. 의견을 읽은 사람이 같은 입력을 확인할 수 있다는 점이 핵심이다.
질문으로 필요한 수정을 흐린다
틀린 코드는 합의된 명명 규칙을 적용해야 하는 상황인데도 선택 여부를 다시 묻는다.
class OpinionExample {
public static void main(String[] args) {
System.out.println("count 이름을 바꿔 보면 어떨까요?");
}
}
고친 코드는 합의된 기준과 필요한 결과를 밝힌다. 구체적인 철자 하나까지 강제하지는 않는다.
class OpinionExample {
public static void main(String[] args) {
System.out.println(
"[필수] 팀 명명 규칙 N-2에 따라 count가 "
+ "현재 대출 권수임을 이름에 나타내 달라. "
+ "activeLoanCount를 제안한다.");
}
}
반대로 N-2 같은 합의가 없다면 필수라는 표시부터 재검토해야 한다. 의견의 강도는 검토자의 직급이나 확신에 맞추는 것이 아니다.
추측을 근거 항목에 넣어 검사를 통과시킨다
틀린 코드는 근거처럼 보이는 문장을 붙였지만 확인할 관찰이 없다. 문자열 항목이 채워져 있다는 사실과 주장이 뒷받침된다는 사실은 다르다.
class OpinionExample {
public static void main(String[] args) {
System.out.println(
"[선택] 호출이 많을 것 같으므로 문자열 캐시를 추가해 달라.");
}
}
고친 코드는 확인하지 못한 사실을 질문으로 남긴다. 기록이 없다는 답이 오면 그것만으로 캐시 추가를 요구하지 않는다.
class OpinionExample {
public static void main(String[] args) {
System.out.println(
"[질문] LoanService.message가 지연 원인인지 "
+ "확인할 실행 기록이 있는가. 있다면 입력 조건과 "
+ "함께 공유해 달라.");
}
}
질문으로 바꾼 뒤에도 원래의 캐시 추가 요청을 몰래 전제하지 않아야 한다. 답변을 통해 변경이 필요 없다는 결론에 이를 가능성도 열어 둔다.
칭찬을 평가 한마디로 끝낸다
틀린 코드는 긍정적인 반응이지만 어떤 코드를 유지하면 좋은지는 전달하지 않는다.
class OpinionExample {
public static void main(String[] args) {
System.out.println("잘 작성했다.");
}
}
고친 코드는 관찰한 판단과 그 효과를 연결한다.
class OpinionExample {
public static void main(String[] args) {
System.out.println(
"[칭찬] 대출 한도 판단을 LoanService.message에 모아 "
+ "안내 문구를 바꿀 위치가 분명하다.");
}
}
칭찬에도 코드를 가리키는 정보가 있으면 팀원이 비슷한 변경에서 참고할 수 있다. 다만 칭찬을 남기기 위해 확인하지 않은 효과까지 덧붙이지 않는다.
한눈에 보기
| 항목 | 확인할 질문 | 예제의 처리 |
|---|---|---|
| 대상 | 어느 코드에 관한 의견인지 알 수 있는가 | 변경·질문에서 비어 있으면 모호함 |
| 근거 | 주장을 다시 확인할 입력이나 설명이 있는가 | 변경 의견에서 비어 있으면 근거 누락 |
| 요청 | 작성자가 답하거나 바꿀 내용이 드러나는가 | 변경·질문에서 비어 있으면 모호함 |
| 필요도 | 필수인 이유가 합의나 확인 결과에 있는가 | 데이터로 저장하고 사람이 판단 |
| 칭찬 | 유지할 판단과 효과가 구체적인가 | 수정 요청 검사에서 제외 |
| 자동 표시 | 표시 없음과 타당함을 혼동하지 않는가 | 빈 항목만 찾는 한계를 검사로 명시 |
의견을 보내기 전에는 작성자가 추측해야 할 부분이 얼마나 남아 있는지 읽어 본다. 코드 위치를 찾고, 주장을 확인하고, 다음 행동을 결정할 수 있으면 대화가 시작될 준비가 된 것이다. 다음 장에서는 작성하는 쪽에서 변경 목적과 확인 결과를 어떻게 제공하면 이 대화를 돕는지 살펴본다.
연습 문제
- 팀에 별도의 명명 규칙이 없다고 가정한다. “count 이름이 별로다. 바꿔 달라”를 선택 의견으로 고쳐라. 대상, 읽기 어려운 이유, 제안을 포함하라.
- R3의 요청을 공백 문자열로 바꾸면 어떤 표시가 나오는지 예측하라. 근거 누락도 함께 표시되는지 설명하라.
- 변경 의견의 근거를 “다들 이렇게 한다”로 채웠다. 현재 프로그램이 이를 표시하는지 답하고, 사람이 추가로 확인할 내용을 두 가지 적어라.
- R5와 같은 칭찬에는 요청이 없어도 된다. 그러나 대상이 빠진 칭찬은 다시 읽도록 표시하고 싶다. inspect의 조건을 수정하고, 새 동작을 확인할 입력을 제시하라.
정답과 해설
-
“[선택] LoanService.message의 count는 선언만으로 무엇을 세는지 알기 어렵다. 현재 대출 권수를 뜻하므로 activeLoanCount로 바꾸는 것을 제안한다. 같은 뜻이 드러나는 다른 이름도 가능하다”라고 쓸 수 있다. 합의된 의무가 없으므로 필수로 표시하지 않는다. 이름에 관한 선호만 말하지 않고 읽는 사람이 겪는 어려움을 설명한다.
-
모호함만 표시된다. R3은 질문이므로 변경 의견의 근거 누락 검사에 들어가지 않는다. 요청이 공백이면 blank가 참을 반환하므로 두 번째 조건에 걸린다. 질문 본문에 의도가 어느 정도 드러나더라도 현재 규칙은 별도 요청 항목을 기준으로 판단한다. 이 차이도 사람이 결과를 검토해야 하는 이유다.
-
대상과 요청이 채워져 있다면 표시가 없다. 프로그램은 근거 문자열의 진위를 읽지 않는다. 사람은 실제로 합의된 팀 규칙인지, 그 규칙이 현재 코드와 조건에 적용되는지 확인해야 한다. “다들”이 누구인지 설명할 수 없거나 확인할 자료가 없다면 근거를 다시 쓰거나 선택 의견의 이유를 구체화해야 한다.
-
근거 검사 부분은 유지하고, 모호함 검사 부분을 아래처럼 바꿀 수 있다. 대상은 모든 종류에서 요구하고, 요청은 칭찬 이외의 종류에서만 요구한다. 다음은 기존 Main 클래스 안에서 사용할 수 있는 완전한 메서드다.
static List<Flag> inspect(Comment comment) { List<Flag> flags = new ArrayList<>(); if (comment.kind() == Kind.CHANGE && blank(comment.evidence())) { flags.add(Flag.MISSING_EVIDENCE); } if (blank(comment.target()) || (comment.kind() != Kind.PRAISE && blank(comment.request()))) { flags.add(Flag.VAGUE); } return List.copyOf(flags); }대상이 빈 문자열이고 요청도 빈 문자열인 칭찬을 넣으면 모호함이 표시되어야 한다. 대상이 “LoanService.message”이고 요청만 빈 칭찬에는 표시가 없어야 한다. 두 입력을 함께 확인해야 칭찬에도 요청을 요구하는 조건으로 잘못 바꾸지 않았는지 알 수 있다. 이 수정은 대상 항목의 존재를 확인할 뿐, 칭찬 내용이 구체적인지까지 보장하지 않는다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.