Devin.KR

작업 기록과 인계 - 다음 사람(그리고 다음 세션)이 이어받게

개발자KR 조회 5

이 장에서 배우는 것

주문 서비스의 수정이 끝났다고 해서 다음 사람이 곧바로 이어서 일할 수 있는 것은 아니다. 재고 차감 시점을 왜 바꾸었는지, 결제 모의 객체로 무엇을 확인했는지, 아직 실행하지 않은 검증은 무엇인지가 남아 있어야 한다. 코드에는 현재 동작이 담기지만, 그 동작을 선택한 이유와 확인한 범위까지 모두 드러나지는 않는다.

앞 장에서 생성 코드의 입력·권한·비밀값·의존성을 점검했다면, 이제 그 판단 중 다음 작업에 필요한 것을 기록한다. 이 장에서는 사람이 읽고 보완할 수 있는 인계 문서를 만든다. 인공지능(AI)이 대화를 요약했더라도, 기록된 검증 결과가 실제 실행 근거와 연결되는지는 사람이 확인해야 한다.

  • 변경 내용과 변경 이유를 구분하고, 다음 판단에 필요한 근거를 남긴다.
  • 저장소의 지속적인 작업 규칙과 특정 작업의 진행 기록을 구분한다.
  • 검증 성공·실패·미실행을 나누고 남은 일을 구체적인 행동으로 적는다.
  • 커밋 기록 형식의 데이터를 모아 결정적인 출력의 인계 문서를 생성한다.

문제 상황

작은 쇼핑몰 주문 서비스를 두 사람이 함께 수정하고 있다. 한 사람은 주문 취소 시 재고를 돌려주는 부분을 맡았고, 다른 사람은 결제 실패 시 주문 상태를 정리하는 부분을 맡았다. 이 저장소의 요구사항에서는 결제 승인이 실패하면 재고를 원래 수량으로 돌려놓아야 한다. 결제 모의 객체는 외부 결제 서버에 접속하지 않고 승인 성공이나 실패를 돌려준다.

오후에 담당자가 바뀌었다. 저장소에는 수정 커밋이 세 개 있고, 대화에는 다음과 같은 AI의 설명이 남아 있다.

주문 취소와 결제 실패 처리를 정리했다. 관련 검증을 진행했으며, 다음 작업자는 문서를 참고해 이어서 작업하면 된다.

이 설명으로는 작업을 이어받기 어렵다. 어떤 검증을 실행했는지, 결과가 성공이었는지, 한 번 성공한 주문 취소를 반복했을 때도 재고가 유지되는지 알 수 없다. “관련 검증을 진행했다”는 말은 검증 하나를 실행하다 실패한 상황과 필요한 검증을 모두 통과한 상황을 구별하지 못한다.

실제 기록을 확인하니 재고 중복 복구 검증은 성공했고, 결제 실패 뒤 재고 수량을 확인하는 검증은 실패했다. 중복 취소 요청 검증은 아직 실행하지 않았다. 다음 작업자가 첫 문장만 믿으면 실패한 부분을 완료된 일로 오해할 수 있다. 반대로 모든 작업을 처음부터 반복하면 이미 확인한 범위까지 다시 추적하게 된다.

필요한 것은 대화 전체의 복사본이 아니다. 변경의 이유, 확인한 결과, 다음 행동을 같은 기준으로 묶은 기록이다. 성공한 내용만 정리하는 문서가 아니라, 어디까지 믿을 수 있는지 드러내는 문서여야 한다.

지침 파일과 작업 기록은 수명이 다르다

지침 파일은 앞으로 반복할 작업의 기준을 담는다. 예를 들어 “주문 상태를 변경할 때는 재고 변화도 함께 확인한다”는 규칙은 담당자나 작업 날짜가 바뀌어도 적용할 수 있다. 특정 AI 도구가 지침 파일을 자동으로 읽는지는 실행 환경에 따라 다르지만, 팀이 유지할 규칙과 개별 작업 기록을 구분하는 원리는 같다.

작업 기록은 특정 변경에서 실제로 일어난 일을 담는다. “결제 실패 뒤 재고 복구 검증에서 기대 수량 5, 실제 수량 4를 관찰했다”는 내용은 이번 작업의 상태다. 이를 지속적인 규칙처럼 지침 파일에 넣으면 다음 작업자는 과거의 실패를 현재 저장소의 상태로 오해할 수 있다.

