Devin.KR

AI 개발 · 실무

Claude·Gemini·Codex 실전 백서 - 터미널 AI 코딩 도구를 일에 쓰는 법

비대화형 실행과 자동화 - 스크립트·CI 에 AI 넣기

세 도구의 비대화형 실행 비교표(-p, exec), JSON·스트림 출력 해석, 시간 제한·재시도·빈 결과 감지(정상 종료인데 결과가 비는 경우), 동시 실행 수 제한, CI 에서 쓸 때 권한을 최소로, 완성 코드는 미리 저장한 JSON 줄 단위 이벤트 기록을 읽어 마지막 답·오류·소요 시간을 뽑고 빈 결과를 실패로 판정하는 해석기

개발자KR · 원고 갱신

이 장에서 배우는 것

터미널에서 요청을 보내고 답을 읽는 일과, 같은 작업을 스크립트에 넣어 반복하는 일은 다르다. 사람은 답이 이상하면 다시 묻지만, 자동화 프로그램은 종료 코드가 0이라는 이유만으로 다음 단계에 넘어갈 수 있다. 앞 장에서 실행 권한의 경계를 살폈다면, 이번에는 실행 결과를 받아들이는 경계를 정한다. AI 도구의 실행이 끝났다는 사실과 팀이 사용할 결과가 생겼다는 사실을 구분하는 것이 핵심이다.

완성 프로그램은 AI 도구를 실행하지 않는다. 미리 만든 JSON 줄 단위 이벤트 기록을 읽고 마지막 답, 오류, 소요 시간을 추출한다. 정상 종료했지만 답이 없는 기록도 실패로 판정한다. Python 3.12 이상에서 외부 패키지, API 키, 네트워크 연결 없이 실행하며, 임시 입력 파일부터 검증까지 한 파일에 담는다.

  • 세 도구의 비대화형 실행 방식과 출력 형식을 구분한다.
  • JSON 문서와 줄 단위 이벤트 기록을 서로 다른 방식으로 읽는다.
  • 종료 코드, 오류 이벤트, 마지막 답을 함께 검사한다.
  • 시간 제한, 재시도, 동시 실행 수를 자동화의 명시적인 정책으로 정한다.
  • 지속적 통합(CI)에서 권한과 결과 채택 조건을 작게 유지한다.

문제 상황

작은 팀이 변경 내용을 요약하는 작업을 CI에 추가했다. 처음에는 실행이 끝나면 터미널 출력을 그대로 검토 자료에 붙였다. 며칠 뒤, 실행은 성공으로 표시됐지만 요약 파일이 비어 있는 작업이 발견됐다. 다른 작업에서는 진행 상황과 최종 답이 섞여 요약에 들어갔다. 같은 시간에 실행한 두 작업은 하나의 작업 폴더를 수정해 어느 작업의 변경인지 구분하기 어려워졌다.

담당자는 처음에 요청문을 더 자세히 쓰려 했다. 그러나 요청문만으로는 프로세스의 대기, 출력 형식의 차이, 실패 판정의 누락을 해결할 수 없다. 자동화에는 요청을 만드는 코드 외에 실행을 통제하는 부분과 결과를 해석하는 부분이 필요하다.

변경 내용을 읽고 검토할 항목을 요약하라. 파일을 수정하지 말고, 확인하지 못한 사항은 별도로 적어라.

이 요청은 작업 의도를 표현한다. 읽기 전용 실행을 실제로 강제하는 일은 실행 환경의 책임이다. 답이 비었을 때 실패로 처리하는 일도 해석기의 책임이다. 팀은 세 책임을 분리하고, 결과가 검사를 통과했을 때만 검토 자료로 채택하기로 한다.

비대화형 실행의 입력과 출력 계약

비대화형 실행(headless execution)은 사람이 매번 응답을 입력하지 않아도 요청 하나를 처리하고 끝나는 실행 방식이다. 자동화에서는 요청, 작업 폴더, 권한, 출력 형식, 종료 조건을 실행 전에 정한다. 대화형 터미널에서 우연히 잘 동작한 명령을 복사하는 것만으로 이 계약이 완성되지는 않는다.

