Python 백엔드 면접 준비

Python 면접 질문과 답변 2026

백엔드 개발자를 위한 집중형 Python 면접 질문 세트입니다. 각 답변은 말로 설명하기 좋은 구조로 활용하세요: 개념을 먼저 말하고, 구체적인 예시를 더한 뒤, 트레이드오프를 언급하세요.

Python 모의 면접 시작신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

Python 핵심 데이터 모델

1Python에는 어떤 내장 데이터 타입이 있으며, 그중 변경 가능한 타입은 무엇인가요?

Python에는 NoneType, bool, int, float, complex와 같은 스칼라 타입, str, bytes, bytearray와 같은 텍스트 및 바이너리 타입, list, tuple, range와 같은 시퀀스 타입, dict, set, frozenset과 같은 컬렉션 타입이 있습니다. 면접에서 중요한 구분점은 변경 가능성(mutability)입니다. list, dict, set, bytearray는 제자리에서 내용을 변경할 수 있는 반면, 숫자형, bool, str, bytes, tuple, range, frozenset, None은 변경 불가능합니다. 튜플 자체의 요소 참조는 고정되어 있지만, 튜플이 참조하는 변경 가능한 객체의 내부 상태는 바뀔 수 있다는 점도 유의할 만한 차이점입니다.

AI 코치와 함께 이 질문에 답해 보세요

2실제 코드에서 list, tuple, set, dict는 어떻게 다른가요?

list는 순서가 있고 변경 가능한 시퀀스이므로 작업 큐, 수집된 결과물, 변경되는 순서형 데이터에 적합합니다. tuple은 순서가 있지만 변경 불가능하므로 고정된 레코드, 함수 반환값, 재할당되지 않아야 하는 값에 유용합니다. set은 고유하고 해시 가능한 객체를 저장하며 포함 여부 확인이나 중복 제거에 이상적입니다. dict는 해시 가능한 키를 값에 매핑하며 인덱스, 조회(lookup), JSON 형태의 데이터에 일반적으로 사용되는 구조입니다. 최신 Python에서 dict는 삽입 순서를 유지하지만, 주된 목적은 여전히 키 기반 접근입니다.

items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
AI 코치와 함께 이 질문에 답해 보세요

3is와 ==의 차이점은 무엇인가요?

==는 대개 __eq__ 메서드를 통해 두 객체의 값이 같은지 비교합니다. 반면 is는 두 참조가 메모리상에서 정확히 동일한 객체를 가리키고 있는지 비교합니다. 프로덕션 코드에서 is는 주로 value is None과 같은 싱글톤 검사나 객체 동일성이 실제로 중요한 경우, 또는 센티널(sentinel) 객체를 확인할 때만 사용해야 합니다. 작은 정수 캐싱이나 문자열 인턴(interning)과 같은 구현 세부 사항에 의존해서는 안 됩니다. 이러한 구현 세부 사항 때문에 예제에서는 is가 올바르게 작동하는 것처럼 보일 수 있지만, 의미론적으로는 잘못된 선택입니다.

value = None

if value is None:
    print("missing")
AI 코치와 함께 이 질문에 답해 보세요

4해시 가능한(hashable) 객체란 무엇이며, 이것이 왜 중요한가요?

해시 가능한 객체는 수명 동안 변하지 않는 해시값을 가지며, 동등성 비교 동작이 해당 해시값과 일관성을 유지하는 객체입니다. `dict`의 키와 `set`의 요소가 이 특성에 의존하므로 해시 가능 여부는 중요합니다. 대부분의 불변 내장 타입은 해시 가능하지만, 불변 컨테이너 내부의 모든 객체가 자동으로 해시 가능해지는 것은 아닙니다. 예를 들어 리스트를 포함하는 튜플은 해시할 수 없습니다. 면접에서는 이를 설계 관점과 연결하는 것이 좋습니다. 객체가 키로 사용된다면, 삽입 후에 `__eq__` 또는 `__hash__`에 관여하는 필드가 변경되어서는 안 되며, 변경될 경우 해당 객체를 `dict`나 `set`에서 찾을 수 없게 될 수 있습니다.

AI 코치와 함께 이 질문에 답해 보세요

5음수 인덱스와 스텝을 포함하여 슬라이싱(slicing)은 어떻게 동작하나요?

