유틸리티 스크립트와 CLI 도구 개발 — Bash/Gradle task를 대체하는 Python · 퀴즈

7 문항 · Bloom: Apply:3, Analyze:3, Create:1

Q1 Apply mcq_single

다음 Typer 함수 시그니처가 만들어 내는 CLI 형태로 가장 정확한 것은? ```python @app.command() def greet(name: str, count: int = 1, shout: bool = False): ... ```

정답: B
Typer는 기본값이 없는 매개변수(`name`)를 위치 인자로, 기본값이 있으면 옵션(`--count`)으로 매핑합니다. `bool` 타입은 자동으로 `--shout` / `--no-shout` 한 쌍의 토글 플래그로 풀립니다. 어노테이션 없이 시그니처만으로 이 라우팅이 나오는 것이 picocli의 `@Option`/`@Parameters`와의 본질적 차이입니다.
오답 해설:
  • A. 기본값 유무가 옵션/위치 인자를 가르는 신호인데, `name`은 기본값이 없어 옵션이 아니라 위치 인자가 됩니다.
  • C. `bool` 매개변수는 위치 인자가 아니라 `--shout`/`--no-shout` 플래그로 변환됩니다.
  • D. Typer는 picocli처럼 `--option=value` 강제가 아니라 공백 구분 형식이 기본입니다.
Q2 Analyze mcq_single

Spring Shell의 `@ShellMethod`나 picocli의 `@Command` 어노테이션 없이 Typer가 함수 시그니처만으로 CLI 메타데이터를 추출할 수 있는 근본 메커니즘은 무엇인가?

정답: C
Typer는 런타임에 `inspect.signature(func)`로 매개변수 객체를 얻고 `__annotations__` 딕셔너리에서 타입 힌트를 읽어 Click 파서로 변환합니다. 어노테이션 객체가 따로 없어도 PEP 3107 이후 함수 시그니처 자체가 메타데이터 컨테이너 역할을 하기 때문에 가능합니다.
오답 해설:
  • A. Python에는 자동 어노테이션 byte code 주입 같은 단계가 없습니다 — 시그니처는 단순히 런타임 객체로 존재합니다.
  • B. Typer는 소스 파싱을 하지 않습니다. 이미 import된 함수 객체의 introspection만으로 충분합니다.
  • D. JVM 비유가 잘못 적용된 케이스입니다. Python은 JVM 위에서 돌지 않으며 generic erasure는 Python introspection과 무관합니다.
Q3 Apply mcq_single

사용자가 입력한 파일명을 받아 `git log -- <filename>`을 실행해야 합니다. 가장 안전한 호출 형태는?

정답: C
`args`를 list로 넘기면 셸을 거치지 않으므로 메타문자(`;`, `&&`, `$()`)에 의한 injection이 원천 차단됩니다. Java의 `ProcessBuilder(List<String>)`와 같은 안전 모델입니다. `check=True`까지 붙이면 종료 코드 ≠ 0일 때 `CalledProcessError`가 발생해 호출 측에서 실패를 감지할 수 있습니다.
오답 해설:
  • A. `shell=True` + f-string은 가장 위험한 패턴입니다 — 사용자 입력이 그대로 셸에 흘러 들어가 injection이 가능합니다.
  • B. `shlex.quote`를 쓰더라도 `shell=True`를 켜는 순간 셸 해석 단계가 추가돼 표면적이 늘어납니다. list args가 같은 결과를 더 단순하게 줍니다.
  • D. 구식 셸 호출 헬퍼는 내부적으로 `shell=True`와 동등하며 종료 코드 처리도 미흡해 production 코드에서 권장되지 않습니다.
Q4 Analyze mcq_multi

`subprocess.run(args=[...])` 대신 `asyncio.create_subprocess_exec`를 선택해야 하는 합리적 상황을 모두 고르시오. (정답 2개)

정답: A, C
asyncio.create_subprocess_exec는 (1) 동시 N개 프로세스를 thread pool 없이 띄우거나 (2) stdout을 라인 단위로 스트리밍하면서 다른 코루틴과 인터리빙할 때 의미가 있습니다. event loop가 socket/pipe를 multiplexing해 주기 때문이며, I/O 대기 시간이 겹칠 때만 효과가 납니다.
오답 해설:
  • B. `shell=True`와 비동기는 직교하는 축입니다. asyncio를 쓴다고 shell injection이 안전해지지 않습니다.
  • D. 한 번의 단발성 호출은 동기 `subprocess.run` 한 줄이 더 단순합니다 — 비동기는 오히려 오버엔지니어링.
  • E. CPU bound 작업 1개는 event loop의 multiplexing 이점이 없으므로 동기 호출이 적합합니다.
