Devin.KR

주석 - 써야 할 것과 지워야 할 것

개발자KR 조회 5

이 장에서 배우는 것

주석이 많다고 코드가 잘 읽히는 것은 아니다. 코드와 같은 말을 반복하는 주석은 읽을 분량을 늘리고, 코드와 다른 말을 하는 주석은 판단을 흐린다. 그렇다고 주석을 모두 지우면 계산 규칙을 선택한 이유나 외부에서 정한 제약까지 사라질 수 있다. 이 장에서는 코드가 설명할 내용과 주석이 보존할 내용을 구분한다.

앞 장에서 조건문의 구조를 정리했다면, 이번에는 그 주변에 남은 설명을 살펴본다. 학원 수강료 계산에 붙은 오래된 설명을 이름과 함수로 옮기고, 계산 결과가 유지되는지 실행으로 확인한다. 마지막에는 주석 처리된 코드와 검토 기한이 지난 주석을 찾는 작은 검사기를 만든다.

  • 동작을 반복하는 설명과 이유를 남기는 설명을 구분한다.
  • 낡은 주석이 코드 이해와 수정에 어떤 영향을 주는지 설명한다.
  • 주석에 숨어 있는 개념을 이름과 함수로 드러낸다.
  • 외부에 공개하는 계약에 필요한 Javadoc을 작성한다.
  • 자동 검사로 찾을 수 있는 흔적과 사람이 판단할 내용을 구분한다.

문제 상황

학원 수강 관리 프로그램에서 수강료 안내 문구를 고치는 일을 맡았다고 하자. 계산 메서드 위에는 “할인은 적용하지 않는다”라는 주석이 있다. 그런데 메서드 안에는 장기 수강생에게 일정 금액을 빼 주는 조건문이 있다. 화면에는 이미 할인된 금액이 표시되고 있다. 주석을 믿고 조건문을 지우면 사용자가 내던 금액이 달라지고, 코드를 믿고 주석을 지우면 규칙의 변경 배경을 놓칠 수 있다.

옆에는 예전에 사용한 계산식도 주석으로 남아 있다. 그 식에는 지금과 다른 할인액이 들어 있다. 작성자와 작성 시점을 알 수 없으므로 임시 보관인지, 되돌릴 후보인지, 설명용 예시인지 구분하기 어렵다. 읽는 사람은 실행되는 계산과 실행되지 않는 계산을 함께 해석해야 한다.

// 할인은 적용하지 않는다.
// int total = months * 100_000 - 5_000;
int total = months * 100_000;
if (months >= 3) {
    total -= 10_000;
}

이 장의 금액과 개월 수는 설명을 위한 가상 규칙이다. 수강 기간은 1개월 이상이고, 월 수강료는 100,000원이며, 3개월 이상이면 총액에서 한 번만 10,000원을 공제한다고 정한다. 프로그램은 이 규칙을 유지해야 한다. “할인 없음”이라는 문장에 맞추어 동작을 바꾸는 것이 이번 작업의 목적은 아니다.

먼저 실제 계산과 확인 가능한 규칙을 대조한다. 현재 규칙과 다르다고 확인된 문장은 지운다. 실행되지 않는 옛 계산식은 변경 이력에서 찾을 수 있도록 관리하고 소스에서는 제거한다. 다음으로 숫자와 조건이 뜻하는 바를 이름에 옮긴다. 마지막으로 계산식만으로 알아낼 수 없는 근거가 있는지 살핀다.

순서가 중요한 이유는 주석도 검증 대상이기 때문이다. 주석을 정답으로 놓으면 잘못된 설명에 코드를 맞추게 된다. 반대로 코드만 정답으로 놓으면 이미 잘못 구현된 동작을 굳힐 수 있다. 이 장에서는 위에서 정한 가상 규칙을 기준으로 삼고, 수정 전후의 출력도 함께 비교한다.