슬라이스 구문 `sequence[start:stop:step]`은 `start`를 포함하고 `stop`을 제외하며, `step`만큼 이동합니다. 생략된 경계값은 `step`의 방향에 따라 유추됩니다. 음수 인덱스는 시퀀스의 끝에서부터 역순으로 계산되므로 `s[-1]`은 마지막 항목을 가리킵니다. 음수 스텝은 오른쪽에서 왼쪽으로 순회하므로, 여러 시퀀스 타입에서 `s[::-1]`을 통해 뒤집힌 시퀀스를 생성할 수 있습니다. 트레이드오프는 슬라이싱이 새로운 객체를 생성하는 경우가 많다는 점입니다. 따라서 표현력은 뛰어나지만 대용량 데이터에서는 비용이 발생할 수 있습니다.

text = "interview"

print(text[1:5])
print(text[::-1])
AI 코치와 함께 이 질문에 답해 보세요

함수와 스코프

6Python은 함수에 인수를 어떻게 전달하나요?

Python은 할당에 의해 객체 참조를 전달하며, 이를 흔히 공유에 의한 호출(call by sharing)이라고 부릅니다. 함수는 호출자가 전달한 동일한 객체에 바인딩된 로컬 매개변수 이름을 받습니다. 객체가 변경 가능한(mutable) 타입이라면 함수 내에서 객체를 수정할 수 있고 호출자 측에서도 그 변경 사항을 확인할 수 있습니다. 반면 함수 내에서 매개변수 이름을 재할당하면 로컬 바인딩만 변경됩니다. 면접에서는 제자리 수정(in-place mutation)과 로컬 재바인딩을 구분하여 설명하는 것이 좋습니다. 예를 들어 items.append(...)나 items.clear()는 호출자의 리스트를 변경하지만, 함수 내부에서 items = []를 실행하면 로컬 이름이 새 리스트를 가리키게 될 뿐입니다.

def add_item(items):
    items.append("new")

def replace_items(items):
    items = []

values = []
add_item(values)
replace_items(values)

print(values)  # ["new"]
AI 코치와 함께 이 질문에 답해 보세요

7변경 가능한(mutable) 기본 인자(default argument)를 사용하는 것이 왜 위험한가요?

기본 인자 값은 함수가 호출될 때마다 평가되는 것이 아니라 함수가 정의될 때 한 번만 평가됩니다. 기본값이 리스트나 딕셔너리 같은 변경 가능한 객체라면, 모든 호출에서 동일한 객체를 공유하게 되어 의도치 않게 상태가 공유되거나 유출될 수 있습니다. 안전한 패턴은 `def f(items=None):` 형태로 작성한 뒤 내부에서 `items is None`일 때 `items = []`를 생성하는 것입니다. 작은 캐시 구현처럼 공유 상태를 의도적으로 활용하는 드문 사례도 있지만, 코드를 읽는 사람에게 혼란을 줄 수 있으므로 반드시 명시적이어야 하고 문서화되어야 합니다.

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag("python"))
print(add_tag("backend"))  # Same list reused

def safe_add_tag(tag, tags=None):
    tags = [] if tags is None else tags
    tags.append(tag)
    return tags
AI 코치와 함께 이 질문에 답해 보세요

8`*args`와 `**kwargs`는 어떤 용도로 사용되나요?

`*args`는 추가적인 위치 인자(positional argument)를 튜플로 모으고, `**kwargs`는 추가적인 키워드 인자(keyword argument)를 딕셔너리로 모읍니다. 이들은 래퍼(wrapper) 함수, 데코레이터, 유연한 API 정의 및 인자 전달(forwarding)에 유용하게 쓰입니다. 호출 시점에서는 `*iterable`을 사용하여 위치 인자 값을 펼치고(언패킹), `**mapping`을 사용하여 키워드 인자 값을 펼칩니다. 다만 명확성 측면에서 트레이드오프가 있습니다. 함수의 공개 규약(public contract)이 안정적인 경우에는 명시적인 매개변수를 정의하는 편이 더 좋고, 인자 전달이나 호환성이 중요한 경계 지점에서는 `*args`와 `**kwargs`를 활용하는 것이 유리합니다.

def call_api(path, *args, **kwargs):
    print(path, args, kwargs)