지침 파일과 작업 기록은 적용 범위와 갱신 계기가 다르다
구분담을 내용갱신 계기
지침 파일주문 상태 변경 시 재고도 확인한다는 작업 규칙팀의 작업 기준이 바뀔 때
작업 기록이번 변경의 이유, 검증 결과, 미완료 항목해당 작업의 상태가 바뀔 때
커밋 기록어느 변경에 어떤 설명과 근거가 연결되는지변경을 기록할 때
인계 문서선택한 기록을 다음 작업 순서에 맞게 모은 내용담당자나 작업 세션이 바뀔 때

작업 기록에서 반복되는 문제가 보이면 팀이 검토한 뒤 지침으로 옮길 수 있다. 예를 들어 재고 수량 검증을 여러 번 빠뜨렸다면 확인 규칙을 추가할 수 있다. 이때 “이번에 빠뜨렸다”는 관찰과 “앞으로 확인한다”는 규칙은 별도로 작성한다. 한 번의 상황을 곧바로 모든 작업의 규칙으로 확대하지 않는 편이 좋다.

지침은 반복 작업의 기준이고 작업 기록은 특정 변경의 사실을 담는다

파일 위치도 역할을 드러내야 한다. 팀 규칙을 모으는 위치와 작업별 기록을 모으는 위치를 정하고, 같은 내용을 여러 곳에서 따로 수정하지 않는다. 인계 문서에는 필요한 규칙의 위치를 안내할 수 있지만, 규칙 전문을 매번 복사할 필요는 없다. 복사된 규칙이 오래되면 작업 기록과 실제 기준이 서로 달라진다.

다음 세션의 AI도 새 작업자와 비슷하다. 이전 대화가 이어진다고 가정하지 말고 현재 코드, 적용할 지침, 이번 작업 기록을 식별할 수 있게 제공한다. “앞에서 말한 대로 처리해라”는 표현은 그 앞부분을 읽지 못하면 의미가 없다. 작업 대상과 근거를 독립적으로 이해할 수 있는 문장이 인계에 적합하다.

인계 문서는 이유·근거·다음 행동을 연결한다

변경 요약은 무엇을 바꾸었는지 설명한다. 변경 이유는 그 선택을 하게 된 요구나 관찰을 설명한다. “재고 복구 조건 정리”만으로는 두 정보가 섞인다. “취소 완료 상태에서는 재고를 다시 복구하지 않도록 조건을 추가했다”가 변경 요약이고, “같은 취소 요청을 다시 처리하면 재고가 두 번 늘어날 수 있기 때문이다”가 이유다.

이유를 남길 때 구현 과정을 모두 적을 필요는 없다. 다음 사람이 그 변경을 되돌리거나 확장할 때 필요한 판단을 남긴다. 요구사항에 따른 선택인지, 재현한 오류에 대한 대응인지, 아직 확인하지 못한 가설인지가 드러나면 좋다. 가설을 확정된 원인처럼 적으면 이후 조사 방향이 잘못 잡힐 수 있다.

인계 문서의 각 항목은 다음 작업자의 질문에 답한다
항목답할 질문작성 예
대상과 기준어떤 기록을 묶었는가입력 순서의 마지막 커밋 c103까지
변경과 이유무엇을 왜 바꾸었는가반복 취소의 재고 중복 복구를 막기 위해 조건 추가
검증과 근거무엇을 실행하고 관찰했는가결제 실패 검증 실패: 기대 재고 5, 실제 4
남은 일다음에 무엇을 해야 하는가재고 복구 경로 수정 후 같은 입력으로 재검증

검증 결과에는 성공, 실패, 미실행을 따로 둔다. 미실행은 실패가 아니다. 실행 결과 자체가 없다는 뜻이다. 또한 성공은 해당 검증이 통과했다는 뜻이지, 주문 서비스 전체가 확인되었다는 뜻이 아니다. 결제 모의 객체로 성공한 결과를 실제 결제 연동까지 확인한 것처럼 넓혀 적지 않는다.