다음 표의 도구 명령과 옵션은 모두 2026년 10월 기준 예시다. Google의 Gemini 계열은 Antigravity CLI 중심으로 표시하고 Gemini CLI를 함께 적었다. 실제 실행 환경에서는 설치된 도구의 공식 문서로 적용 범위를 다시 확인한다. 여기서는 실행 형태와 결과 처리 원리에 집중한다.

2026년 10월 기준 예시: 비대화형 진입점과 출력 선택
도구요청을 인자로 전달구조화 출력자동화에서 확인할 점
Claude Codeclaude -p "요청"--output-format json 또는 stream-json출력 형식 옵션은 -p와 함께 사용한다.
Gemini 계열: Antigravity CLIagy -p "요청"--output-format json 또는 stream-json헤드리스 실행에서는 표준 입력으로 준 요청을 읽지 않는 함정이 있어 요청을 인자로 준다.
Gemini 계열: Gemini CLIgemini -p "요청"--output-format json 또는 stream-jsonCode Assist 표준·기업용과 API 키 사용자 쪽에 남은 도구라는 사용 경로를 구분한다.
Codex CLIcodex exec "요청"--json으로 이벤트 출력백그라운드·스크립트에서는 표준 입력을 닫아 입력 대기를 방지한다.

2026년 10월 기준 예시로 Codex CLI의 표준 입력을 닫는 명령 형태는 다음과 같다. 이는 셸 명령 예시이며, 이 장의 Python 프로그램이 실행하는 명령은 아니다.

codex exec "변경 내용을 요약하라" --json < /dev/null

JSON은 형식 이름일 뿐, 모든 도구가 같은 필드와 사건을 출력한다는 뜻은 아니다. JSON 문서 하나를 반환하는 출력은 전체를 읽어 한 번 해석한다. JSON 줄 단위 기록(JSON Lines)은 비어 있지 않은 각 줄을 독립된 JSON 값으로 해석한다. 여러 JSON 객체가 줄마다 이어진 파일을 하나의 JSON 문서로 읽으면 파싱에 실패한다.

스트림에는 답 외에도 시작, 진행, 도구 호출, 오류, 종료에 해당하는 사건이 들어갈 수 있다. 따라서 마지막 줄을 곧바로 마지막 답으로 취급해서는 안 된다. 출력 순서와 답의 역할을 구분해야 한다. 실제 필드와 이벤트 이름은 각 도구의 공식 문서와 저장한 기록으로 확인한다. 해석기에는 확인한 형식만 받아들이는 변환 계층을 둔다.

도구별 기록을 공통 사건으로 변환한 뒤 결과 채택 여부를 판정한다

이 장의 코드는 도구별 원본 형식을 그대로 재현하지 않는다. 교육용 공통 형식을 정의하고 그 형식으로 저장된 기록을 읽는다. 실제 도입에서는 도구별 변환기가 원본 기록을 이 공통 형식으로 바꾸게 한다. 진행 메시지를 답으로 오인하는 문제와 제품별 출력 차이를 한 함수에 섞지 않기 위한 설계다.

완성 프로그램에서만 사용하는 교육용 이벤트 계약
type필수 필드뜻
startat_ms실행 시작. 기록 안의 상대 시간이며 정확히 한 번 등장한다.
answertext, final답 후보. final이 참인 사건 중 마지막 것을 최종 답으로 삼는다.
errormessage실행 중 보고된 오류. 종료 코드가 0이어도 실패 원인이 된다.
endat_ms, exit_code실행 종료. 기록의 마지막 유효 사건이어야 한다.

이 계약에서 마지막 답은 마지막으로 도착한 모든 문자열을 뜻하지 않는다. 명시적으로 최종 답이라고 표시한 답 중 마지막 문자열이다. 마지막 최종 답이 공백뿐이면 앞선 답으로 되돌아가지 않고 실패로 판정한다. 앞선 내용이 폐기됐을 가능성을 해석기가 임의로 무시하지 않기 위해서다.

실행 제한과 CI의 채택 조건

자동화에는 시간 제한(timeout)이 필요하다. 연결이나 입력을 기다리는 프로세스를 끝없이 두지 않기 위해서다. 전체 경과 시간의 상한과, 아무 출력도 오지 않는 시간의 상한은 다른 정책이다. 진행 메시지가 계속 와도 전체 상한을 넘으면 작업을 끝내야 한다. 아래 프로그램은 저장된 기록의 시간을 계산할 뿐 실제 프로세스를 중단하지 않는다. 실제 실행을 감싸는 스크립트가 시간 제한과 종료 후 자식 프로세스 정리를 맡는다.