코드가 말할 수 있는 것과 주석이 맡을 것

코드는 실제로 무엇을 수행하는지 보여 준다. 적절한 이름은 그 수행에 쓰이는 개념을 드러낸다. 그러나 여러 계산 방식 중 왜 하나를 선택했는지, 어떤 외부 합의 때문에 불편한 처리를 유지하는지는 코드만으로 충분히 설명하기 어렵다. 주석은 이런 빈자리를 맡는다.

설명의 종류에 따라 둘 자리를 고른다
설명하려는 내용우선 둘 자리수강료 예시
값의 의미와 단위변수·상수 이름MONTHLY_FEE_WON
판단하는 개념함수 이름qualifiesForDiscount
선택의 이유와 제약관련 코드 가까운 주석신청서에 합의된 공제 방식
호출자가 알아야 할 계약Javadoc허용 개월 수와 반환 금액의 단위

“개월 수에 월 수강료를 곱한다”는 설명은 곱셈을 읽을 수 있는 독자에게 새 정보를 주지 않는다. 반면 “신청서에 총액에서 한 번 공제한다고 명시했으므로 월별로 공제하지 않는다”는 설명은 계산식의 선택 근거를 알려 준다. 이 근거가 실제로 확인된 내용이라면 주석으로 남길 가치가 있다.

좋은 주석이라고 해서 길어야 하는 것은 아니다. 무엇이 제약인지, 어디에서 확인했는지, 어느 코드에 영향을 주는지가 드러나면 된다. 근거 문서를 관리하고 있다면 문서의 식별자를 적을 수 있다. 다만 존재하지 않는 문서 번호를 만들어 설명을 권위 있어 보이게 해서는 안 된다. 완성 코드에서는 예제에 주어진 가상 신청서 규칙을 근거로 사용한다.

동작과 개념은 코드에 드러내고 선택의 근거는 주석에 남긴다

주석을 이름으로 옮기는 과정에서도 판단이 필요하다. 길고 모호한 이름으로 문장 전체를 밀어 넣으면 읽기 쉬워지지 않는다. “장기 수강 할인 대상인가”처럼 실제로 반복해서 쓰는 개념을 이름으로 잡는다. 함수로 분리하는 목적은 주석을 한 줄 줄이는 데 있지 않다. 읽는 사람이 조건식을 해석하기 전에 그 조건의 의미를 알게 하는 데 있다.

다음 두 코드는 같은 조건을 표현한다. 뒤의 코드는 조건의 의미를 이름으로 전달하므로 설명 주석이 필요하지 않다. 실제 숫자는 이름이 붙은 기준값으로 남겨 규칙을 확인할 수 있게 한다.

// 장기 수강 할인 대상인지 확인한다.
if (months >= 3) {
    total -= 10_000;
}

// 이름으로 판단의 의미를 드러낸다.
if (qualifiesForDiscount(months)) {
    total -= LONG_TERM_DISCOUNT_WON;
}

낡은 주석은 단순히 쓸모없는 문장이 아니다. 코드를 수정하려는 사람에게 서로 다른 설명을 동시에 제시한다. 어느 쪽이 맞는지 조사하는 시간이 들고, 조사 없이 선택하면 동작이 달라질 수 있다. 따라서 관련 코드를 변경할 때는 주석도 함께 읽는다. 지금도 사실인지, 코드로 옮길 수 있는지, 근거가 남아 있는지 확인한다.

주석 처리된 코드 역시 보관 목적을 다시 살핀다. 다시 사용할 가능성만으로 소스에 남겨 두면 그 코드가 현재 자료 구조와 규칙에도 맞는지는 알 수 없다. 변경 이력으로 보존할 수 있는 구현은 소스에서 지우는 편이 읽는 부담을 줄인다. 다만 문서 안에서 사용법을 보여 주는 예제까지 모두 죽은 코드로 취급하지는 않는다. 위치와 목적을 함께 판단해야 한다.