검증 근거에는 실행 대상과 관찰값을 적는다. 실제 저장소에서는 실행 명령, 입력 조건, 결과 요약, 보관한 로그 위치 등을 연결할 수 있다. 로그에 고객 정보나 비밀값이 포함되어 있다면 필요한 관찰만 정리한다. 앞 장에서 점검한 민감한 내용을 인계 편의를 이유로 다시 퍼뜨리지 않아야 한다.

남은 일은 “추가 확인 필요”보다 작아야 한다. “결제 실패 검증을 재실행해 재고가 5로 복구되는지 확인한다”처럼 대상과 완료 조건을 적는다. 담당자나 관련 작업 번호가 정해져 있다면 함께 남긴다. 정해지지 않은 담당자를 AI가 임의로 배정하거나 예상 완료일을 사실처럼 채우게 해서는 안 된다.

입력 기록에 있는 변경 요약·이유·검증 결과·남은 일을 모아라. 실패와 미실행을 성공으로 바꾸지 마라. 기록에 없는 실행 결과를 추정하지 말고, 부족한 정보는 확인할 항목으로 남겨라.

이 요청은 AI의 문장을 다듬는 데 쓸 수 있다. 다만 이번 프로그램은 이미 구조화된 기록을 정해진 형식으로 출력하므로 AI 호출이 필요하지 않다. 표현을 바꾸는 일과 검증 사실을 판정하는 일을 구별하면, 단순한 집계 작업을 더 쉽게 확인할 수 있다.

자동 생성은 기록을 모으고, 사실 확인은 남겨 둔다

완성 프로그램은 커밋 식별자, 변경 요약, 이유, 검증 목록, 남은 일 목록을 가진 데이터를 받는다. 실제 버전 관리 명령을 실행하거나 커밋 메시지의 자유로운 문장을 해석하지 않는다. 팀이 커밋별로 정리한 데이터를 입력받는 형태다. 여기서는 식별자 c101부터 c103까지를 쓰며, 입력 목록은 오래된 변경부터 정렬되어 있다고 약속한다.

검증 하나에는 이름, 상태, 근거가 있다. 성공과 실패에는 근거를 필수로 둔다. 미실행은 근거가 비어 있어도 받아들이고, 문서에는 “실행 기록 없음”으로 출력한다. 이렇게 해야 빈 문자열을 보고 검증이 통과했다고 해석하는 실수를 줄일 수 있다. 다만 근거 문자열이 있다는 사실만으로 그 내용의 진실성이 보장되지는 않는다.

명시적으로 적은 남은 일 외에도, 실패한 검증과 실행하지 않은 검증을 다음 행동으로 모은다. 검증 목록 자체가 비어 있다면 검증 대상을 정해야 한다는 항목을 만든다. 남은 일 앞에는 커밋 식별자를 붙여 서로 비슷한 문장도 출처를 찾을 수 있게 한다.

커밋별 변경과 검증 기록을 모아 근거와 다음 행동이 있는 인계 문서를 만든다

이 예제의 기록은 커밋에 연결된 관찰의 묶음이다. 뒤의 커밋에서 이전 실패를 해결하더라도 앞의 기록을 자동으로 없애지 않는다. 같은 검증 이름이라고 같은 조건의 실행인지는 알 수 없기 때문이다. 실제 팀에서 해결 상태까지 자동 집계하려면 검증 식별자와 해결 관계 같은 추가 기준이 필요하다. 이 프로그램의 남은 일 목록은 기록에 나타난 후속 확인 목록이며, 현재 모든 이슈의 최종 상태를 판정하는 도구는 아니다.

기준 시각은 문자열로 전달한다. 실행할 때마다 현재 시각을 읽지 않으므로 같은 입력은 같은 문서를 만든다. 프로그램은 파일을 만들지 않고 표준 출력에만 문서를 표시한다. 실행 경로나 임시 폴더 이름도 출력에 들어가지 않는다. 예제 안의 검증 근거는 고정된 입력 데이터이며, 프로그램을 실행한다고 주문 서비스 검증을 새로 수행하는 것은 아니다.

완성 코드