재시도(retry)는 실패를 성공으로 바꾸는 보편적인 방법이 아니다. 일시적인 실행 장애처럼 재시도 대상으로 정한 경우에만 횟수 상한을 두고 수행한다. 요청이 이미 파일을 수정했다면 같은 요청을 다시 실행할 때 변경이 중복될 수 있다. 재시도 전에 작업 폴더 상태를 확인하고, 각 시도의 기록을 별도로 남긴다. 최종 답이 비었다는 사실만으로 즉시 재시도하지 말고, 원본 기록과 종료 상태부터 확인한다.

동시 실행 수는 작업 큐에서 제한한다. 예를 들어 팀이 실행 슬롯을 두 개로 정했다면 세 번째 작업은 앞선 작업이 끝날 때까지 기다린다. 이 수는 제품의 제한 수치가 아니라 팀이 정한 운영 정책이다. 실행 중인 작업 수에 재시도 작업도 포함해야 한다. 원래 작업과 재시도 작업이 각각 별도 제한을 사용하면 실제 동시 실행 수가 의도보다 늘어난다.

2026년 10월 기준 예시로, 세 도구에서 여러 작업을 동시에 실행할 때는 git worktree로 작업 폴더를 나눠 서로의 변경을 덮지 않게 한다. 다만 폴더 분리는 동시 실행 수 제한을 대신하지 않는다. 공용 외부 자원에 접근하는 작업이라면 그 자원의 동시 접근도 별도로 통제해야 한다.

종료 코드가 0이어도 오류가 있거나 마지막 답이 비면 결과를 채택하지 않는다

CI에서는 대화로 승인을 받을 수 있다고 가정하지 않는다. 읽기만 필요한 작업에는 쓰기 권한과 배포 자격 증명을 주지 않는다. 승인을 묻지 않는 설정이 실행 권한을 작게 만드는 설정과 같다는 생각도 피한다. 권한 범위와 승인 방식은 각각 확인해야 한다.

2026년 10월 기준 예시로 Codex CLI에는 read-only 샌드박스와 never 승인 정책이 있고, Claude Code에는 --allowedTools와 --disallowedTools가 있다. Antigravity CLI에는 터미널을 제한하는 --sandbox가 있다. 옵션 이름이 비슷해도 강제하는 범위가 같다고 가정하지 않는다. 구체적인 조합과 적용 범위는 각 도구의 공식 문서에서 확인하고, CI에서 실제 쓰기 시도가 차단되는지 별도로 검증한다.

CI의 채택 조건은 실행 전에 정한다. 이 장에서는 기록이 계약에 맞고, 종료 코드가 0이며, 오류가 없고, 마지막 최종 답이 공백이 아닐 때만 성공으로 삼는다. 이 조건을 통과해도 답의 사실성과 코드의 품질까지 검증된 것은 아니다. 변경을 만드는 자동화라면 테스트 실행과 diff 검토를 거쳐야 한다.

완성 코드

다음 내용을 main.py로 저장한다. 프로그램은 임시 폴더에 네 개의 기록을 쓰고 다시 읽는다. 성공, 빈 답, 오류 이벤트, 잘못된 JSON을 각각 검사한다. 시간은 기록에 넣은 고정 상대 시간으로 계산하므로 실제 실행 속도와 무관하게 출력이 일정하다. 입력 파일은 프로그램 종료 시 임시 폴더와 함께 정리된다.

import json
import tempfile
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class Result:
    answer: str
    errors: tuple[str, ...]
    elapsed_ms: int | None

    @property
    def ok(self) -> bool:
        return not self.errors


def is_integer(value: object) -> bool:
    return type(value) is int