Javadoc과 자동 검사의 역할

Javadoc은 선언에 붙여 호출자가 알아야 할 계약을 설명하는 문서 주석이다. 다른 클래스가 사용하는 공개 메서드라면 이름과 자료형만으로 드러나지 않는 입력 범위, 반환값의 의미, 단위, 중요한 제약을 적는다. 모든 선언에 형식적으로 붙이는 것보다 실제 사용 판단에 필요한 내용을 유지하는 편이 낫다.

수강료 계산 메서드에는 개월 수의 허용 범위와 반환 금액의 단위가 필요하다. 이 장의 예제는 정수 곱셈이 넘치지 않도록 입력을 1부터 12까지로 제한한다. 이 범위 역시 예제를 위한 계약이다. 구현 안의 지역 변수마다 같은 설명을 반복할 필요는 없다.

/**
 * 가상 수강 규칙에 따른 총 납부액을 계산한다.
 *
 * @param months 수강 개월 수, 1 이상 12 이하
 * @return 할인을 반영한 총 납부액, 원 단위
 * @throws IllegalArgumentException 개월 수가 허용 범위를 벗어난 경우
 */
public static int calculateFee(int months) {
    // 구현은 완성 코드에 있다.
    return 0;
}

위 조각의 반환문은 설명 위치를 보여 주기 위한 자리 표시다. 실행할 전체 구현은 뒤에서 제공한다. 계약에는 호출자가 의존할 내용을 적고, 내부 계산 순서처럼 바뀌어도 호출 방법에 영향이 없는 내용은 넣지 않는다. 공개 여부만으로 문서의 양을 결정하지 말고 실제 사용 경계와 독자를 기준으로 삼는다.

자동 검사기는 문장의 진실 여부를 직접 판단할 수 없다. “할인 없음”이 낡았는지 알려면 현재 규칙을 알아야 한다. 여기서는 사람이 검토할 후보를 두 가지 흔적으로 좁힌다. 첫째는 코드처럼 생긴 한 줄 주석이다. 둘째는 명시적으로 적은 검토 기한이 지난 주석이다.

검토 기한 표기는 이 예제에서 정한 팀 규칙이다. // @review-before 2026-06-01 공제 근거 재확인처럼 작성한다. 검사 기준일이 해당 날짜에 도달하면 검토 대상으로 보고한다. 날짜가 지났다는 사실은 설명이 틀렸다는 증거가 아니다. 근거를 다시 확인할 시점이 왔다는 뜻이다. 확인 없이 날짜만 뒤로 미루면 검사 결과의 의미가 사라진다.

검사기는 의심 흔적을 찾고 사람은 근거를 확인해 주석을 고친다

완성할 검사기는 줄의 앞 공백을 제거한 뒤 //로 시작하는 줄만 읽는다. 문자열 안의 주석 기호와 코드 뒤에 붙은 주석은 검사하지 않는다. 블록 주석과 Javadoc도 대상에서 제외한다. Java 문법 전체를 분석하는 도구가 아니라 범위를 제한한 연습용 검사기이기 때문이다.

코드 후보는 세미콜론으로 끝나는 몇 가지 형태만 찾는다. 정수 변수 선언, 대입, 반환문을 대상으로 한다. 설명 문장에 비슷한 형태가 있으면 잘못 보고할 수 있고, 다른 형태의 주석 처리 코드는 놓칠 수 있다. 이 한계를 숨기지 않고 검사 결과를 “후보”라고 부른다. 기계가 빠르게 위치를 찾고 사람이 맥락을 확인하는 역할 분담이다.

완성 코드

파일 하나에 계산 전후 구현, 주석 검사기, 작은 확인 함수를 함께 넣는다. 검사할 소스는 문자열로 고정하고 기준일도 고정값으로 전달한다. 파일을 읽거나 쓰지 않으므로 실행 위치와 현재 날짜에 따라 출력이 달라지지 않는다. 각 확인 결과를 출력한 뒤 실패가 있으면 실행을 실패로 끝낸다.

