Python 입문 — JVM 개발자 관점에서 보는 Python 3.13 · 퀴즈

7 문항 · Bloom: Remember:0, Understand:3, Apply:2, Analyze:2, Evaluate:0, Create:0

Q1 Understand mcq_single

Maven/Gradle 사용자가 'mvn dependency:tree로 의존성 그래프를 본다'고 할 때, 같은 일을 하는 uv 명령은 무엇일까요?

정답: B
uv는 Maven/Gradle의 의존성 그래프 탐색 기능을 `uv tree`라는 단일 서브커맨드로 제공합니다. pyproject.toml에 선언된 직접 의존성과 uv.lock에 잠겨 있는 transitive 의존성을 트리 형태로 그려 줍니다. uv 하나가 jenv + Maven + Gradle Wrapper 역할을 통합한다는 큰 그림을 떠올리면 명령 이름이 단순해지는 게 당연합니다.
오답 해설:
  • A. `uv install`은 uv에 존재하지 않는 명령입니다. 의존성 추가는 `uv add`이고, 그래프 출력 옵션도 따로 없습니다. pip의 `pip install` 멘탈 모델을 그대로 옮긴 흔한 오해입니다.
  • C. `uv graph` 같은 서브커맨드는 없습니다. Maven의 `dependency:tree`라는 'goal:detail' 형태에 익숙해서 비슷한 패턴을 추측한 결과로 보이는 함정입니다.
  • D. `uv lock`은 lock 파일을 재계산하는 명령이고 `--show` 옵션은 없습니다. lock 파일을 출력해도 트리 형태로 정리해 주지는 않으므로 의도가 다릅니다.
Q2 Understand mcq_single

uv init이 만들어 주는 pyproject.toml과 uv.lock 두 파일에 대한 설명으로 가장 정확한 것은 무엇일까요?

정답: B
pyproject.toml은 'name/version/dependencies'처럼 사람이 선언하는 설계 의도이고, uv.lock은 transitive까지 고정한 reproducible-build 잠금 파일입니다. 역할이 다르므로 둘 다 git에 커밋해야 팀원과 CI가 동일한 환경을 재현할 수 있습니다. JVM 비유로는 pyproject.toml ≈ pom.xml + build.gradle, uv.lock ≈ Gradle dependency-locks/.lockfile입니다.
오답 해설:
  • A. Java 진영에서 pom.xml만 커밋하고 lock 같은 개념은 없던 경험을 그대로 옮긴 흔한 오해입니다. uv.lock을 제외하면 'transitive 버전이 머신마다 달라지는' 재현성 문제가 그대로 돌아옵니다.
  • C. 역할이 정반대로 뒤집혀 있습니다. pom.xml에 대응하는 것은 pyproject.toml(선언)이고, uv.lock은 lock 전용입니다. IDE 설정과는 무관합니다.
  • D. 두 파일은 정보가 겹치지 않습니다. pyproject.toml에는 직접 의존성과 버전 범위만, uv.lock에는 해석된 정확한 버전과 해시가 들어 있어 서로 보완 관계입니다.
Q3 Understand mcq_multi

Java/Kotlin 베테랑이 처음 Python 코드를 PR에 올릴 때 흔히 저지르는 '진짜 오류 또는 안티패턴' **2개**를 고르세요.

정답: B, C
두 정답 모두 첫 Python PR에서 자주 보이는 실수입니다. (B) `!= None`은 안티패턴 — None은 NoneType의 싱글톤이므로 `is None`/`is not None`이 관용이고, 게다가 'None인지'와 '비었는지'를 동시에 확인하고 싶다면 truthy 평가 한 줄(`if not my_list:`)로 끝납니다. (C) Python에서 들여쓰기는 컨벤션이 아니라 블록 구조 자체이므로 어긋나면 IndentationError로 실행 자체가 막힙니다. 나머지 (A)(D)(E)는 모두 권장되는 정상 코드 패턴입니다.
오답 해설:
  • A. 오히려 권장 패턴입니다. None 비교의 관용은 `==`이 아닌 `is`이며, 베테랑이라면 처음부터 이렇게 쓰는 것이 정답입니다.
  • D. `-> None`은 'void 같은' 반환을 시그니처에 명시하는 정상적인 type hint입니다. Java의 `void`에 가까운 역할이라 오히려 Java 개발자에게 친숙한 표기입니다.
  • E. PEP 8이 권장하는 정확한 들여쓰기입니다. 4-space는 Python 커뮤니티의 사실상 표준이라 함정이 될 수 없습니다.
