← 목차
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 명령은 무엇일까요?
A.
uv install --tree
B.
uv tree
C.
uv graph dependencies
D.
uv lock --show
정답: 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 두 파일에 대한 설명으로 가장 정확한 것은 무엇일까요?
A.
pyproject.toml만 커밋하고 uv.lock은 .gitignore에 넣어 머신마다 새로 만든다.
B.
pyproject.toml은 설계 의도(직접 의존성 + 메타데이터)를, uv.lock은 transitive까지 고정한 재현 가능성을 담당하며 둘 다 커밋한다.
C.
uv.lock은 Maven의 pom.xml에 정확히 대응하고, pyproject.toml은 IDE 설정 파일에 가깝다.
D.
두 파일은 동일한 정보를 다른 포맷으로 표현하므로 한 쪽만 있어도 빌드가 재현된다.
정답: 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개**를 고르세요.
A.
`if user is None:` 으로 None 비교를 한다.
B.
`if my_list != None:` 으로 빈 리스트를 거르려고 한다.
C.
들여쓰기가 어긋난 블록을 작성해 IndentationError가 난다.
D.
함수 시그니처에 `-> None` 으로 반환 타입을 명시한다.
E.
`for x in xs:` 다음 줄을 4-space 들여쓰기로 시작한다.
정답: 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` 를 피하려면 어떤 명령이 가장 적절할까요?
A.
`uv run python app/api/routes.py`
B.
`cd app/api && uv run python routes.py`
C.
`uv run python -m app.api.routes`
D.
`PYTHONPATH=. uv run python app/api/routes.py`
정답: 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() ```
A.
`s.upper()` 호출에서 `None`에 `.upper()`를 호출할 가능성이 있다며 에러를 낸다.
B.
`s is None` 분기 후 narrowing이 적용되어 `s.upper()` 줄에서는 `s`의 타입이 `str`로 좁혀지므로 통과한다.
C.
`str | None` 문법은 Python 3.13에서 deprecated이므로 `Optional[str]`로 바꾸라고 경고한다.
D.
런타임 검증이 없으므로 mypy는 type hint를 모두 무시하고 항상 통과한다.
정답: 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`의 가장 본질적인 차이로 옳은 것은 무엇일까요?
A.
Protocol은 추상 메서드 선언을 허용하지 않으므로 Java interface보다 표현력이 약하다.
B.
Protocol은 structural subtyping이라 `implements` 같은 명시적 선언 없이 메서드 시그니처만 일치하면 호환되는 반면, Java interface는 nominal이라 `implements` 선언이 필수다.
C.
Protocol은 런타임에 isinstance 검사를 강제하므로 Java interface보다 더 엄격하다.
D.
Protocol은 외부 라이브러리 클래스에 적용할 수 없고, Java interface는 라이브러리 클래스에도 retrofit 가능하다.
정답: 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: ...`
제출하고 채점하기