Main.java

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    private static final int MONTHLY_FEE_WON = 100_000;
    private static final int DISCOUNT_MIN_MONTHS = 3;
    private static final int LONG_TERM_DISCOUNT_WON = 10_000;

    private static final Pattern CODE_CANDIDATE = Pattern.compile(
            "(?:int\\s+[A-Za-z_$][\\w$]*\\s*=.+"
                    + "|[A-Za-z_$][\\w$]*\\s*=(?!=).+"
                    + "|return\\s+.+);");
    private static final Pattern REVIEW_MARKER = Pattern.compile(
            "^@review-before\\s+(\\d{4}-\\d{2}-\\d{2})(?:\\s+.*)?$");

    private static int failures;

    public static void main(String[] args) {
        int[] inputs = {1, 2, 3, 4, 12};
        int[] expectedFees = {100_000, 200_000, 290_000, 390_000, 1_190_000};

        for (int i = 0; i < inputs.length; i++) {
            int months = inputs[i];
            int before = legacyFee(months);
            int after = calculateFee(months);
            System.out.println(
                    "수강 " + months + "개월: 수정 전=" + before + ", 수정 후=" + after);
            check(before == after && after == expectedFees[i],
                    months + "개월 결과 유지 및 규칙 확인");
        }

        LocalDate reviewDate = LocalDate.of(2026, 10, 21);
        String source = String.join("\n",
                "// int total = months * 100_000;",
                "// @review-before 2026-06-01 공제 근거 재확인",
                "// 신청서 합의에 따라 총액에서 한 번 공제한다.",
                "int total = 100_000;",
                "String label = \"// return total;\";",
                "// @review-before 2027-01-01 안내 문구 재확인");

        List<Finding> findings = inspect(source, reviewDate);
        System.out.println("검사 후보:");
        for (Finding finding : findings) {
            System.out.println(
                    finding.lineNumber() + "행: " + finding.reason());
        }

        List<Finding> expectedFindings = List.of(
                new Finding(1, "주석 처리된 코드 후보"),
                new Finding(2, "검토 기한 도래"));
        check(findings.equals(expectedFindings), "후보 종류와 줄 번호");

        check(inspect("// 신청서에 합의된 총액 공제 방식이다.", reviewDate).isEmpty(),
                "이유 주석 유지");
        check(inspect("String text = \"// return total;\";", reviewDate).isEmpty(),
                "문자열 속 기호 제외");
        check(inspect("// @review-before 2027-01-01 재확인", reviewDate).isEmpty(),
                "미래 검토 기한 제외");
        check(inspect("// @review-before 2026-10-21 재확인", reviewDate).equals(
                List.of(new Finding(1, "검토 기한 도래"))),
                "검토 당일부터 보고");
        check(inspect("  // return total;", reviewDate).equals(
                List.of(new Finding(1, "주석 처리된 코드 후보"))),
                "들여쓴 주석 검사");

        System.out.println("실패 수: " + failures);
        if (failures > 0) {
            throw new IllegalStateException("확인 실패");
        }
    }

    private static int legacyFee(int months) {
        validateMonths(months);
        // 할인은 적용하지 않는다.
        // int total = months * 100_000 - 5_000;
        int total = months * 100_000;
        if (months >= 3) {
            total -= 10_000;
        }
        return total;
    }

    /**
     * 가상 수강 규칙에 따른 총 납부액을 계산한다.
     *
     * @param months 수강 개월 수, 1 이상 12 이하
     * @return 할인을 반영한 총 납부액, 원 단위
     * @throws IllegalArgumentException 개월 수가 허용 범위를 벗어난 경우
     */
    public static int calculateFee(int months) {
        validateMonths(months);
        int total = months * MONTHLY_FEE_WON;
        if (qualifiesForDiscount(months)) {
            // 예제 신청서에 합의된 방식이므로 총액에서 한 번만 공제한다.
            total -= LONG_TERM_DISCOUNT_WON;
        }
        return total;
    }

    private static boolean qualifiesForDiscount(int months) {
        return months >= DISCOUNT_MIN_MONTHS;
    }

    private static void validateMonths(int months) {
        if (months < 1 || months > 12) {
            throw new IllegalArgumentException("수강 개월 수는 1 이상 12 이하");
        }
    }

    private static List<Finding> inspect(String source, LocalDate reviewDate) {
        List<Finding> findings = new ArrayList<>();
        String[] lines = source.split("\\R", -1);
        for (int i = 0; i < lines.length; i++) {
            String line = lines[i].stripLeading();
            if (!line.startsWith("//")) {
                continue;
            }

            String comment = line.substring(2).strip();
            if (CODE_CANDIDATE.matcher(comment).matches()) {
                findings.add(new Finding(i + 1, "주석 처리된 코드 후보"));
            }

            Matcher marker = REVIEW_MARKER.matcher(comment);
            if (marker.matches()) {
                LocalDate deadline = LocalDate.parse(marker.group(1));
                if (!reviewDate.isBefore(deadline)) {
                    findings.add(new Finding(i + 1, "검토 기한 도래"));
                }
            }
        }
        return List.copyOf(findings);
    }

    private static void check(boolean condition, String name) {
        System.out.println((condition ? "PASS: " : "FAIL: ") + name);
        if (!condition) {
            failures++;
        }
    }

    private record Finding(int lineNumber, String reason) {
    }
}