Q4 Apply mcq_single

다음 디렉토리 구조에서 `app/api/routes.py`는 `from ..domain.user import User` 로 상대 import를 사용합니다. ``` app/ __init__.py api/__init__.py api/routes.py domain/__init__.py domain/user.py ``` 프로젝트 루트에서 routes.py를 실행할 때 `ImportError: attempted relative import with no known parent package` 를 피하려면 어떤 명령이 가장 적절할까요?

정답: C
상대 import(`from ..domain ...`)는 모듈이 어떤 패키지에 속해 있는지 인터프리터가 알 때만 해석할 수 있습니다. `python -m app.api.routes` 형태로 실행하면 `app.api.routes`가 패키지 컨텍스트로 로드되므로 `..domain`이 `app.domain`으로 정상 해석됩니다. JVM 비유로는 패키지 선언과 디렉토리 결합이 javac에서 강제되듯, Python에서는 `-m` 실행이 그 결합을 살려 주는 안전한 방법입니다.
오답 해설:
  • A. 스크립트로 직접 실행하면 `__name__ == '__main__'`이 되고 부모 패키지 정보가 사라져서 상대 import가 즉시 깨집니다. Java의 `java com.example.Main` 같은 친숙함만 보고 골랐다가 정확히 함정에 걸리는 패턴입니다.
  • B. 디렉토리를 바꿔도 상대 import에 필요한 '패키지 컨텍스트'는 생기지 않습니다. 오히려 `app` 패키지의 root에서 멀어져 import 해석이 더 어려워집니다.
  • D. PYTHONPATH 조작은 절대 import에는 도움이 될 수 있지만, 스크립트로 직접 실행하는 형태에서 `..domain` 같은 상대 import는 여전히 부모 패키지 정보를 얻지 못합니다. classpath에 jar 추가하면 해결되리라는 JVM 직관이 안 통하는 자리입니다.
Q5 Apply mcq_single

다음 Python 코드에 대해 mypy strict 모드가 어떻게 반응할지 가장 정확히 설명한 선택지는 무엇일까요? ```python def shout(s: str | None) -> str: if s is None: return '' return s.upper() ```

정답: B
mypy는 control-flow analysis로 타입을 좁히는 narrowing을 지원합니다. `if s is None: return ''` 분기를 거치면 그 다음 줄에서 `s`의 타입은 `str`로 자동 좁혀져 `.upper()` 호출이 안전하다고 판단합니다. Kotlin의 smart cast와 같은 발상입니다. `str | None`(PEP 604)과 `Optional[str]`은 등가이며 둘 다 3.10+에서 권장됩니다.
오답 해설:
  • A. narrowing이 없다고 가정한 답입니다. `if s is None: return` 같은 가드를 mypy가 인식하지 못한다면 맞겠지만, 실제로는 정확히 이런 패턴을 narrowing으로 처리합니다.
  • C. 정반대입니다. `str | None`(PEP 604)은 Python 3.10부터 정식 도입된 권장 표기이고, `Optional[str]`은 별칭으로 여전히 유효하지만 새 코드에는 union 문법이 더 자주 쓰입니다. deprecated 관계가 아닙니다.
  • D. 런타임에 hint가 무시되는 것은 사실이지만 mypy의 존재 이유가 정적 검증입니다. type hint를 무시한다면 mypy가 잡아 주는 NPE 류 버그를 모두 놓쳐 도구의 가치가 사라집니다.
