← 학습 카테고리

Learn

Python

5개 모듈 · 현재 1번째

Python 모듈 1/5 python-learn-01

핵심 파이썬과 자료구조 — 데이터 엔지니어링 관점

50 Python Interview Questions for Data Engineers (2026) — DataCamp (blog) Section 1: Core Python & Data Structures (pp.2-4)

이 모듈을 다 읽으면

  • 리스트와 튜플의 가변성 차이가 레코드 표현·딕셔너리 키 설계에 주는 영향을 설명할 수 있다
  • CPython의 참조 카운팅과 순환 가비지 컬렉터가 어떻게 함께 동작하는지, 이것이 CPython 고유의 구현 세부사항임을 설명할 수 있다
  • 얕은 복사와 깊은 복사의 차이를 파이프라인 설정 객체 공유 버그 사례로 판단할 수 있다
  • 데코레이터와 *args/**kwargs를 파이프라인 컴포넌트 설계(로깅, 재시도, 인증)에 활용하는 방법을 설명할 수 있다

리스트/튜플의 가변성, CPython 메모리 관리(참조 카운팅 + 순환 GC), 얕은/깊은 복사, 데코레이터와 가변 인자 함수 설계를 데이터 엔지니어링 실무 관점에서 다룬다. 원문이 뭉뚱그린 'Python은 참조 카운팅을 쓴다'는 서술은 CPython 구현체에 한정된 사실임을 정정하고, 대용량 객체 사이의 순환 참조가 메모리 해제 시점에 미치는 실무적 영향을 보충한다.

리스트 vs 튜플 — 왜 불변성이 레코드 설계에 중요한가

리스트는 가변(mutable) 객체로, 생성 후에도 원소를 추가·삭제·변경할 수 있어 데이터를 동적으로 모으는 용도(배치 버퍼, 누적 결과 리스트)에 적합하다. 튜플은 불변(immutable) 객체로 한 번 생성되면 내용을 바꿀 수 없다.

데이터 엔지니어링에서 튜플이 선호되는 이유는 두 가지다. 첫째, 고정된 스키마를 가진 레코드(예: (user_id, event_ts, event_type))를 표현할 때 '내용이 바뀌지 않는다'는 보장 자체가 데이터 무결성 신호가 된다. 둘째, 튜플은 해시 가능(hashable)하므로 딕셔너리 키나 집합(set)의 원소로 쓸 수 있다 — 단, 이 성질은 튜플 내부의 모든 원소가 해시 가능할 때만 성립한다. 튜플 안에 리스트처럼 가변 원소가 들어 있으면 그 튜플 자체도 해시 불가능해져 딕셔너리 키로 쓸 수 없다는 점은 원문에는 없지만 실무에서 자주 걸리는 함정이다.

핵심 포인트

  • 리스트는 가변, 튜플은 불변 — 데이터가 계속 누적되는 버퍼는 리스트, 스키마가 고정된 레코드는 튜플이 자연스럽다.
  • 튜플은 해시 가능해야 딕셔너리 키/집합 원소로 쓸 수 있으며, 내부에 리스트 같은 가변 원소가 있으면 해시 불가능해진다.
  • 튜플은 메모리 오버헤드가 리스트보다 작아 수백만 개의 고정 레코드를 다룰 때 메모리 절감 효과가 있다.

CPython의 메모리 관리 — 참조 카운팅과 순환 가비지 컬렉터

CPython은 모든 객체와 자료구조를 private heap에 두고 Python Memory Manager가 이 heap의 할당을 담당한다. 객체 해제는 기본적으로 참조 카운팅으로 이뤄진다 — 어떤 객체를 가리키는 참조가 0이 되는 즉시 그 자리에서 메모리가 회수된다. 다만 참조 카운팅만으로는 서로를 가리키는 순환 참조(circular reference)를 해제할 수 없으므로, CPython은 주기적으로 도달 불가능한 객체 그룹을 찾아 정리하는 세대별(generational) 순환 가비지 컬렉터를 별도로 둔다.

— 정정: 원문은 이를 'Python이 참조 카운팅을 사용한다'고 일반화했지만, 이는 Python 언어 명세가 아니라 CPython이라는 특정 구현체의 특징이다. 예를 들어 PyPy는 참조 카운팅 없이 세대별 추적 GC만 사용하며, 객체 해제 시점이 CPython과 다르게 동작한다(파일 핸들이 참조 카운트 0 시점에 즉시 닫히는 CPython의 관례가 PyPy에서는 보장되지 않아, PyPy에서는 파일을 다룰 때 with문 사용이 CPython보다 훨씬 더 중요하다).

데이터 엔지니어링 실무 함의: pandas DataFrame이나 pyarrow Table을 감싸는 커스텀 캐시 객체들 사이에 부모↔자식 형태의 순환 참조가 생기면, 그 객체들의 참조 카운트가 0으로 떨어지지 않아 순환 GC가 개입할 때까지 대용량 메모리가 즉시 해제되지 않는다. 장시간 실행되는 ETL 워커나 스트리밍 컨슈머에서 이런 패턴이 누적되면 메모리 사용량이 계단식으로 증가하는 것처럼 보일 수 있으므로, 순환 참조가 필요한 캐시 구조에는 weakref 모듈을 사용하거나 명시적으로 gc.collect()를 호출하는 것을 고려해야 한다.

핵심 포인트

  • CPython은 참조 카운트가 0이 되는 즉시 객체를 해제하고, 순환 참조는 별도의 세대별 순환 GC가 주기적으로 정리한다.
  • 참조 카운팅은 CPython 구현의 특징이며, 파이썬 언어 자체의 요구사항은 아니다(PyPy는 참조 카운팅을 쓰지 않는다) — 정정.
  • 대용량 객체 간 순환 참조는 즉시 해제되지 않으므로, 장시간 실행 워커에서는 weakref나 gc.collect()로 관리할 필요가 있다.

얕은 복사 vs 깊은 복사 — 설정 객체 공유 버그

얕은 복사(shallow copy, copy.copy() 또는 list.copy())는 새로운 컨테이너를 만들지만 그 안의 중첩 객체에 대한 참조는 원본과 공유한다. 따라서 얕은 복사본에서 중첩된 리스트를 수정하면 원본도 함께 바뀐다. 깊은 복사(deep copy, copy.deepcopy())는 원본 안에서 발견되는 모든 객체를 재귀적으로 새로 복제하므로, 복사본이 원본과 완전히 독립적이다.

실무 예시: 여러 ETL 잡에 공통 설정을 재사용하기 위해 base_config = {"retries": 3, "sources": ["s3", "gcs"]} 를 얕은 복사로 여러 잡에 나눠주면, 한 잡이 sources 리스트에 append를 하는 순간 다른 모든 잡의 설정에도 그 변경이 반영되는 은근한 버그가 발생한다. 중첩된 딕셔너리/리스트를 포함한 설정 객체를 잡마다 독립적으로 다뤄야 한다면 반드시 copy.deepcopy()를 사용해야 한다.

핵심 포인트

  • 얕은 복사는 최상위 컨테이너만 새로 만들고 중첩 객체는 원본과 공유 — 중첩 데이터를 수정하면 원본도 바뀐다.
  • 깊은 복사(copy.deepcopy)는 모든 하위 객체까지 재귀적으로 복제해 완전히 독립적인 사본을 만든다.
  • 여러 잡이 공유하는 중첩 설정 딕셔너리는 얕은 복사로 나눠주면 상호 오염 버그가 생기므로 deepcopy가 안전하다.

함수 설계 이디엄 — 데코레이터, *args/**kwargs, 람다

데코레이터는 함수의 소스 코드를 영구히 바꾸지 않으면서 다른 함수를 감싸 동작을 확장하는 패턴이다. 데이터 엔지니어링에서는 ETL 태스크 시작/종료 로깅, 실행 시간 측정, 불안정한 API 연결에 대한 재시도 로직, 쓰기 전 인증 검사 같은 횡단 관심사(cross-cutting concern)에 자주 쓰인다.

*args는 임의 개수의 위치 인자를 튜플로, **kwargs는 임의 개수의 키워드 인자를 딕셔너리로 받게 해준다. 소스 시스템이나 설정에 따라 전달할 파라미터 개수·종류가 달라지는 유연한 래퍼 함수나 모듈형 파이프라인 컴포넌트를 만들 때 필수적이다.

람다는 이름 없이 한 줄로 정의하는 짧은 함수로, map(), filter(), pandas의 .apply() 같은 고차 함수에 짧게 인라인으로 넘기는 용도에 적합하다. 다만 로직이 복잡해지면 가독성과 디버깅 편의성을 위해 이름 있는 함수로 분리하는 것이 좋다.

핵심 포인트

  • 데코레이터는 원본 함수를 바꾸지 않고 로깅·타이밍·재시도·인증 같은 횡단 관심사를 추가하는 데 쓰인다.
  • *args/**kwargs는 소스 시스템별로 파라미터가 달라지는 유연한 파이프라인 컴포넌트 설계에 필요하다.
  • 람다는 map/filter/apply에 넘기는 짧은 로직에 적합하며, 복잡한 로직은 이름 있는 함수로 분리해야 한다.