call_api("/users", 1, 2, timeout=5)

values = [1, 2]
options = {"timeout": 5}
call_api("/users", *values, **options)
AI 코치와 함께 이 질문에 답해 보세요

9Python은 LEGB (Local, Enclosing, Global, Built-in) 규칙으로 변수 이름을 어떻게 찾나요?

Python은 Local, Enclosing, Global, Builtins 스코프 순서로 변수 이름을 찾습니다. 이름을 참조할 때도 이 순서를 따릅니다. 함수 내부에서 이름을 할당하면 기본적으로 로컬 바인딩이 생성되므로, 로컬 할당 전에 이름을 읽으려고 하면 UnboundLocalError가 발생할 수 있습니다. global은 할당 대상을 모듈 스코프로 지정하고, nonlocal은 할당 대상을 둘러싼 외부 함수 스코프로 지정합니다. 상태의 흐름을 파악하기 어렵게 만들기 때문에 견고한 프로덕션 코드에서는 두 키워드 모두 신중하게 사용해야 합니다.

total = 0

def add(value):
    global total
    total += value

def counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment
AI 코치와 함께 이 질문에 답해 보세요

10클로저(closure)란 무엇이며 늦은 바인딩(late binding)이란 무엇인가요?

클로저는 외부 함수가 반환된 후에도 해당 외부 스코프(enclosing scope)의 변수에 대한 접근을 유지하는 함수입니다. 이는 데코레이터, 콜백 팩토리, 설정이 지정된 함수를 만들 때 유용합니다. 늦은 바인딩은 자유 변수(free variable)가 함수 생성 시점이 아니라 내부 함수가 호출되는 시점에 조회되는 것을 의미합니다. 루프 내에서 생성된 람다는 모두 루프의 마지막 값을 참조하게 될 수 있습니다. 기본 인수를 사용하여(예: lambda i=i: i) 현재 값을 캡처하거나, 작은 팩토리 함수를 사용하여 이 문제를 해결할 수 있습니다.

callbacks = []

for i in range(3):
    callbacks.append(lambda: i)

print([callback() for callback in callbacks])  # [2, 2, 2]

callbacks = []
for i in range(3):
    callbacks.append(lambda i=i: i)

print([callback() for callback in callbacks])  # [0, 1, 2]
AI 코치와 함께 이 질문에 답해 보세요

OOP, 이터레이션 및 리소스 관리

11클래스 변수와 인스턴스 변수는 어떻게 다른가요?

클래스 변수는 클래스 객체에 저장되며, 인스턴스가 동일한 이름의 자체 속성으로 가리지(shadowing) 않는 한 모든 인스턴스가 공유합니다. 인스턴스 변수는 특정 객체에 저장되며, 주로 __init__에서 self.name과 같은 형태로 할당됩니다. 불변인 클래스 상수는 일반적으로 문제없지만, 변경 가능한 클래스 속성을 인스턴스별 상태로 잘못 사용하면 흔한 버그의 원인이 됩니다. 면접에서는 속성 탐색 순서를 언급하는 것이 좋습니다. Python은 먼저 인스턴스를 확인한 다음 클래스와 상위 클래스를 확인합니다.

class Cart:
    items = []

first = Cart()
second = Cart()

first.items.append("book")
print(second.items)  # ["book"]

class SafeCart:
    def __init__(self):
        self.items = []
AI 코치와 함께 이 질문에 답해 보세요

12상속, MRO (Method Resolution Order) 및 super()는 어떻게 동작하나요?

Python은 메서드 결정 순서인 MRO (Method Resolution Order)를 통해 속성을 탐색하며, 이는 ClassName.__mro__로 확인할 수 있습니다. 단일 상속에서는 순서가 단순하지만, 다중 상속에서는 일관된 순서를 생성하기 위해 C3 선형화(C3 linearization) 알고리즘을 사용합니다. super()는 단순히 '부모 클래스 호출'을 의미하는 것이 아니라 'MRO 상의 다음 구현체 호출'을 뜻합니다. 이를 통해 메서드가 호환 가능한 인자를 받고 각 클래스가 super()를 올바르게 호출할 때 협력적 다중 상속(cooperative multiple inheritance)이 가능해집니다.

class A:
    def save(self):
        print("A")

class B(A):
    def save(self):
        print("B")
        super().save()