def parse_log(path: Path) -> Result:
    answer = ""
    errors: list[str] = []
    started: int | None = None
    ended: int | None = None

    for number, line in enumerate(
        path.read_text(encoding="utf-8").splitlines(), start=1
    ):
        if not line.strip():
            continue

        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            errors.append(f"{number}행: JSON 해석 실패")
            continue

        if not isinstance(event, dict):
            errors.append(f"{number}행: 객체가 아님")
            continue

        if ended is not None:
            errors.append(f"{number}행: 종료 뒤 이벤트")
            continue

        kind = event.get("type")

        if kind == "start":
            at_ms = event.get("at_ms")
            if started is not None:
                errors.append(f"{number}행: 시작 중복")
            elif not is_integer(at_ms) or at_ms < 0:
                errors.append(f"{number}행: 시작 시간 오류")
            else:
                started = at_ms
            continue

        if started is None:
            errors.append(f"{number}행: 시작 전 이벤트")
            continue

        if kind == "answer":
            text = event.get("text")
            final = event.get("final")
            if not isinstance(text, str) or type(final) is not bool:
                errors.append(f"{number}행: 답 필드 오류")
            elif final:
                answer = text.strip()

        elif kind == "error":
            message = event.get("message")
            if not isinstance(message, str) or not message.strip():
                errors.append(f"{number}행: 오류 필드 오류")
            else:
                errors.append(f"도구 오류: {message.strip()}")

        elif kind == "end":
            at_ms = event.get("at_ms")
            exit_code = event.get("exit_code")
            if not is_integer(at_ms) or not is_integer(exit_code):
                errors.append(f"{number}행: 종료 필드 오류")
            elif at_ms < started:
                errors.append(f"{number}행: 종료 시간 역전")
            else:
                ended = at_ms
                if exit_code != 0:
                    errors.append(f"종료 코드: {exit_code}")

        else:
            errors.append(f"{number}행: 알 수 없는 이벤트")

    if started is None:
        errors.append("시작 이벤트 없음")
    if ended is None:
        errors.append("종료 이벤트 없음")
    if not answer:
        errors.append("마지막 답이 비어 있음")

    elapsed = None
    if started is not None and ended is not None:
        elapsed = ended - started

    return Result(answer, tuple(errors), elapsed)


def make_fixtures() -> dict[str, list[dict[str, object] | str]]:
    return {
        "success": [
            {"type": "start", "at_ms": 0},
            {"type": "answer", "text": "검토 중", "final": False},
            {"type": "answer", "text": "초안", "final": True},
            {"type": "answer", "text": "검토 항목 2개", "final": True},
            {"type": "end", "at_ms": 1200, "exit_code": 0},
        ],
        "empty": [
            {"type": "start", "at_ms": 0},
            {"type": "answer", "text": "   ", "final": True},
            {"type": "end", "at_ms": 400, "exit_code": 0},
        ],
        "error": [
            {"type": "start", "at_ms": 100},
            {"type": "answer", "text": "부분 결과", "final": True},
            {"type": "error", "message": "읽기 작업 실패"},
            {"type": "end", "at_ms": 800, "exit_code": 0},
        ],
        "broken": [
            {"type": "start", "at_ms": 0},
            "{",
            {"type": "end", "at_ms": 300, "exit_code": 0},
        ],
    }


def write_log(
    path: Path, events: list[dict[str, object] | str]
) -> None:
    lines = [
        event if isinstance(event, str)
        else json.dumps(event, ensure_ascii=False)
        for event in events
    ]
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def check(condition: bool, message: str) -> None:
    if not condition:
        raise RuntimeError(message)


def main() -> None:
    fixtures = make_fixtures()
    results: dict[str, Result] = {}

    with tempfile.TemporaryDirectory() as directory:
        root = Path(directory)
        for name, events in fixtures.items():
            path = root / f"{name}.jsonl"
            write_log(path, events)
            results[name] = parse_log(path)

    expected = {
        "success": ("검토 항목 2개", (), 1200),
        "empty": ("", ("마지막 답이 비어 있음",), 400),
        "error": ("부분 결과", ("도구 오류: 읽기 작업 실패",), 700),
        "broken": (
            "",
            ("2행: JSON 해석 실패", "마지막 답이 비어 있음"),
            300,
        ),
    }

    for name, (answer, errors, elapsed) in expected.items():
        result = results[name]
        check(result.answer == answer, f"{name}: 답 불일치")
        check(result.errors == errors, f"{name}: 오류 불일치")
        check(result.elapsed_ms == elapsed, f"{name}: 시간 불일치")
        check(result.ok == (name == "success"), f"{name}: 판정 불일치")

    print("기록 해석 검증: 4건 통과")
    for name, result in results.items():
        status = "성공" if result.ok else "실패"
        duration = (
            "알 수 없음" if result.elapsed_ms is None
            else f"{result.elapsed_ms}ms"
        )
        answer_text = result.answer or "(없음)"
        error_text = " / ".join(result.errors) or "(없음)"
        print(f"{name}: {status}, 소요={duration}")
        print(f"  마지막 답: {answer_text}")
        print(f"  오류: {error_text}")