Q5 Apply mcq_single

다음 httpx 클라이언트 설정에서 production 외부 호출에 가장 큰 위험으로 남아 있는 부분은? ```python async with httpx.AsyncClient( base_url='https://api.example.com', transport=httpx.AsyncHTTPTransport(retries=3), ) as c: r = await c.get('/items') r.raise_for_status() ```

정답: C
httpx의 timeout default가 명시적으로 설정되지 않으면 hang 시 worker가 영원히 대기할 수 있고, transport `retries=N`은 connection-level(TCP/DNS 실패)만 다룹니다. 5xx/429 같은 status code 기반 재시도는 tenacity 같은 application-level 데코레이터가 따로 필요합니다. `Timeout(connect=, read=, write=, pool=)` 4종을 명시하고 tenacity로 status 재시도를 보강해야 OkHttp + Resilience4j 수준이 됩니다.
오답 해설:
  • A. `base_url` 지정은 오히려 권장 패턴입니다 — connection pool 재사용과 일관성에 유리합니다.
  • B. `raise_for_status()`는 4xx/5xx를 예외로 승격하는 표준 패턴이며 위험 요소가 아닙니다.
  • D. `with` 블록은 connection pool 정리를 보장하는 권장 패턴입니다 — OkHttpClient close()와 같은 역할.
Q6 Analyze true_false

OkHttp의 `Interceptor`처럼 `httpx.Auth` 서브클래스를 만들어 모든 요청에 `Authorization` 헤더를 주입하면, 그것만으로 OkHttp `Interceptor` 체인의 모든 기능(요청 변형, 재시도 결정, 응답 캐싱 훅 등)을 동등하게 대체할 수 있다.

정답: B
False입니다. `httpx.Auth.auth_flow`는 인증 흐름(헤더 주입, 401 재시도용 두 번째 yield) 전용 훅입니다. 일반적인 요청 변형·로깅·캐싱은 `httpx`의 **event hooks**(`event_hooks={'request': [...], 'response': [...]}`)나 custom `Transport`로 처리해야 하며, 이는 OkHttp `Interceptor` 체인과 책임이 분리된 모델입니다. JVM 직관 그대로 'Auth = Interceptor' 매핑은 오해입니다.
오답 해설:
  • A. True로 답하면 httpx의 책임 분리(Auth vs event hooks vs Transport)를 OkHttp의 단일 Interceptor 체인과 동일시하는 흔한 오개념입니다.
Q7 Create short_answer

팀 동료에게 README나 venv 안내 없이 한 줄 명령으로 실행 가능한 'PR 통계 수집 CLI'를 single-file로 만들어 공유하려고 합니다. 다음 4가지를 각각 한 문장씩 설명하시오. 1. 파일 상단에 추가해야 할 PEP 723 메타데이터 블록의 형태와 필수 키 2개 2. 동료가 실행할 한 줄 명령(gist raw URL 가정) 3. 첫 실행과 두 번째 실행의 속도 차이가 나는 이유 4. single-file을 졸업하고 정식 `pyproject.toml` 패키지로 가야 할 임계점 1가지

채점 기준:
  • (1) `# /// script` ... `# ///` 블록을 코드 상단에 두고, `requires-python`(예: `>=3.13`)과 `dependencies`(TOML array, 예: `["typer>=0.12", "httpx>=0.27"]`) 두 키를 포함한다고 명시 (1pt)
  • (2) `uvx --from <gist-raw-url> <command>` 형태(또는 `uv run <raw-url>`) 한 줄 명령을 정확히 제시 (1pt)
  • (3) 첫 실행은 의존성 다운로드 + resolve 비용이 들지만, 두 번째 실행부터는 `uv`가 resolve 결과와 wheel을 캐싱해 거의 즉시 시작된다고 설명 (1pt)
  • (4) 다음 중 하나를 임계점으로 제시: 의존성이 5개를 넘어선다 / 다중 모듈로 쪼개야 한다 / 단위 테스트 폴더가 필요하다 / `uv.lock`이 동반된 production 배포가 필요하다 (1pt)
  • (보너스) JVM 비교 — jbang `//DEPS` 주석 또는 shaded jar 빌드 단계와의 등가성을 한 줄이라도 언급 (1pt)