legacyFee 안의 낡은 주석과 주석 처리된 계산식은 수정 전 상태를 비교하기 위해 의도적으로 보관한 교육용 표본이다. 실제로 유지할 계산 구현은 calculateFee다. 검사 입력 문자열에도 탐지 대상이 들어 있지만, 그것은 실행할 구현이 아니라 검사 자료다. 이 구분 없이 예제 전체를 보고 “검사기가 모든 낡은 주석을 제거했다”고 해석해서는 안 된다.

줄별 해설

처음의 import 선언 다섯 줄은 날짜, 목록, 정규식에 필요한 표준 라이브러리를 불러온다. 그다음 세 상수는 월 수강료, 할인 대상 개월 수, 총 할인액을 나타낸다. 금액 이름에 원 단위를 붙였으므로 계산에 쓰인 숫자의 의미를 별도 주석으로 반복하지 않는다.

CODE_CANDIDATE 선언은 주석 본문 전체가 특정 코드 형태와 일치하는지 확인한다. 첫 갈래는 초기값이 있는 정수 변수 선언이고, 두 번째는 단순 대입이며, 세 번째는 값을 반환하는 문장이다. 끝의 세미콜론도 요구한다. 대입 갈래의 (?!=)는 바로 다음 문자가 등호인 형태를 제외하기 위한 조건이다. 이 정규식이 Java 문장의 정확성을 검증하는 것은 아니다.

REVIEW_MARKER는 주석 본문 처음부터 검토 표기가 나오는 경우만 받는다. 날짜 뒤에는 설명을 붙일 수 있다. 날짜 모양은 정규식이 확인하고 실제 달력 날짜로 바꾸는 일은 LocalDate.parse가 맡는다. 예제 입력에는 유효한 날짜만 넣는다. 잘못된 날짜를 사용하면 현재 구현은 예외로 끝나며, 이를 별도 검사 결과로 바꾸는 기능은 포함하지 않는다.

main의 두 배열은 입력과 기대 금액을 같은 순서로 보관한다. 2개월과 3개월을 함께 넣어 할인 기준 앞뒤를 확인하고, 1개월과 12개월로 허용 범위 양끝도 확인한다. 반복문 안에서는 수정 전후 결과가 같은지와 기대 금액이 맞는지를 한 조건으로 검사한다. 두 구현이 같은 실수를 하더라도 기대 금액과 다르면 실패한다.