Q6 Analyze mcq_single

Spring에서 `interface UserRepository` + `class JpaUserRepositoryImpl implements UserRepository` 패턴을 Python으로 옮기려고 합니다. `typing.Protocol`과 Java `interface`의 가장 본질적인 차이로 옳은 것은 무엇일까요?

정답: B
본질적인 차이는 명목적(nominal) vs 구조적(structural) 타입 시스템입니다. Java interface는 `implements Greeter` 같은 명시적 선언이 있어야 호환으로 인정되지만, Protocol은 메서드 이름과 시그니처만 일치하면 mypy가 자동으로 호환으로 추론합니다. 이 차이 덕분에 외부 라이브러리 클래스를 수정하지 않고도 내가 정의한 Protocol에 끼워 맞출 수 있어 adapter 클래스가 불필요해집니다.
오답 해설:
  • A. Protocol도 `def greet(self) -> str: ...` 형태로 본문을 생략한 추상 메서드를 선언할 수 있습니다. 표현력이 약한 게 아니라 호환 판정 방식이 다른 것입니다.
  • C. 기본 Protocol은 mypy가 정적으로 검증할 뿐 isinstance를 강제하지 않습니다. `@runtime_checkable`을 붙여야 isinstance가 가능해지고, 이마저도 강제가 아니라 옵션입니다. '더 엄격하다'는 정확히 정반대 인상입니다.
  • D. 사실 관계가 정확히 뒤집혀 있습니다. Protocol의 가장 빛나는 자리가 바로 외부 라이브러리 클래스에 사후 적용 가능하다는 점이고, Java interface는 외부 클래스에 implements를 붙일 방법이 없어 보통 wrapper/adapter가 필요합니다.
Q7 Analyze short_answer

Spring에서 `interface PaymentGateway`로 의존성 주입을 하던 자리를 Python으로 옮긴다고 합시다. (1) Protocol과 ABC(`abc.ABC` + `@abstractmethod`) 중 어느 쪽을 기본으로 선택할지 한 줄로 답하고, (2) 그 선택의 근거를 nominal vs structural 관점에서 1~2문장으로 설명한 뒤, (3) 함수 시그니처에 어떤 type hint를 쓸지 한 줄 코드로 보여주세요.

이 문항은 LO-S1-4(type hints 적용)와 LO-S1-5(duck typing vs nominal 분석)를 동시에 평가합니다. 의존성 주입처럼 '계약 모양만 맞으면 누구든 받겠다'는 자리에는 Protocol이 더 Pythonic합니다 — 외부 라이브러리 클래스도 코드 수정 없이 호환되기 때문입니다. ABC는 명시적 계약과 isinstance 검사가 필요할 때, 또는 default implementation을 공유해야 할 때 적합합니다. 함수 시그니처에 Protocol 타입을 넣으면 mypy가 호출 측까지 검증해 줍니다.채점 기준:
  • (1) Protocol 또는 ABC 중 하나를 명확히 선택했다 (1pt)
  • (2) nominal vs structural 차이를 의존성 주입 맥락과 연결해 설명했다 (2pt)
  • (2-bonus) Protocol 선택 시 'adapter 불필요/외부 라이브러리 호환' 또는 ABC 선택 시 'isinstance/default impl' 같은 구체 근거를 들었다 (1pt)
  • (3) 함수 시그니처에 선택한 타입을 매개변수 annotation으로 사용한 코드 한 줄을 제시했다 (1pt)
  • 예시 모범 답안: (1) 보통은 Protocol을 기본으로 선택한다. (2) 의존성 주입 자리는 '메서드 시그니처가 일치하는 모든 구현을 받아들이고 싶다'는 요구가 강해서 structural subtyping인 Protocol이 외부 라이브러리 어댑터까지 포함하기 좋다. ABC는 isinstance 검사나 default implementation 공유가 필요할 때만 명시적 nominal 계약으로 선택한다. (3) `def charge(gateway: PaymentGateway, amount: int) -> Receipt: ...`