if __name__ == "__main__":
    main()

줄별 해설

가져오기와 Result 정의. json은 각 줄을 해석하고, tempfile과 Path는 입력 기록을 임시 파일로 다룬다. Result는 마지막 답, 오류 목록, 소요 시간을 묶는다. frozen=True는 해석을 마친 결과를 실수로 바꾸지 않게 한다. 성공 여부를 별도의 가변 필드로 저장하지 않고 errors가 비었는지로 계산해 서로 모순되는 상태를 줄인다.

is_integer. Python에서는 bool이 int의 하위 형식이다. 단순히 isinstance(value, int)로 검사하면 True를 시간이나 종료 코드로 받아들일 수 있다. 이 계약은 정수만 허용하므로 type(value) is int를 사용한다. JSON을 읽었다고 해서 필드의 자료형까지 검증된 것은 아니다.

parse_log의 읽기 부분. UTF-8로 파일을 읽고 줄 번호를 붙인다. 빈 줄은 건너뛰되, 원래 파일의 줄 번호는 유지한다. 해석할 수 없는 줄은 오류로 남기고 다음 줄을 읽는다. 이렇게 하면 손상 뒤에 남아 있는 종료 정보도 확인할 수 있다. 다만 오류가 한 번이라도 기록됐으므로 뒤의 정상 사건이 앞의 손상을 성공으로 덮지 못한다.

사건 순서 검사. JSON 값이 객체인지 먼저 확인한다. 유효한 종료 사건 뒤에 다른 사건이 오면 계약 위반이다. 시작은 한 번만 허용하고, 시작 전의 답이나 오류도 받아들이지 않는다. 이 순서 검사는 실행 기록이 중간부터 잘렸거나 서로 다른 실행의 기록이 합쳐진 상황을 발견하는 데 도움이 된다.

answer 처리. text가 문자열이고 final이 불리언인지 확인한다. final이 거짓인 진행성 답은 최종 답을 바꾸지 않는다. 참이면 공백을 제거한 text로 answer를 덮어쓴다. 성공 사례에 초안과 마지막 답을 함께 넣은 이유는 마지막 최종 답을 선택하는 동작을 검증하기 위해서다.

error와 end 처리. 오류 메시지는 비어 있지 않은 문자열이어야 한다. 오류가 오면 답이 이미 있어도 실패 원인으로 보존한다. 종료 사건은 시간과 종료 코드의 형식을 확인하고 시작보다 앞선 종료 시간을 거부한다. 종료 코드가 0이 아니면 그 값도 오류 목록에 넣는다. 예제의 error 기록은 일부러 종료 코드를 0으로 둬 오류 이벤트 검사의 필요성을 보여 준다.

파일 끝의 검사. 반복문을 끝낸 뒤 시작과 종료가 모두 있었는지 확인한다. 마지막 답이 없으면 별도 오류를 추가한다. 종료 사건 누락과 빈 답은 동시에 발생할 수 있으므로 둘 중 하나만 보고 나머지를 숨기지 않는다. 시간을 계산할 수 없으면 None을 사용한다. 알 수 없는 시간을 0으로 바꾸면 즉시 완료된 실행과 구분할 수 없다.

입력 생성과 검증. make_fixtures는 실제 서비스 기록이 아닌 고정 예제 데이터를 만든다. write_log는 정상 사건을 JSON으로 바꾸고, broken 사례의 문자열은 그대로 써서 손상된 줄을 만든다. check는 예상 답, 오류, 시간, 판정을 모두 비교한다. 검증이 실패하면 예외로 끝나므로 성공 메시지가 출력되지 않는다.