아래 파일 하나를 Main.java로 저장한다. JDK 17의 표준 라이브러리만 사용한다. 앞부분의 작은 확인 함수는 문서 생성기의 동작을 검사한다. 주문·재고·결제 코드의 정확성을 검사하는 함수와는 역할이 다르다.

Main.java

import java.util.ArrayList;
import java.util.List;

public class Main {
    enum Status {
        PASS("성공"), FAIL("실패"), NOT_RUN("미실행");

        private final String label;

        Status(String label) {
            this.label = label;
        }

        String label() {
            return label;
        }
    }

    record Verification(String name, Status status, String evidence) {
        Verification {
            requireText(name, "검증 이름");
            if (status == null) {
                throw new IllegalArgumentException("검증 상태가 필요하다");
            }
            if (evidence == null) {
                throw new IllegalArgumentException("근거는 null일 수 없다");
            }
            if (status != Status.NOT_RUN) {
                requireText(evidence, "실행 근거");
            }
        }
    }

    record CommitRecord(
            String id,
            String summary,
            String reason,
            List<Verification> verifications,
            List<String> remaining) {
        CommitRecord {
            requireText(id, "커밋 식별자");
            requireText(summary, "변경 요약");
            requireText(reason, "변경 이유");
            verifications = List.copyOf(verifications);
            remaining = List.copyOf(remaining);
            for (String task : remaining) {
                requireText(task, "남은 일");
            }
        }
    }