이 확인은 선택한 입력에 대한 결과다. 가능한 모든 입력의 동등성을 실행으로 증명한 것은 아니다. 다만 두 구현을 나란히 보면 같은 입력 검증을 거친 뒤 같은 곱셈과 같은 조건의 공제를 수행한다는 점도 확인할 수 있다. 실행 결과와 코드 검토를 함께 사용한다.

reviewDate는 현재 시각을 조회하지 않고 고정 날짜를 받는다. 따라서 시간이 지나도 책의 예상 출력이 유지된다. String.join으로 만든 여섯 줄은 각각 코드 후보, 지난 검토 기한, 이유 설명, 실행 코드, 문자열, 미래 검토 기한을 표현한다. 검사 결과가 첫째 줄과 둘째 줄에만 있어야 하는 이유가 입력에 드러난다.

Finding은 줄 번호와 이유를 담는 레코드다. 두 값이 같으면 같은 결과로 비교할 수 있으므로 기대 목록과 실제 목록을 직접 대조한다. 후보 개수만 검사하면 엉뚱한 두 줄을 찾아도 통과할 수 있다. 목록 전체를 비교하면 종류, 위치, 순서까지 함께 확인한다.

calculateFee의 Javadoc은 입력 범위와 반환 단위를 설명한다. 메서드 안의 이유 주석은 공제를 총액에 한 번만 적용하는 근거를 설명한다. 서로 독자가 다르다. 호출자는 계약을 보고 사용할 수 있고, 구현을 바꾸는 사람은 가까운 주석에서 선택의 배경을 확인할 수 있다.

inspect는 줄을 순서대로 읽는다. stripLeading으로 들여쓰기를 제거하고 한 줄 주석이 아니면 다음 줄로 넘어간다. 이후 주석 기호 두 글자를 제외한 본문만 검사한다. 배열 위치는 0부터 시작하지만 사람이 읽는 줄 번호는 1부터 시작하므로 결과에는 i + 1을 저장한다.

검토 날짜 비교는 기준일이 기한보다 이르지 않을 때 보고하도록 작성했다. 따라서 기한 당일도 포함한다. 마지막에는 결과 목록의 복사본을 반환한다. check는 매번 PASS 또는 FAIL을 출력하고 실패 수를 누적한다. 끝의 실패 수가 0이 아니면 예외를 던져 출력만 보고 실패를 놓치는 일을 줄인다.

실행 결과

JDK 17이 설치된 macOS 또는 Linux 터미널에서 Main.java를 저장한 디렉터리로 이동해 실행한다. 프로그램은 입력을 기다리지 않고 종료한다.

javac -d out *.java
java -cp out Main

컴파일 단계의 출력은 없으며 실행 결과는 다음과 같다.

수강 1개월: 수정 전=100000, 수정 후=100000
PASS: 1개월 결과 유지 및 규칙 확인
수강 2개월: 수정 전=200000, 수정 후=200000
PASS: 2개월 결과 유지 및 규칙 확인
수강 3개월: 수정 전=290000, 수정 후=290000
PASS: 3개월 결과 유지 및 규칙 확인
수강 4개월: 수정 전=390000, 수정 후=390000
PASS: 4개월 결과 유지 및 규칙 확인
수강 12개월: 수정 전=1190000, 수정 후=1190000
PASS: 12개월 결과 유지 및 규칙 확인
검사 후보:
1행: 주석 처리된 코드 후보
2행: 검토 기한 도래
PASS: 후보 종류와 줄 번호
PASS: 이유 주석 유지
PASS: 문자열 속 기호 제외
PASS: 미래 검토 기한 제외
PASS: 검토 당일부터 보고
PASS: 들여쓴 주석 검사
실패 수: 0