출력과 프로그램의 종료 의미. 네 기록 중 세 기록은 의도적으로 실패한다. 그러나 이 프로그램의 목적은 그 실패를 올바르게 감지하는지 검증하는 것이다. 따라서 예상한 실패를 모두 감지하면 프로그램 자체는 정상 종료한다. 실제 CI에서 단일 작업을 판정하는 프로그램으로 바꿀 때는 Result.ok가 거짓이면 실행 전체를 실패로 종료하도록 연결해야 한다.

이 구현은 작은 저장 기록을 한 번에 읽는다. 큰 기록에서는 파일 객체를 줄마다 순회해 메모리 사용을 줄일 수 있다. 파일 접근 오류는 정상 결과로 바꾸지 않고 예외로 드러낸다. 오류를 숨겨 빈 기록으로 대체하면 원래의 파일 문제와 빈 답 문제를 구분하기 어려워진다.

실행 결과

저장한 폴더에서 다음 명령을 실행한다. 컴파일 확인 명령은 성공하면 아무것도 출력하지 않는다. 이어서 실행 명령의 출력을 아래 예상 결과와 비교한다.

python3 -W error -m py_compile main.py
python3 main.py

기록 해석 검증: 4건 통과
success: 성공, 소요=1200ms
  마지막 답: 검토 항목 2개
  오류: (없음)
empty: 실패, 소요=400ms
  마지막 답: (없음)
  오류: 마지막 답이 비어 있음
error: 실패, 소요=700ms
  마지막 답: 부분 결과
  오류: 도구 오류: 읽기 작업 실패
broken: 실패, 소요=300ms
  마지막 답: (없음)
  오류: 2행: JSON 해석 실패 / 마지막 답이 비어 있음

error 사례에는 답도 있고 소요 시간도 있다. 그러나 오류 사건이 있으므로 채택하지 않는다. broken 사례에서는 손상된 줄 뒤의 종료 사건을 읽어 소요 시간을 계산하지만, 해석 실패를 보존한다. 진단 정보를 얻었다는 사실과 결과를 사용할 수 있다는 판단은 서로 다르다.

팀 저장소에 넣기 전에는 코드 실행과 함께 diff를 검토한다. 특히 성공 판정식이 느슨해지지 않았는지, 오류를 무시하는 예외 처리가 추가되지 않았는지 살핀다. AI가 수정한 코드라도 동일한 검증을 적용한다.

실무에서 자주 틀리는 것

종료 코드만 성공 조건으로 사용한다

다음 코드는 정상 종료한 빈 결과를 성공으로 판정한다. 오류 메시지가 별도로 전달되는 상황도 반영하지 못한다.

def accepted(exit_code: int, answer: str, errors: list[str]) -> bool:
    return exit_code == 0

고친 코드는 종료 코드, 답, 오류를 함께 검사한다. 완성 프로그램에서는 기록 구조 검사까지 마친 뒤 같은 취지로 성공 여부를 계산한다.

def accepted(exit_code: int, answer: str, errors: list[str]) -> bool:
    return exit_code == 0 and bool(answer.strip()) and not errors

줄 단위 기록을 JSON 문서 하나로 읽는다

다음 함수는 JSON 문서 하나를 읽을 때 사용할 수 있지만, 여러 객체가 줄마다 이어지는 입력에는 맞지 않는다.

import json


def read_events(text: str) -> object:
    return json.loads(text)

고친 함수는 비어 있지 않은 줄마다 해석한다. 이 짧은 함수는 읽는 단위만 보여 준다. 실제 채택 판정에는 완성 코드처럼 객체 형식, 사건 순서, 필수 필드를 검사하는 과정이 더 필요하다.

import json


def read_events(text: str) -> list[object]:
    return [
        json.loads(line)
        for line in text.splitlines()
        if line.strip()
    ]

새로운 최종 답이 비면 이전 답을 되살린다

다음 코드는 마지막 최종 답이 빈 문자열일 때 앞선 답을 남긴다. 그러면 실제 마지막 답이 비었다는 사실을 놓친다.

def update_answer(previous: str, text: str, final: bool) -> str:
    if final and text.strip():
        return text.strip()
    return previous

고친 코드는 최종 답 사건이면 빈 문자열도 반영한다. 빈 결과를 실패로 처리하는 책임은 이후 판정 단계에 둔다.

def update_answer(previous: str, text: str, final: bool) -> str:
    if final:
        return text.strip()
    return previous

성공할 때까지 재시도한다