class C(A):
    def save(self):
        print("C")
        super().save()

class D(B, C):
    pass

D().save()  # B, C, A
AI 코치와 함께 이 질문에 답해 보세요

13iterable, iterator, generator의 차이점은 무엇인가요?

iterable은 리스트나 파일 객체처럼 iter()를 통해 이터레이터(iterator)를 반환할 수 있는 모든 객체입니다. 이터레이터는 __next__를 구현하고 모든 값을 소진했을 때 StopIteration 예외를 발생시키는 상태를 가진(stateful) 객체입니다. 제너레이터(generator)는 yield 키워드나 제너레이터 표현식을 사용하여 이터레이터를 간편하게 생성하는 방법입니다. 제너레이터는 값을 지연 생성하므로 메모리 효율적이지만, 대개 일회용이며 리스트처럼 인덱스로 접근할 수 없습니다.

def read_lines(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            yield line.strip()
AI 코치와 함께 이 질문에 답해 보세요

14컨텍스트 관리자(context manager)란 무엇이며, with 문은 왜 유용한가요?

컨텍스트 관리자는 __enter__와 __exit__ 메서드를 통해 진입 및 종료 동작을 정의하거나 contextlib.contextmanager를 사용하여 생성할 수 있습니다. with 문을 사용하면 리소스 관리를 명시적이고 신뢰성 있게 수행할 수 있어, 예외가 발생하더라도 파일이 닫히고, 락이 해제되며, 트랜잭션이 커밋되거나 롤백됩니다. __exit__는 예외 정보를 전달받으며, True를 반환하여 예외를 억제할 수도 있지만 예외를 무시하는 것은 신중하게 사용해야 합니다.

from contextlib import contextmanager

with open("data.txt", encoding="utf-8") as file:
    data = file.read()

@contextmanager
def temporary_setting():
    print("enter")
    try:
        yield
    finally:
        print("exit")
AI 코치와 함께 이 질문에 답해 보세요

15얕은 복사(shallow copy)와 깊은 복사(deep copy)는 어떻게 다른가요?

얕은 복사는 새로운 외부 컨테이너를 생성하지만 중첩된 내부 객체들에 대해서는 동일한 참조를 유지합니다. 깊은 복사는 가능한 경우 중첩된 객체들을 재귀적으로 복사합니다. 단순한 1차원 리스트의 경우 얕은 복사로 충분할 때가 많습니다. 하지만 중첩된 변경 가능 구조에서는 얕은 복사 사용 시 상태가 공유되어 예기치 않은 문제가 생길 수 있습니다. 깊은 복사는 비용이 더 많이 들며, 열려 있는 파일, 소켓, 데이터베이스 세션 또는 객체 식별성에 민감한 동작을 하는 리소스에 대해 `TypeError`나 `PicklingError`를 발생시키며 실패할 수 있습니다.

from copy import copy, deepcopy

original = [[1], [2]]
shallow = copy(original)
deep = deepcopy(original)

original[0].append(99)

print(shallow)  # [[1, 99], [2]]
print(deep)     # [[1], [2]]
AI 코치와 함께 이 질문에 답해 보세요

런타임, 비동기 및 프로덕션 코드

16GIL (Global Interpreter Lock)이란 무엇이며 백엔드 코드에 어떤 영향을 미치나요?

표준 CPython에서 GIL은 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 제한합니다. 따라서 CPU 바운드(CPU-bound) Python 코드에서는 스레드를 사용하더라도 속도가 거의 향상되지 않습니다. 반면 많은 블로킹 작업의 대기 시간 동안에는 GIL이 해제되므로 I/O 바운드 작업에서는 스레드가 여전히 유용합니다. CPU 연산량이 많은 작업에는 multiprocessing, 네이티브 확장 모듈, 벡터화된 네이티브 라이브러리, 혹은 별도의 서비스를 활용해야 합니다. 2026년 기준 인터뷰에서는 Python 3.13 이상의 프리 스레딩(free-threaded) CPython 빌드도 언급하는 것이 좋습니다. 이 빌드는 호환 가능한 코드에 한해 GIL을 선택적으로 비활성화할 수 있지만, 여전히 라이브러리 의존성 및 성능 검증이 필요합니다.

AI 코치와 함께 이 질문에 답해 보세요

17스레드, 프로세스, asyncio 중 어떤 상황에서 무엇을 선택해야 하나요?

블로킹 I/O 작업이 있거나 동기 API만을 제공하는 라이브러리를 사용할 때는 스레드를 사용합니다. CPU 집약적 작업, 작업 격리, 또는 장애 격리가 필요할 때는 직렬화 및 메모리 오버헤드를 감수하고 프로세스를 사용합니다. 동시 I/O 작업이 많고 비동기 데이터베이스 드라이버나 HTTP 클라이언트처럼 비동기를 지원하는 라이브러리가 갖춰져 있다면 asyncio를 사용합니다. 실무에서의 선택은 프레임워크와 의존성 환경에 따라서도 달라지며, 비동기 코드와 블로킹 호출을 혼용하면 성능이 오히려 저하될 수 있습니다.

AI 코치와 함께 이 질문에 답해 보세요

18Python 웹 서비스에서 async/await는 어떤 경우에 유용한가요?

`async`와 `await`는 협력적 동시성(cooperative concurrency)을 가능하게 합니다. 코루틴은 I/O를 기다릴 때 제어권을 이벤트 루프로 반환하여 다른 작업이 실행될 수 있도록 합니다. 이는 데이터베이스, HTTP 호출, 큐 또는 소켓을 기다리는 API에서 매우 유용합니다. 다만 CPU 바운드 코드를 마법처럼 빠르게 만들어 주지는 않으며, 워커, 프로세스 또는 네이티브 코드로 분리하지 않으면 CPU 연산 집약적인 작업이 이벤트 루프를 블로킹합니다. FastAPI의 경우 일반 `def` 엔드포인트는 스레드 풀에서 실행되지만, `async def` 내부의 블로킹 호출은 이벤트 루프를 차단할 수 있습니다. 즉, 전체 호출 경로가 논블로킹 비동기 I/O를 사용할 때만 `async`의 이점을 누릴 수 있습니다.

import asyncio

async def load_user():
    await asyncio.sleep(0.1)
    return {"id": 1}

async def main():
    user, settings = await asyncio.gather(
        load_user(),
        load_user(),
    )
    return user, settings
AI 코치와 함께 이 질문에 답해 보세요

19프로덕션 환경의 Python에서는 예외를 어떻게 처리해야 하나요?

처리 가능한 가장 구체적인 예외를 잡고, 복구하거나 맥락 정보를 추가하거나 도메인 오류로 변환할 수 있는 계층에서 예외를 처리해야 합니다. 예외 종류를 지정하지 않는 bare `except`는 피하고 오류를 조용히 무시하지 마십시오. 데이터 손실이나 운영상의 문제가 은폐될 수 있기 때문입니다. 동일한 예외를 다시 발생시킬 때는 `raise`를 사용하고, 인과 관계의 맥락을 보존하려면 `raise NewError(...) from exc` 형식을 사용합니다. 훌륭한 프로덕션 코드는 시스템 경계에서 로그를 남기고, 적절한 HTTP 오류를 반환하며, 기밀 정보(시크릿)를 유출하지 않으면서도 유용한 예외 메시지를 유지합니다.

try:
    result = parse_payload(payload)
except ValueError as exc:
    raise InvalidRequest("Invalid payload") from exc
AI 코치와 함께 이 질문에 답해 보세요

20프로덕션 환경의 Python 서비스를 어떻게 구성하고 테스트하시겠습니까?

서버나 데이터베이스 없이도 핵심 동작을 테스트할 수 있도록 도메인 로직을 프레임워크 및 인프라 코드와 분리합니다. 환경 변수나 타입이 정의된 설정을 활용하고, `print` 대신 로깅을 사용하며, 시스템 경계에 의존성 주입을 적용하고 네트워크 호출에는 명시적인 타임아웃을 설정합니다. 테스트의 경우 순수 로직을 위한 단위 테스트, 데이터베이스나 외부 시스템 경계를 위한 통합 테스트, 그리고 핵심 흐름을 검증하는 소수의 E2E(End-to-End) 테스트를 조합합니다. 외부 시스템은 명확한 경계에서 모킹하되, 비공개 구현 세부사항보다는 관찰 가능한 동작을 위주로 테스트합니다.

AI 코치와 함께 이 질문에 답해 보세요