금액 비교가 통과했다는 것은 고른 입력에서 동작이 유지되고 가상 규칙에도 맞았다는 뜻이다. 주석 검사가 통과했다는 것은 정한 탐지 범위에서 예상한 후보를 찾았다는 뜻이다. 프로그램 전체의 설명이 모두 정확하다는 뜻으로 넓혀 읽지 않는다.

실무에서 자주 틀리는 것

동작을 번역한 주석을 계속 덧붙인다

틀린 코드에서는 대입과 반환을 그대로 설명한다. 수정할 줄이 늘어날 뿐 판단에 필요한 정보는 추가되지 않는다.

// 총액을 계산한다.
int x = months * 100_000;
// 총액을 반환한다.
return x;

고친 코드는 값의 의미와 단위를 이름에 둔다. 설명을 지우기 전에 이름만으로 뜻이 드러나는지 확인한다.

int totalFeeWon = months * MONTHLY_FEE_WON;
return totalFeeWon;

주석에 맞추려고 현재 규칙을 바꾼다

다음은 주석과 실행 내용이 충돌하는 상태다. 주석만 믿고 할인 처리를 삭제하면 결과가 달라진다.

// 할인은 적용하지 않는다.
if (qualifiesForDiscount(months)) {
    total -= LONG_TERM_DISCOUNT_WON;
}

현재 규칙에서 총액 공제가 확인되었다면 낡은 문장을 제거하고 필요한 근거를 남긴다. 규칙을 확인하지 못했다면 어느 한쪽을 추측으로 고치지 않는다.

if (qualifiesForDiscount(months)) {
    // 예제 신청서의 합의에 따라 총액에서 한 번 공제한다.
    total -= LONG_TERM_DISCOUNT_WON;
}

Javadoc에 구현의 임시 모습을 적는다

아래 설명은 호출자가 알아야 할 계약보다 구현 방식에 기대고 있다. 반복문을 사용하도록 바꾸면 문서도 불필요하게 고쳐야 한다.

/**
 * 곱셈 한 번과 if문 한 번으로 금액을 구한다.
 */
public static int calculateFee(int months) {
    return months * 100_000;
}

고친 문서는 범위와 단위를 설명한다. 아래 조각은 계약의 모양을 보여 주며, 본문의 할인 구현은 완성 코드와 같다.

/**
 * 가상 수강 규칙에 따른 총 납부액을 계산한다.
 *
 * @param months 수강 개월 수, 1 이상 12 이하
 * @return 할인을 반영한 총 납부액, 원 단위
 * @throws IllegalArgumentException 개월 수가 허용 범위를 벗어난 경우
 */

검사 후보를 자동으로 삭제한다

검사 결과만 보고 다음과 같이 지우면 사용법 예시나 아직 유효한 근거까지 잃을 수 있다. 검토 기한이 지났다는 이유만으로 내용이 틀렸다고 결정할 수도 없다.

if (CODE_CANDIDATE.matcher(comment).matches()) {
    lines[i] = "";
}

고친 코드는 위치와 이유를 보고한다. 담당자가 실제 코드, 규칙의 근거, 변경 이력을 보고 삭제하거나 수정한다.

if (CODE_CANDIDATE.matcher(comment).matches()) {
    findings.add(new Finding(i + 1, "주석 처리된 코드 후보"));
}

한 줄 주석처럼 보이는 줄도 여러 줄 문자열이나 블록 주석 내부에 있을 수 있다. 이 검사기는 그런 문맥을 추적하지 않는다. 실제 저장소 전체에 적용할 때는 이런 한계를 고려해 결과를 검토하거나 문법을 이해하는 도구를 선택한다.

한눈에 보기

