소프트웨어 공학 · 심화
코드 리뷰 실무 - 버그를 잡고 팀을 키우는 읽기
리뷰는 무엇을 위해 하나 - 결함·지식 공유·기준
리뷰의 목적 세 가지, 리뷰로 잘 잡히는 것과 안 잡히는 것, 테스트·정적 검사와 역할 나누기, 완성 코드는 결함 목록을 발견 수단(리뷰·테스트·정적 검사)별로 집계해 표로 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
대출 기능을 수정한 동료가 검토를 요청했다. 프로그램은 실행되고, 평소에 쓰던 입력으로는 원하는 결과가 나온다. 이때 “잘 돌아간다”라는 말만 남기면 검토를 마쳐도 되는가. 실행했다는 사실은 중요하지만, 어떤 입력을 넣었고 어떤 규칙을 확인했는지가 빠져 있다. 코드 리뷰(code review)는 변경한 코드가 팀이 의도한 일을 하는지 함께 확인하는 활동이다. 실행 결과뿐 아니라 그 결과를 만드는 판단과 근거도 검토한다.
이 장에서는 도서관 대출 가능 여부를 판단하는 작은 변경을 살펴본다. 복잡한 기술보다 리뷰의 목적과 확인 수단의 역할에 집중한다. 마지막 프로그램은 수정 전후의 결과를 비교하고, 발견한 결함을 리뷰·테스트·정적 검사별로 집계해 표로 출력한다.
- 리뷰의 목적을 결함 발견, 지식 공유, 기준 형성으로 구분한다.
- 리뷰가 잘 확인하는 대상과 실행이나 도구의 도움이 필요한 대상을 구별한다.
- 같은 입력으로 수정 전후를 실행하고, 유지된 결과와 바뀐 결과를 설명한다.
- 결함을 발견한 수단과 수정 결과를 검증한 수단을 구분해 기록한다.
문제 상황
도서관 대출 서비스에 대출 가능 여부를 알려 주는 메서드가 추가되었다. 화면에서는 이 메서드가 반환한 참과 거짓에 따라 대출 가능 안내를 표시한다. 변경 작성자는 반납 예정일이 아직 남아 있고 책도 있는 회원으로 실행해 보았다. 결과가 참이어서 검토를 요청했다.
팀이 합의한 업무 규칙은 다음과 같다. 대출 제한 상태인 회원은 새로 빌릴 수 없다. 남은 책이 없으면 대출할 수 없다. 기존 대출의 반납 예정일이 오늘이면 대출할 수 있고, 예정일이 지난 다음 날부터 연체로 취급한다. 여기서는 이 세 조건만 판단한다. 실제 대출 기록을 만들거나 재고를 변경하는 기능은 예제 범위에 포함하지 않는다.
그런데 수정 전 코드는 회원의 제한 상태와 남은 책 수를 받으면서도 사용하지 않는다. 날짜도 예정일 당일부터 대출을 막도록 작성되어 있다. 평범한 입력 하나만 실행하면 이 세 문제는 드러나지 않는다. 메서드가 받는 값이 충분해 보인다는 인상과 실제로 그 값을 판단에 반영한다는 사실은 다르다.
“제한 회원도 대출할 수 있는 것으로 읽힌다. 합의한 대출 규칙에는 제한 상태를 확인해야 한다고 되어 있다. 이 조건을 판단에 넣고, 제한 회원 입력으로 결과도 확인하면 좋겠다.”
이 의견에는 취향보다 업무 규칙이 먼저 등장한다. 작성자가 왜 틀렸는지를 평가하는 대신, 코드가 놓친 조건과 확인할 입력을 제시한다. 리뷰의 출발점은 동료의 실력을 판정하는 일이 아니라 변경의 결과를 함께 책임지는 일이다.
리뷰의 세 가지 목적
결함을 발견한다
결함은 합의한 동작과 실제 동작의 차이다. 제한 회원을 허용하는 코드처럼 조건이 빠진 경우도 있고, 반납 예정일을 해석하는 기준이 어긋난 경우도 있다. 실행 중 예외가 발생해야만 결함인 것은 아니다. 아무 오류 메시지 없이 잘못된 허용 결과를 돌려주는 코드도 업무 규칙을 위반한다.
리뷰에서는 “이 코드가 어떤 입력에서 잘못된 결정을 하는가”를 묻는다. 이번 예제라면 제한 상태가 참인 회원을 넣었을 때가 구체적인 반례다. 반례를 제시할 수 있으면 수정의 필요성을 설명하기 쉬워지고, 수정 후 확인할 대상도 분명해진다. 다만 사람이 코드를 읽었다는 이유만으로 모든 반례를 찾았다고 생각해서는 안 된다.
지식을 공유한다
날짜 비교식을 읽을 줄 알아도 반납 예정일 당일을 연체로 보는지는 코드만으로 확정하기 어렵다. 업무 규칙은 작성자에게 익숙하고 검토자에게는 생소할 수 있다. 리뷰 과정에서 “예정일 다음 날부터 연체”라는 근거를 확인하면, 한 사람의 기억이 팀의 공통 이해로 바뀐다.
지식 공유는 작성자만 설명하는 시간이 아니다. 검토자는 다른 기능에서 이미 합의한 규칙을 알려 줄 수 있고, 작성자는 해당 변경이 적용되는 범위를 설명할 수 있다. 이때 확인되지 않은 추측은 규칙처럼 남기지 않는다. 근거가 없다면 먼저 담당자가 확인해야 할 질문으로 기록한다.
팀의 기준을 만든다
같은 상황에서 사람마다 다른 결론을 내리면 다음 변경에서도 논쟁이 반복된다. 팀은 리뷰를 통해 “업무 규칙을 바꾸면 대표 입력과 결과를 함께 제시한다”처럼 반복해서 적용할 기준을 만들 수 있다. 기준은 검토자의 취향을 늘어놓은 목록보다, 판단에 필요한 증거를 정하는 약속에 가까워야 한다.
모든 의견이 결함 지적일 필요는 없다. 이해를 확인하는 질문, 선택 가능한 개선 제안, 반드시 고쳐야 할 규칙 위반은 성격이 다르다. 팀 기준이 있으면 작성자는 무엇을 해결해야 검토가 끝나는지 알 수 있다. 기준을 새로 정했다면 그 이유와 적용 범위도 함께 남겨야 한다.
| 목적 | 검토할 질문 | 남길 결과 |
|---|---|---|
| 결함 발견 | 제한 회원을 허용하는가 | 누락된 조건과 반례 |
| 지식 공유 | 언제부터 연체인가 | 예정일 다음 날이라는 근거 |
| 기준 형성 | 수정을 무엇으로 확인할 것인가 | 대표 입력과 기대 결과를 제시한다는 약속 |
리뷰와 실행과 정적 검사의 역할을 나눈다
리뷰는 코드와 업무 의도를 연결하는 데 유용하다. 입력으로 받은 제한 상태를 사용하지 않는 이유, 날짜 조건이 업무 설명과 다른 이유처럼 문맥이 필요한 문제를 확인하기 좋다. 반면 많은 입력 조합을 반복해서 실행하거나 특정 환경에서만 발생하는 현상을 재현하는 일은 읽기만으로 충분하지 않다.
테스트(test)는 준비한 입력에 대한 실제 결과를 기대 결과와 비교한다. 반납 예정일 당일에 허용되어야 한다는 기대를 명시하면 날짜 판단의 어긋남이 드러난다. 하지만 기대 결과 자체를 잘못 정했거나 제한 회원 입력을 준비하지 않았다면, 테스트가 통과해도 빠진 규칙은 남는다. 통과 결과는 검사한 조건에 대한 증거로 읽어야 한다.
정적 검사(static analysis)는 프로그램을 실행하지 않고 코드의 구조나 규칙 위반을 살핀다. 이 장에서는 JDK 컴파일러의 타입 관련 경고를 예로 든다. 타입 인자가 없는 목록을 구체적인 타입의 목록에 대입하는 코드는 컴파일될 수 있지만, 검사되지 않은 변환 경고가 발생할 수 있다. 도구는 이런 구조를 일정하게 확인하는 데 유용하다. 도서관의 연체 기준을 도구가 스스로 알아내는 것은 아니다.
같은 결함이 여러 수단에 걸릴 수도 있다. 제한 회원 누락은 리뷰에서 먼저 발견한 뒤 테스트로 재현할 수 있다. 그렇다고 이를 서로 다른 결함 두 개로 세면 실제보다 문제가 많아 보인다. 이 장의 집계에서는 각 결함의 최초 발견 수단을 하나만 기록한다. 수정 결과를 확인한 수단은 별도로 설명한다.
| 수단 | 잘 확인하는 것 | 단독으로 부족한 것 | 이번 예제의 역할 |
|---|---|---|---|
| 리뷰 | 의도와 구현의 불일치 | 많은 실행 조합의 반복 확인 | 제한 회원 조건 누락 발견 |
| 테스트 | 정해 둔 입력의 실제 결과 | 준비하지 않은 규칙과 입력 | 예정일 당일과 재고 없음 확인 |
| 정적 검사 | 타입 등 도구가 아는 구조적 문제 | 업무 규칙의 타당성 | 검사되지 않은 변환 경고 발견 |
검토자가 자동으로 확인할 수 있는 내용을 매번 손으로 반복하면, 정작 문맥이 필요한 질문에 쓸 시간이 줄어든다. 반대로 도구가 조용하다는 이유로 업무 규칙 확인을 생략하면 판단 조건의 누락이 남을 수 있다. 역할을 나눈다는 것은 담당 영역을 서로 넘기지 않는다는 뜻이 아니다. 서로 다른 증거를 모아 같은 변경을 확인한다는 뜻이다.
완성 코드
다음 프로그램은 검토 기록을 재현하는 독립 실행 예제다. BeforePolicy가 검토 대상의 기존 동작이고, AfterPolicy가 고친 동작이다. 제한 회원, 예정일 당일, 재고 없음의 세 입력에서 기존 결함을 재현한다. 정상 입력과 예정일이 지난 입력에서는 수정 전후 결과가 유지되는지도 확인한다. 따라서 “고친 뒤에도 같은가”라는 질문을 모든 입력에 일괄 적용하지 않는다. 정상 동작은 유지하고 잘못된 동작은 기대 결과로 바뀌어야 한다.
정적 검사 항목은 앞서 발견해 수정한 경고의 기록이다. 실행 중 정적 검사 도구를 호출하거나 새 경고를 탐지하는 기능은 없다. 완성 코드의 labels는 타입 인자를 지정한 수정 결과다. 검토 과정에서 최초 발견 수단이 정해졌다고 가정하고, 그 기록을 findings에 담아 집계한다.
Main.java
import java.time.LocalDate;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class Main {
enum Discovery {
REVIEW("리뷰"),
TEST("테스트"),
STATIC_ANALYSIS("정적 검사");
private final String label;
Discovery(String label) {
this.label = label;
}
String label() {
return label;
}
}
record Finding(String name, Discovery discovery) {
}
record Request(
boolean blocked,
int availableCopies,
LocalDate dueDate) {
}
static final class BeforePolicy {
boolean canBorrow(Request request, LocalDate today) {
return today.isBefore(request.dueDate());
}
}
static final class AfterPolicy {
boolean canBorrow(Request request, LocalDate today) {
return !request.blocked()
&& request.availableCopies() > 0
&& !today.isAfter(request.dueDate());
}
}
private static int passed;
private static int failed;
private static void check(boolean condition, String name) {
if (condition) {
passed++;
System.out.println("PASS " + name);
} else {
failed++;
System.out.println("FAIL " + name);
}
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2030, 4, 10);
BeforePolicy before = new BeforePolicy();
AfterPolicy after = new AfterPolicy();
Request normal = new Request(false, 1, today.plusDays(1));
Request overdue = new Request(false, 1, today.minusDays(1));
Request blocked = new Request(true, 1, today.plusDays(1));
Request dueToday = new Request(false, 1, today);
Request noCopies = new Request(false, 0, today.plusDays(1));
boolean normalBefore = before.canBorrow(normal, today);
boolean normalAfter = after.canBorrow(normal, today);
boolean overdueBefore = before.canBorrow(overdue, today);
boolean overdueAfter = after.canBorrow(overdue, today);
boolean blockedBefore = before.canBorrow(blocked, today);
boolean blockedAfter = after.canBorrow(blocked, today);
boolean dueBefore = before.canBorrow(dueToday, today);
boolean dueAfter = after.canBorrow(dueToday, today);
boolean copiesBefore = before.canBorrow(noCopies, today);
boolean copiesAfter = after.canBorrow(noCopies, today);
System.out.println("수정 전후 비교");
System.out.println("정상: " + normalBefore + " -> " + normalAfter);
System.out.println("연체: " + overdueBefore + " -> " + overdueAfter);
System.out.println("제한 회원: " + blockedBefore + " -> " + blockedAfter);
System.out.println("예정일 당일: " + dueBefore + " -> " + dueAfter);
System.out.println("재고 없음: " + copiesBefore + " -> " + copiesAfter);
check(normalBefore && normalAfter, "정상 입력 허용 유지");
check(!overdueBefore && !overdueAfter, "연체 입력 거절 유지");
check(blockedBefore && !blockedAfter, "제한 회원 결함 재현과 수정");
check(!dueBefore && dueAfter, "예정일 당일 결함 재현과 수정");
check(copiesBefore && !copiesAfter, "재고 없음 결함 재현과 수정");
List<String> labels = List.of("대출");
check("대출".equals(labels.get(0)), "타입을 지정한 목록 사용");
List<Finding> findings = List.of(
new Finding("제한 회원 조건 누락", Discovery.REVIEW),
new Finding("예정일 당일 거절", Discovery.TEST),
new Finding("재고 확인 누락", Discovery.TEST),
new Finding("목록의 검사되지 않은 변환",
Discovery.STATIC_ANALYSIS));
Map<Discovery, Integer> counts = new EnumMap<>(Discovery.class);
for (Discovery discovery : Discovery.values()) {
counts.put(discovery, 0);
}
for (Finding finding : findings) {
Discovery discovery = finding.discovery();
counts.put(discovery, counts.get(discovery) + 1);
}
int total = 0;
for (Discovery discovery : Discovery.values()) {
total += counts.get(discovery);
}
check(counts.get(Discovery.REVIEW) == 1, "리뷰 집계");
check(counts.get(Discovery.TEST) == 2, "테스트 집계");
check(counts.get(Discovery.STATIC_ANALYSIS) == 1, "정적 검사 집계");
check(total == findings.size(), "집계 합계");
System.out.println("최초 발견 수단 | 결함 수");
System.out.println("-----------------------");
for (Discovery discovery : Discovery.values()) {
System.out.println(discovery.label() + " | " + counts.get(discovery));
}
System.out.println("합계 | " + total);
System.out.println("검사 결과: PASS " + passed + ", FAIL " + failed);
if (failed > 0) {
throw new AssertionError("실패한 검사가 있다.");
}
}
}
줄별 해설
import 선언은 날짜, 목록, 열거형 키를 사용하는 맵을 준비한다. 외부 라이브러리는 사용하지 않는다. LocalDate는 날짜만 표현하므로 이 예제의 일 단위 규칙과 맞는다. 현재 날짜를 직접 읽는 대신 main에서 고정 날짜를 만들어 전달한다. 실행하는 날이 달라져도 입력과 결과가 바뀌지 않는다.
Discovery 선언은 최초 발견 수단을 세 값으로 제한한다. 화면에 출력할 한국어 이름은 label 필드에 둔다. 집계할 때 문자열을 직접 입력하지 않으므로 “정적검사”와 “정적 검사”처럼 표기가 달라져 항목이 나뉘는 일을 피한다. 출력 순서는 열거형 선언 순서로 고정한다.
Finding과 Request 선언은 각각 발견 기록과 판단 입력을 담는다. 레코드(record)는 여기서 작은 데이터 묶음을 표현한다. Finding의 name은 어떤 결함인지 설명하고 discovery는 집계 기준을 담는다. 이름을 집계에 직접 사용하지 않아도, 결함 목록의 내용을 검토하려면 이름이 필요하다.
BeforePolicy의 반환문은 오늘이 예정일보다 앞서는지만 확인한다. 제한 상태와 재고를 받았다는 사실은 코드에 드러나지만, 그 값을 읽는 식은 없다. 예정일 당일에는 isBefore가 거짓이므로 대출이 거절된다. 이 코드만 보고 작성자가 어떤 업무 규칙을 의도했는지 단정하지 말고 합의한 규칙과 비교해야 한다.
AfterPolicy의 반환문은 세 조건을 함께 요구한다. 제한 상태가 아니고, 남은 책이 양수이며, 오늘이 예정일을 지나지 않아야 한다. 예제에서는 요청과 날짜가 유효한 값으로 주어진다는 전제를 둔다. 이 메서드가 실제 서비스 전체의 모든 조건을 검증한다고 해석해서는 안 된다.
check 함수는 조건이 참이면 PASS를 출력하고 통과 수를 늘린다. 거짓이면 FAIL을 출력하고 실패 수를 늘린다. 검사를 끝까지 수행한 뒤 실패가 있으면 AssertionError를 던진다. 이 방식은 실행 마지막에 실패를 알아볼 수 있게 하면서 여러 검사의 결과도 함께 보여 준다.
다섯 Request 생성문은 비교할 입력을 명시한다. 정상과 연체는 기존에 맞던 결과를 유지하는 확인용 입력이다. 제한 회원, 예정일 당일, 재고 없음은 각각 누락되거나 어긋난 판단을 드러내는 입력이다. 한 입력에서 여러 조건을 동시에 바꾸지 않아 결과가 달라진 이유를 설명하기 쉽다.
수정 전후 결과를 저장하는 부분에서는 동일한 Request와 today를 두 정책에 전달한다. 입력이 같아야 구현 변경에 따른 차이를 비교할 수 있다. 이후 출력문은 관찰한 결과를 보여 주고, check 호출은 그 결과가 기대한 값인지 판정한다. 비교 출력만으로 검증을 대신하지 않는 이유다.
labels 선언은 목록의 원소 타입을 String으로 지정한다. 그다음 check는 원소를 읽은 결과를 확인할 뿐, 정적 검사 경고가 없는지 검사하지는 않는다. 경고 유무는 컴파일 단계에서 확인해야 한다. 실행 검증과 컴파일 검증은 서로 다른 증거다.
findings 생성문은 검토 과정에서 확인한 네 결함을 한 번씩 기록한다. 여기서는 각 항목이 서로 다른 결함이라는 점을 사람이 먼저 확인했다. 프로그램이 이름을 분석해 같은 결함을 자동으로 합치지는 않는다. 실제 기록을 가져온다면 집계 전에 결함의 중복 여부를 정리해야 한다.
counts를 채우는 두 반복문은 모든 수단을 0으로 준비한 다음 해당 수단의 수를 늘린다. 발견 항목이 없는 수단도 표에 표시하기 위한 구조다. 마지막 반복문은 열거형 순서로 출력하므로 맵의 순회 방식에 결과 순서를 맡기지 않는다. 집계 검사는 기대 개수와 합계를 확인하지만 발견 기록의 타당성까지 입증하지는 않는다.
실행 결과
Main.java만 둔 디렉터리에서 다음 두 명령을 차례로 실행한다. 아래 명령과 출력은 실행 안내이므로 Java 소스와 구분해 제시한다.
javac -d out *.java
java -cp out Main
예상 표준 출력은 다음과 같다. 정상과 연체는 결과가 같고, 나머지 세 입력은 잘못된 결과에서 기대 결과로 바뀐다.
수정 전후 비교
정상: true -> true
연체: false -> false
제한 회원: true -> false
예정일 당일: false -> true
재고 없음: true -> false
PASS 정상 입력 허용 유지
PASS 연체 입력 거절 유지
PASS 제한 회원 결함 재현과 수정
PASS 예정일 당일 결함 재현과 수정
PASS 재고 없음 결함 재현과 수정
PASS 타입을 지정한 목록 사용
PASS 리뷰 집계
PASS 테스트 집계
PASS 정적 검사 집계
PASS 집계 합계
최초 발견 수단 | 결함 수
-----------------------
리뷰 | 1
테스트 | 2
정적 검사 | 1
합계 | 4
검사 결과: PASS 10, FAIL 0
타입 관련 경고를 더 엄격하게 확인하려면 컴파일 명령을 다음과 같이 바꿀 수 있다. 완성 코드는 이 명령에서도 경고 없이 컴파일되도록 작성했다. -Xlint:all은 추가 경고를 활성화하고, -Werror는 경고가 있으면 컴파일을 실패하게 한다.
javac -Xlint:all -Werror -d out *.java
표에서 테스트가 두 건이라고 해서 테스트가 리뷰보다 두 배 유용하다는 결론을 내릴 수는 없다. 예제의 결함 구성과 발견 순서가 만든 숫자다. 제한 회원 조건도 테스트로 확인했고, 재고 누락도 리뷰에서 발견할 수 있었다. 이 표의 목적은 수단의 우열을 정하는 것이 아니라 어떤 경로로 문제를 알게 되었는지 정리하는 데 있다.
실무에서 자주 틀리는 것
다음 코드는 각 블록을 별도 파일로 저장해 실행할 수 있는 짧은 예다. 완성 프로그램과 함께 컴파일하지 않고 각각 살펴본다. 틀린 코드와 고친 코드에 같은 입력을 사용해 결과의 차이를 확인한다.
출력만 보고 기대 결과를 확인했다고 생각한다
틀린 코드는 예정일 당일이라는 입력에서 거짓을 출력한다. 하지만 실행이 끝났다는 사실만으로는 그 거짓이 맞는지 틀린지 드러나지 않는다. 검토자가 기대 결과를 따로 알고 있어야 문제를 알아볼 수 있다.
import java.time.LocalDate;
class PrintOnly {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2030, 4, 10);
LocalDate dueDate = LocalDate.of(2030, 4, 10);
boolean allowed = today.isBefore(dueDate);
System.out.println(allowed);
}
}
고친 코드는 같은 날짜를 넣고, 업무 규칙에 맞게 당일을 허용한다. check는 기대한 참이 나왔는지를 확인한다. 앞 코드는 false를, 뒤 코드는 PASS 예정일 당일을 출력한다. 이렇게 결과 변경이 의도한 수정인지 설명할 수 있어야 한다.
import java.time.LocalDate;
class CheckExpected {
static void check(boolean condition, String name) {
System.out.println((condition ? "PASS " : "FAIL ") + name);
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2030, 4, 10);
LocalDate dueDate = LocalDate.of(2030, 4, 10);
boolean allowed = !today.isAfter(dueDate);
check(allowed, "예정일 당일");
}
}
같은 결함을 확인 수단 수만큼 센다
제한 회원 누락을 리뷰에서 발견하고 테스트로 재현했다고 하자. 틀린 코드는 두 확인 활동을 결함 두 건으로 센다. 이런 기록은 검증을 더 충실히 할수록 결함 수가 늘어나는 모양을 만든다.
class CountActivities {
public static void main(String[] args) {
boolean foundByReview = true;
boolean reproducedByTest = true;
int defects = 0;
if (foundByReview) {
defects++;
}
if (reproducedByTest) {
defects++;
}
System.out.println("결함 수: " + defects);
}
}
고친 코드는 같은 두 사실을 전달받되 결함의 존재는 한 번만 센다. 두 프로그램의 출력은 각각 결함 수: 2와 결함 수: 1이다. 발견 활동의 수가 필요하다면 별도 항목으로 기록해야 한다. 서로 다른 질문을 하나의 숫자에 섞지 않는 것이 중요하다.
class CountOneDefect {
public static void main(String[] args) {
boolean foundByReview = true;
boolean reproducedByTest = true;
int defects = (foundByReview || reproducedByTest) ? 1 : 0;
System.out.println("결함 수: " + defects);
}
}
실행 결과가 같으면 컴파일 경고도 괜찮다고 생각한다
다음 틀린 코드는 타입 인자가 없는 List를 List<String>에 대입한다. 원소가 문자열이라 이번 실행은 성공하지만, 컴파일러는 검사되지 않은 변환을 경고한다. 앞서 집계한 정적 검사 항목이 이런 종류의 문제다. 이 블록은 경고를 관찰하기 위한 의도적인 잘못된 예다.
import java.util.List;
class RawLabels {
public static void main(String[] args) {
List raw = List.of("대출");
List<String> labels = raw;
System.out.println(labels.get(0));
}
}
고친 코드는 처음부터 원소 타입을 지정한다. 두 프로그램은 같은 문자열 대출을 출력한다. 여기서는 실행 출력의 변경이 수정 목표가 아니다. 같은 결과를 유지하면서 타입 검사를 우회하는 대입을 없애는 것이 목표다. 실행 확인만으로 이 수정의 효과를 판정할 수 없다는 점을 보여 준다.
import java.util.List;
class TypedLabels {
public static void main(String[] args) {
List<String> labels = List.of("대출");
System.out.println(labels.get(0));
}
}
한눈에 보기
| 확인할 것 | 이번 장의 예 | 남길 증거 |
|---|---|---|
| 업무 규칙과 구현의 일치 | 제한 회원은 대출 불가 | 조건 추가와 재현 입력 |
| 공유해야 할 판단 근거 | 예정일 다음 날부터 연체 | 당일 허용의 이유 |
| 반복해서 적용할 팀 기준 | 변경 결과를 실행으로 제시 | 입력·기대값·실제값 |
| 수단별 역할과 한계 | 타입 경고는 컴파일로 확인 | 경고 없는 컴파일 결과 |
| 집계 기준 | 최초 발견 수단으로 분류 | 중복을 정리한 결함 목록 |
리뷰를 마쳤다는 말은 살펴보지 않은 범위까지 안전하다고 보증하는 말이 아니다. 어떤 규칙을 확인했고 어떤 입력을 실행했으며 어떤 검사를 통과했는지를 구체적으로 말하는 편이 낫다. 이런 기록은 다음 사람이 변경을 이해할 때도 도움이 된다. 이어지는 내용에서는 그 확인을 시작하기 위해 변경 부분을 어떤 순서로 읽을지 살펴본다.
연습 문제
- 대출 가능 결과는 업무 규칙에 맞지만, 검토자가 예정일 당일을 허용하는 이유를 몰랐다. 리뷰에서 그 이유를 확인해 기록했다. 세 가지 목적 가운데 어떤 목적을 직접 달성했는지 설명하라.
- 제한 회원 조건 누락을 리뷰에서 먼저 발견했다. 이후 같은 결함을 테스트 두 개로 재현하고 수정 후 모두 통과했다. 이 장의 기준으로 결함 수와 최초 발견 수단을 적고, 테스트가 제공한 증거를 설명하라.
- 정상 입력의 수정 전후 결과가 모두 참이었다. 이 사실만으로 변경을 승인하기 부족한 이유를 예제의 다른 입력 두 개를 들어 설명하라.
- 완성 코드의 findings에 리뷰에서 발견한 서로 다른 결함을 한 건 추가한다고 하자. 집계 검사에서 바꿔야 할 값과 최종 표의 각 수치를 적어라. 이 변경만으로 정책 코드가 새 결함을 재현하거나 수정하는지도 설명하라.
정답과 해설
- 직접 달성한 목적은 지식 공유다. 검토자가 몰랐던 업무 규칙의 근거를 알게 되었기 때문이다. 결과가 이미 맞았으므로 이 사실만으로 결함을 발견했다고 하지는 않는다. 당일 허용 여부를 대표 입력으로 확인한다는 약속까지 정했다면 기준 형성에도 이어진다.
- 결함 수는 한 건이고 최초 발견 수단은 리뷰다. 두 테스트는 같은 결함을 서로 다른 입력으로 재현한 확인 활동이다. 수정 후 통과 결과는 그 두 입력에서 기대 동작이 나온다는 증거다. 준비하지 않은 다른 입력까지 모두 맞는다는 증거는 아니다.
- 정상 입력은 제한 상태가 거짓이고 재고가 있으며 예정일도 남아 있다. 따라서 제한 상태나 재고 판단이 빠져 있어도 참이 나온다. 제한 회원 입력에서는 거절해야 하고, 재고가 없는 입력에서도 거절해야 한다. 정상 입력의 결과 유지와 빠진 조건의 수정은 각각 확인해야 한다.
- 리뷰 집계의 기대값은 1에서 2로 바뀐다. 테스트는 2, 정적 검사는 1이며 합계는 5가 된다. 합계 검사는 findings.size()와 비교하므로 직접 고칠 숫자가 없다. 기록 추가만으로 정책 코드의 동작은 바뀌지 않는다. 새 결함을 재현하는 입력과 기대 결과, 필요한 정책 수정은 따로 마련해야 한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.