    static void requireText(String value, String field) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(field + "이 필요하다");
        }
    }

    static String render(String fixedTime, List<CommitRecord> records) {
        requireText(fixedTime, "기준 시각");
        List<CommitRecord> snapshot = List.copyOf(records);
        List<String> lines = new ArrayList<>();
        List<String> pending = new ArrayList<>();

        lines.add("인계 문서");
        lines.add("기준 시각: " + fixedTime);
        String lastId = snapshot.isEmpty()
                ? "없음" : snapshot.get(snapshot.size() - 1).id();
        lines.add("대상: 입력 " + snapshot.size()
                + "건 / 마지막 커밋 " + lastId);
        lines.add("");
        lines.add("[변경과 검증]");

        if (snapshot.isEmpty()) {
            lines.add("- 입력 기록 없음");
            pending.add("인계 대상 커밋 기록 수집");
        }

        for (CommitRecord record : snapshot) {
            lines.add("- " + record.id() + ": " + record.summary());
            lines.add("  이유: " + record.reason());

            if (record.verifications().isEmpty()) {
                lines.add("  검증: 기록 없음");
                pending.add(record.id() + ": 검증 대상과 실행 방법 정리");
            }

            for (Verification verification : record.verifications()) {
                String evidence = verification.evidence().isBlank()
                        ? "실행 기록 없음" : verification.evidence();
                lines.add("  검증: " + verification.name()
                        + " / " + verification.status().label()
                        + " / " + evidence);

                if (verification.status() == Status.FAIL) {
                    pending.add(record.id() + ": "
                            + verification.name() + " 실패 원인 수정 후 재검증");
                } else if (verification.status() == Status.NOT_RUN) {
                    pending.add(record.id() + ": "
                            + verification.name() + " 실행");
                }
            }

            for (String task : record.remaining()) {
                pending.add(record.id() + ": " + task);
            }
        }

        lines.add("");
        lines.add("[남은 일]");
        if (pending.isEmpty()) {
            lines.add("- 기록에 명시된 남은 일 없음");
        } else {
            for (String task : pending) {
                lines.add("- " + task);
            }
        }
        return String.join("\n", lines);
    }

    private static int failures;

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

    public static void main(String[] args) {
        String fixedTime = "2026-10-21T09:00:00+09:00";
        List<CommitRecord> records = List.of(
                new CommitRecord(
                        "c101",
                        "취소 시 재고 중복 복구 방지",
                        "취소 완료 주문을 다시 처리하면 재고가 늘어날 수 있다",
                        List.of(new Verification(
                                "재고 중복 복구 검증",
                                Status.PASS,
                                "취소 2회 후 재고 5 유지")),
                        List.of()),
                new CommitRecord(
                        "c102",
                        "결제 실패 시 주문 상태 정리",
                        "승인 실패 주문을 결제 완료로 남기지 않아야 한다",
                        List.of(new Verification(
                                "결제 실패 뒤 재고 복구 검증",
                                Status.FAIL,
                                "기대 재고 5, 실제 4")),
                        List.of("복구 경로의 상태 전이 조건 확인")),
                new CommitRecord(
                        "c103",
                        "중복 취소 요청 처리 조건 추가",
                        "같은 요청의 반복 처리 범위를 명확히 한다",
                        List.of(new Verification(
                                "중복 취소 요청 검증",
                                Status.NOT_RUN,
                                "")),
                        List.of()));

        String document = render(fixedTime, records);
        check(document.contains("이유: 취소 완료 주문을 다시 처리하면"),
                "변경 이유 보존");
        check(document.contains("/ 실패 / 기대 재고 5, 실제 4"),
                "실패 근거 보존");
        check(document.contains("- c103: 중복 취소 요청 검증 실행"),
                "미실행을 남은 일로 수집");
        check(document.equals(render(fixedTime, records)),
                "같은 입력의 문서 일치");

        boolean rejected = false;
        try {
            new Verification("근거 없는 성공", Status.PASS, "");
        } catch (IllegalArgumentException expected) {
            rejected = true;
        }
        check(rejected, "근거 없는 성공 거부");

        CommitRecord noVerification = new CommitRecord(
                "c104", "기록 형식 정리", "인계 항목을 통일한다",
                List.of(), List.of());
        check(render(fixedTime, List.of(noVerification))
                        .contains("- c104: 검증 대상과 실행 방법 정리"),
                "빈 검증 목록을 남은 일로 수집");
        check(render(fixedTime, List.of())
                        .contains("- 인계 대상 커밋 기록 수집"),
                "빈 입력에 수집 작업 표시");

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

줄별 해설

Status 선언은 검증 상태를 세 값으로 제한한다. 자유로운 문자열로 “완료”, “확인”, “거의 성공”을 섞어 쓰면 집계 기준이 흔들린다. 내부 값과 출력용 한글 이름을 나누어, 조건문은 고정된 값으로 비교하고 독자는 한글 설명을 읽게 한다.

Verification 선언은 검증 한 건의 경계를 만든다. 이름이 없거나 상태가 없으면 생성 단계에서 거부한다. 성공과 실패에는 비어 있지 않은 실행 근거가 필요하다. 미실행에 빈 근거를 허용하는 이유는, 실행하지 않았다는 상태를 숨기지 않고 기록하기 위해서다.

CommitRecord 선언은 변경 요약과 이유를 별도 필드로 둔다. 남은 일도 검증 목록과 분리한다. 검증 실패에서 파생되는 후속 행동 외에 “상태 전이 조건 확인”처럼 사람이 판단해 남기는 일이 있기 때문이다. 목록에는 List.copyOf를 적용해 입력 목록의 나중 변경이 기록에 반영되지 않게 한다. 이 메서드는 null인 목록이나 null 원소도 거부한다.

requireText 호출은 필수 문장의 빈 값을 막는다. 공백만 있는 이유도 허용하지 않는다. 다만 이 함수는 문장의 품질까지 판단하지 못한다. “필요해서 변경”도 비어 있지는 않으므로 통과한다. 구조적 검사와 내용 검토를 구분해야 하는 지점이다.

render의 첫 부분은 기준 시각과 입력 목록을 확정한다. 마지막 식별자는 입력 순서의 마지막 기록에서 가져온다. 문자열 식별자를 정렬해 최신 커밋을 추정하지 않는다. 실제 저장소의 시간순 정리는 이 메서드를 호출하기 전에 해야 한다. 여기서 쓰는 스냅샷(snapshot)은 이번 출력에서 사용할 목록을 복사한 값이다.

lines와 pending은 문서 본문과 남은 일을 각각 모은다. 모든 변경을 출력한 다음 남은 일을 한곳에 보여 주기 위한 구성이다. 두 목록 모두 입력 순서대로 추가하므로 출력 순서가 유지된다. 같은 문장도 커밋 식별자가 다르면 다른 출처의 항목으로 남는다.

검증 반복문은 상태와 근거를 함께 출력한다. 실패이면 수정 후 재검증 항목을, 미실행이면 실행 항목을 추가한다. 성공 기록에는 자동 후속 작업을 만들지 않는다. 검증이 하나도 없을 때는 “성공”으로 처리하지 않고 검증 대상과 실행 방법을 정리하도록 남긴다.

남은 일이 비었을 때의 문구는 “기록에 명시된 남은 일 없음”이다. “모든 작업 완료”보다 범위가 좁다. 입력에 빠진 작업까지 프로그램이 알 수 없기 때문이다. 문서의 결론이 입력으로 확인할 수 있는 범위를 넘지 않도록 문구도 설계한다.

main의 check 호출은 이유와 실패 근거의 보존, 미실행 항목의 수집, 반복 실행의 문서 일치, 잘못된 입력 거부, 빈 목록 처리를 확인한다. 실패하면 확인 이름을 출력하고 마지막에 예외를 던진다. 단순히 실패 문구를 출력한 뒤 정상 종료하면 실행한 사람이 결과를 놓칠 수 있다.

이 확인들은 생성기의 중요한 규칙을 다루지만 모든 문장을 비교하지는 않는다. 문서 형식을 바꾸는 작업에서는 예상 출력 전체와 실제 출력도 함께 확인해야 한다. 특히 새 필드를 넣었는데 문서에 반영하지 않은 경우에는 생성이 정상 종료되더라도 인계 정보가 빠질 수 있다.

실행 결과

저장한 디렉터리에서 다음 두 명령을 순서대로 실행한다. 첫 명령은 컴파일하고, 두 번째 명령은 확인 결과와 인계 문서를 출력한 뒤 종료한다.

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

예상 출력은 다음과 같다. 앞의 PASS는 문서 생성기의 확인 결과다. 문서 안의 성공·실패·미실행은 입력 데이터에 기록된 주문 서비스 검증 상태다. 두 결과를 같은 것으로 읽지 않아야 한다.

PASS 변경 이유 보존
PASS 실패 근거 보존
PASS 미실행을 남은 일로 수집
PASS 같은 입력의 문서 일치
PASS 근거 없는 성공 거부
PASS 빈 검증 목록을 남은 일로 수집
PASS 빈 입력에 수집 작업 표시

인계 문서
기준 시각: 2026-10-21T09:00:00+09:00
대상: 입력 3건 / 마지막 커밋 c103

[변경과 검증]
- c101: 취소 시 재고 중복 복구 방지
  이유: 취소 완료 주문을 다시 처리하면 재고가 늘어날 수 있다
  검증: 재고 중복 복구 검증 / 성공 / 취소 2회 후 재고 5 유지
- c102: 결제 실패 시 주문 상태 정리
  이유: 승인 실패 주문을 결제 완료로 남기지 않아야 한다
  검증: 결제 실패 뒤 재고 복구 검증 / 실패 / 기대 재고 5, 실제 4
- c103: 중복 취소 요청 처리 조건 추가
  이유: 같은 요청의 반복 처리 범위를 명확히 한다
  검증: 중복 취소 요청 검증 / 미실행 / 실행 기록 없음

[남은 일]
- c102: 결제 실패 뒤 재고 복구 검증 실패 원인 수정 후 재검증
- c102: 복구 경로의 상태 전이 조건 확인
- c103: 중복 취소 요청 검증 실행

기준 시각과 입력 데이터가 고정되어 있으므로 다시 실행해도 같은 결과가 나온다. 실제 인계에서는 생성된 문서를 읽고 누락된 실행 조건이나 로그 위치를 보완한다. 생성기가 실행되었다는 사실만으로 기록 속 검증을 수행했다고 표시하지 않는다.

실무에서 자주 틀리는 것

다음 세 예제는 잘못된 계산과 고친 계산을 같은 입력으로 실행한다. 동작을 유지하는 정리가 아니라 오류 수정이므로, 바뀌어야 할 결과가 무엇인지 확인한다. 각 코드는 독립적으로 컴파일하고 실행할 수 있다. 완성 프로그램에 합칠 필요는 없다.

실패가 아니면 성공이라고 적는다

실패 여부 하나만 사용하면 미실행도 성공으로 묶인다. 틀린 코드의 조건은 “실패가 아님”을 “성공”으로 바꾼다. 고친 코드는 상태를 그대로 구분한다. 아래 코드를 StatusExample.java로 저장해 실행하면 같은 미실행 입력에서 잘못된 결과와 의도한 결과를 함께 확인한다.

public class StatusExample {
    enum Status { PASS, FAIL, NOT_RUN }

    static String wrong(Status status) {
        return status != Status.FAIL ? "성공" : "실패";
    }

    static String fixed(Status status) {
        return switch (status) {
            case PASS -> "성공";
            case FAIL -> "실패";
            case NOT_RUN -> "미실행";
        };
    }

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

    public static void main(String[] args) {
        Status input = Status.NOT_RUN;
        check(wrong(input).equals("성공"), "기존 오류 재현");
        check(fixed(input).equals("미실행"), "수정 결과 확인");
    }
}

출력은 “PASS 기존 오류 재현”, “PASS 수정 결과 확인” 순서다. 첫 번째 PASS는 틀린 코드가 올바르다는 뜻이 아니다. 미실행을 성공으로 표시하는 오류가 재현되었다는 뜻이다. 확인 이름에 의도를 드러내면 이런 혼동을 줄일 수 있다.

실패한 검증이 있어도 남은 일이 없다고 쓴다

사람이 적은 할 일 목록만 복사하면 실패한 검증이 후속 작업에서 빠질 수 있다. 아래 틀린 코드는 명시된 작업만 세고, 고친 코드는 실패 검증에서 생기는 재검증 작업도 센다. PendingExample.java로 저장해 실행하면 명시된 작업이 없는 동일 입력에서 결과가 달라지는 것을 확인한다.

public class PendingExample {
    static int wrong(int explicitTasks, int failedChecks) {
        return explicitTasks;
    }

    static int fixed(int explicitTasks, int failedChecks) {
        return explicitTasks + failedChecks;
    }

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

    public static void main(String[] args) {
        int explicitTasks = 0;
        int failedChecks = 1;
        check(wrong(explicitTasks, failedChecks) == 0, "기존 누락 재현");
        check(fixed(explicitTasks, failedChecks) == 1, "재검증 작업 포함");
    }
}

출력은 “PASS 기존 누락 재현”, “PASS 재검증 작업 포함” 순서다. 실제 생성기에서는 개수만 늘리지 않고 검증 이름과 커밋 식별자를 붙인다. 그래야 다음 사람이 무엇을 다시 확인할지 알 수 있다. 이 작은 예제는 누락 조건만 드러내기 위해 개수를 사용했다.

요약에서 출처 식별자를 없앤다

내용이 읽기 좋다는 이유로 커밋 식별자를 빼면 같은 설명이 여러 변경에 걸쳐 있을 때 출처를 찾기 어렵다. 아래 예제를 TraceExample.java로 저장해 실행한다. 틀린 코드는 설명만 반환하고, 고친 코드는 같은 입력에 식별자를 함께 남긴다.

public class TraceExample {
    static String wrong(String commitId, String summary) {
        return summary;
    }

    static String fixed(String commitId, String summary) {
        return commitId + ": " + summary;
    }

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

    public static void main(String[] args) {
        String commitId = "c102";
        String summary = "재고 복구 확인";
        check(wrong(commitId, summary).equals("재고 복구 확인"),
                "기존 출처 누락 재현");
        check(fixed(commitId, summary).equals("c102: 재고 복구 확인"),
                "출처 식별자 유지");
    }
}

출력은 “PASS 기존 출처 누락 재현”, “PASS 출처 식별자 유지” 순서다. 실제 저장소에서는 팀이 찾을 수 있는 커밋 식별자나 변경 요청 링크를 사용한다. 식별자가 있다는 것과 기록이 최신이라는 것은 다른 문제다. 기록 이후 코드를 수정했다면 어떤 상태에서 검증한 결과인지도 다시 확인해야 한다.

한눈에 보기

인계 기록은 다음 작업자가 판단하고 실행할 정보를 남긴다
확인 대상남길 정보주의할 해석
변경 요약관찰 가능한 동작이나 처리 조건의 변화“정리함”만으로는 변경 범위를 알기 어렵다
변경 이유요구사항, 관찰한 오류, 선택의 근거가설을 확인된 원인으로 적지 않는다
검증대상, 상태, 입력 조건과 관찰 근거생성기 확인 성공과 서비스 검증 성공은 다르다
남은 일실패 수정, 미실행 검증, 별도 확인 항목기록에 없다는 사실을 완료로 해석하지 않는다
출처커밋 식별자와 기록의 기준후속 변경의 해결 여부를 이름만으로 추정하지 않는다
지침반복해서 적용할 팀의 작업 기준특정 작업의 일시적 상태와 섞지 않는다

인계 문서를 읽는 사람은 먼저 대상 범위를 보고, 실패와 미실행 항목을 확인한 뒤 필요한 변경 근거를 따라가면 된다. 작성자는 문서가 길어지는지보다 다음 행동을 결정할 정보가 빠지지 않았는지 살핀다. 다음 장에서는 이렇게 드러난 남은 일을 바탕으로 자동화와 병렬 작업을 어느 범위까지 맡길지 다룬다.

연습 문제

  1. c103의 검증 상태를 성공으로 바꾸고 근거를 “같은 취소 요청 2회 후 재고 5 유지”로 채워라. 남은 일에서 사라져야 할 항목과 그대로 남아야 할 항목을 설명하라. 기존 main의 확인 중 무엇을 바꿔야 하는지도 찾아라.
  2. c104처럼 검증 목록이 비어 있는 기록을 실제 입력 목록 끝에 추가하라. 대상 건수, 마지막 커밋, 검증 표시, 남은 일의 변화를 확인하라. 이를 성공으로 처리하면 안 되는 이유를 설명하라.
  3. “주문 상태를 바꾸면 재고 변화도 확인한다”, “c102 검증에서 기대 재고 5, 실제 4를 관찰했다”, “c102의 복구 경로를 수정하고 같은 입력으로 재검증한다”를 지침, 검증 기록, 남은 일로 구분하라.
  4. 뒤의 커밋에 같은 이름의 성공 검증이 추가되면 앞의 실패 항목을 자동으로 지우자는 제안이 나왔다. 검증 이름만 비교하는 방식의 문제와, 해결 관계를 기록하기 위해 필요한 정보를 설명하라.

정답과 해설

  1. c103의 “중복 취소 요청 검증 실행”이 사라진다. c102의 실패 후 재검증과 상태 전이 조건 확인은 그대로 남는다. 성공 한 건이 다른 커밋의 실패를 해결하지는 않는다. main의 “미실행을 남은 일로 수집” 확인은 더 이상 해당 입력에 맞지 않으므로, c103의 실행 항목이 없고 성공 근거가 보존되는지 확인하도록 바꾼다. 미실행 처리 자체의 확인은 별도의 미실행 입력으로 유지할 수 있다.

  2. 대상은 4건, 마지막 커밋은 c104가 된다. c104에는 “검증: 기록 없음”이 출력되고 남은 일에 “c104: 검증 대상과 실행 방법 정리”가 추가된다. 검증 목록이 비었다는 것은 성공 결과가 있다는 뜻이 아니다. 어떤 검증을 해야 하는지조차 기록되지 않은 상태일 수 있다. 기록 부재를 드러내야 다음 사람이 필요한 확인을 정할 수 있다.

  3. 첫 문장은 반복 작업에 적용할 지침이다. 두 번째 문장은 특정 변경에서 관찰한 검증 기록이다. 세 번째 문장은 아직 해야 할 행동이므로 남은 일이다. 특히 두 번째 문장의 실제 수량은 당시 관찰값이다. 이를 지침에 넣어 현재 상태처럼 유지하면 이후 수정 결과와 혼동할 수 있다.

  4. 이름이 같아도 입력 주문, 실행한 코드, 검증 범위가 다를 수 있다. 결제 모의 객체의 조건이 달라졌을 가능성도 있다. 자동으로 해결 처리하려면 검증을 식별할 값, 실행 대상 커밋, 조건과 범위, 어떤 실패를 해결했는지의 연결 정보가 필요하다. 앞의 실패 관찰은 이력으로 보존하고 현재 후속 작업의 상태를 별도로 갱신하는 설계도 가능하다. 이 예제는 그런 연결 정보가 없으므로 이름만 보고 실패를 지우지 않는다.

댓글 0

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

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