다음 함수는 계속 실패하는 작업을 끝내지 못한다. 변경을 만드는 작업이라면 부작용도 반복될 수 있다.

from collections.abc import Callable


def run_with_retry(action: Callable[[], bool]) -> bool:
    while True:
        if action():
            return True

고친 함수는 전체 시도 횟수에 상한을 둔다. 이 예제의 action은 부작용 없이 다시 실행할 수 있고, 거짓 반환이 재시도 대상으로 분류된 경우라는 전제다. 실제 실행에서는 시도별 기록, 대기 정책, 남은 전체 시간을 함께 관리한다.

from collections.abc import Callable


def run_with_retry(action: Callable[[], bool]) -> bool:
    for _ in range(2):
        if action():
            return True
    return False

한눈에 보기

자동화가 결과를 채택하기 전에 확인할 경계
경계확인할 내용실패했을 때
실행시간 상한, 입력 대기, 종료 상태실행을 정리하고 해당 시도의 기록을 보존한다.
해석JSON 형식, 필드 형식, 사건 순서해석 오류를 남기고 결과를 채택하지 않는다.
결과오류 없음, 정상 종료, 비어 있지 않은 마지막 답빈 답과 부분 결과를 성공으로 넘기지 않는다.
재시도대상 오류, 횟수 상한, 반복 실행의 부작용정해진 상한에서 멈추고 검토 대상으로 남긴다.
동시 실행실행 슬롯과 작업 폴더 분리대기열에 두고 재시도도 같은 제한에 포함한다.
CI 권한작업에 필요한 접근 범위와 자격 증명권한을 넓히기 전에 작업 범위와 실패 원인을 확인한다.
변경 채택테스트 결과와 diff해석기 성공만으로 변경을 병합하지 않는다.

도구를 호출하는 부분은 제품별로 달라져도, 결과를 받아들이는 기준은 팀이 소유할 수 있다. 기준을 코드와 고정 입력으로 남기면 도구를 바꾸거나 버전을 올린 뒤에도 같은 검사를 반복할 수 있다. 다음 장에서 외부 도구 연결을 추가하더라도 이 채택 경계는 유지한다.

연습 문제

  1. 성공 기록의 마지막 최종 답을 공백 문자열로 바꿔라. 앞선 “초안”을 채택하지 않고 실패하는지 예상값을 수정해 확인하라.
  2. 종료 사건이 없는 기록을 추가하라. 답은 존재하도록 만들고, 오류 목록과 소요 시간이 어떻게 나와야 하는지 설명하라.
  3. 종료 코드가 7이고 답은 존재하는 기록을 추가하라. 오류 이벤트가 없어도 실패하는지 검증하라.
  4. 팀의 동시 실행 슬롯이 두 개인 상황에서 실행 중인 작업 두 개와 재시도 대기 작업 한 개가 있다. 재시도 작업을 언제 시작해야 하며, 작업 폴더 분리만으로 충분하지 않은 이유를 설명하라.

정답과 해설

  1. 마지막 최종 답 사건이 answer를 빈 문자열로 덮어쓴다. 오류는 “마지막 답이 비어 있음”이고 소요 시간은 1200ms다. 앞선 답을 되살리면 마지막 결과의 결손을 숨기므로 실패 판정을 유지한다. 성공 사례를 바꿨다면 기존의 성공 여부 예상도 함께 고쳐야 한다.
  2. 유효한 시작과 답이 있고 종료만 없다면 오류는 “종료 이벤트 없음”이다. elapsed_ms는 None이다. 답이 있다는 이유로 실행 완료를 추정하지 않는다. 출력에서는 소요 시간을 “알 수 없음”으로 표시한다.
  3. 오류 목록에 “종료 코드: 7”이 들어가며 실패한다. 마지막 답은 진단 목적으로 남길 수 있지만 채택하지 않는다. 오류 이벤트와 프로세스 종료 상태는 서로 보완하는 신호다.
  4. 실행 중인 작업 하나가 종료되어 슬롯이 반환된 뒤 재시도 작업을 시작한다. 재시도도 실행 자원을 소비하므로 같은 제한을 적용한다. 작업 폴더 분리는 변경 충돌을 줄이지만 프로세스 수와 공용 자원 사용량까지 제한하지는 않는다.
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

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

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