주석을 읽고 남기거나 옮기거나 지우는 판단 기준
발견한 내용확인할 질문처리 방향
코드 동작을 반복한 문장이름만으로 뜻이 드러나는가이름을 보완하고 중복 설명 제거
조건의 의미를 설명한 문장하나의 판단 개념인가조건을 이름 있는 함수로 표현
이유·제약·근거지금도 확인 가능한 사실인가관련 코드 가까이에 유지
공개 메서드의 사용 조건호출자가 알아야 하는가Javadoc으로 계약 작성
주석 처리된 구현문서 예시인가, 옛 구현인가목적 확인 후 옛 구현 제거
기한이 지난 검토 표기근거를 다시 확인했는가검토 후 내용과 기한 갱신

주석을 줄이는 일의 기준은 줄 수가 아니다. 읽는 사람이 코드의 의미와 변경의 근거를 적은 추측으로 파악할 수 있는지가 기준이다. 이름과 함수가 동작을 설명하고, 주석이 선택의 배경을 보완하며, Javadoc이 사용 계약을 전달하도록 역할을 나눈다.

연습 문제

  1. 검사 입력 마지막에 // total = total - 10_000;을 추가하라. 후보 목록에 어떤 결과가 추가되는지 적고, 기대 목록을 고쳐 확인하라.
  2. // @review-before 2026-10-21 공제 근거 재확인을 기준일 2026-10-20과 2026-10-21에 각각 검사하라. 어느 날부터 보고해야 하는지 확인문으로 표현하라.
  3. // 수강 기간이 3개월 이상인지 확인한다.라는 주석 아래에 months >= 3 조건이 있다. 이름과 함수로 옮기고, 주석으로 남길 가치가 있는 정보 한 가지를 별도로 제시하라.
  4. // System.out.println(total);이 현재 검사기에 잡히지 않는 이유를 설명하라. 이 사례를 추가 지원할지 결정하기 전에 확인해야 할 점도 적어라.

정답과 해설

첫째 문제의 추가 줄은 7행이다. 대입 형태이며 세미콜론으로 끝나므로 new Finding(7, "주석 처리된 코드 후보")가 기존 두 결과 뒤에 추가된다. 기대 목록에 이 결과를 넣으면 개수뿐 아니라 줄 번호와 순서까지 확인할 수 있다. 입력만 바꾸고 기대 목록을 그대로 두면 후보 비교는 FAIL이 된다.

둘째 문제에서는 2026-10-20에 후보가 없고, 2026-10-21에 검토 기한 도래가 보고된다. “기한 전까지 검토한다”라는 팀 규칙을 코드와 확인문에 같은 의미로 반영한 것이다.

String comment = "// @review-before 2026-10-21 공제 근거 재확인";
check(inspect(comment, LocalDate.of(2026, 10, 20)).isEmpty(),
        "검토 기한 전에는 제외");
check(inspect(comment, LocalDate.of(2026, 10, 21)).equals(
        List.of(new Finding(1, "검토 기한 도래"))),
        "검토 기한 당일부터 보고");

셋째 문제는 qualifiesForDiscount(months)와 DISCOUNT_MIN_MONTHS로 표현할 수 있다. “3개월 이상인가”를 설명하는 주석은 이름과 조건을 반복하므로 제거한다. 반면 그 기준이 어떤 가상 신청서 합의에서 정해졌는지 확인되었다면 그 근거는 주석에 남길 수 있다. 확인하지 않은 배경을 그럴듯하게 만들어 적어서는 안 된다.

넷째 문제의 출력문은 변수 선언, 단순 대입, 반환문 중 어느 형태에도 해당하지 않는다. 따라서 현재 정규식은 놓친다. 지원 범위를 넓히기 전에 실제 옛 구현인지, 설명을 위한 사용 예인지 확인해야 한다. 메서드 호출까지 잡으면 후보는 늘지만 설명용 코드도 함께 잡힐 수 있다. 새 패턴을 넣을 때는 찾을 사례와 제외할 사례를 모두 확인해야 검사 결과의 의미를 유지할 수 있다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.