접근 방식: traceback과 예외 계약을 분리하기

내부에서는 원본 오류 위치가 필요하지만, 호출자가 HTTP 라이브러리의 모든 예외를 알게 할 필요는 없어.

경계 계층에서 도메인 예외로 변환하고, 원본 예외는 명시적인 원인으로 연결하면 두 요구를 함께 만족할 수 있어.

표현 결과 권장 상황
raise 기존 traceback 보존 같은 예외를 그대로 전달할 때
raise e 재발생 위치가 추가됨 대부분 피하는 편이 좋음
raise X from e 새 예외와 원인을 연결 도메인 예외로 변환할 때

코드 예제: raise, raise e, 예외 체이닝 비교

raise e가 원본 traceback을 완전히 삭제하는 건 아니야. 다만 현재 줄을 새 재발생 지점으로 추가해 로그의 시작점처럼 보이게 해.

def request_api():
    payload = {"user": None}
    return payload["user"]["id"]  # 실제 오류 위치

def with_bare_raise():
    try:
        request_api()
    except TypeError:
        raise

def with_raise_e():
    try:
        request_api()
    except TypeError as e:
        raise e  # 이 줄이 traceback에 추가된다
# raise
Traceback (most recent call last):
  File "example.py", line 3, in request_api
    return payload["user"]["id"]
TypeError: 'NoneType' object is not subscriptable

# raise e
Traceback (most recent call last):
  File "example.py", line 16, in with_raise_e
    raise e
  File "example.py", line 14, in with_raise_e
    request_api()
  File "example.py", line 3, in request_api
    return payload["user"]["id"]
TypeError: 'NoneType' object is not subscriptable

공통 예외가 필요하다면 새 예외만 던지지 말고 from e로 원인을 남겨. 로그에는 도메인 의미와 실제 오류가 함께 표시돼.

class ExternalServiceError(RuntimeError):
    pass

def fetch_user():
    try:
        return request_api()
    except (TimeoutError, TypeError) as e:
        raise ExternalServiceError(
            "사용자 정보를 가져오지 못했어"
        ) from e
TypeError: 'NoneType' object is not subscriptable

The above exception was the direct cause of the following exception:

ExternalServiceError: 사용자 정보를 가져오지 못했어
  1. 외부 API 클라이언트에서 원본 예외가 발생해.
  2. 서비스 경계에서 호출자가 처리할 수 있는 도메인 예외로 바꿔.
  3. from e로 원인을 연결하고 최상위 계층에서 전체 예외를 기록해.

실전 체크리스트: 언제 변환하고 무엇을 남길까

모든 예외를 하나로 감싸면 프로그래밍 오류까지 외부 장애로 오해할 수 있어. 복구 방식이 같은 예외만 선별해서 변환하는 게 좋아.

  • 같은 예외를 다시 전달할 때는 raise를 사용해.
  • 호출자가 하위 라이브러리에 의존하지 않아야 할 때만 도메인 예외로 변환해.
  • 변환한 예외에는 작업 종류, 대상 식별자, 재시도 가능 여부를 담아.
  • 인증 토큰과 응답 원문 같은 민감 정보는 메시지에서 제외해.
  • 원인 분석이 필요하면 raise ... from e로 체인을 보존해.
  • 의도적으로 내부 원인을 숨길 때만 from None을 사용해.
  • 로그에는 문자열만 남기지 말고 traceback 전체를 기록해.

접근 방식: 스레드가 아니라 태스크를 기준으로 보기

asyncio는 한 스레드에서 여러 요청을 번갈아 실행할 수 있어. 그래서 스레드 로컬에 저장한 값은 다른 요청이 덮어쓸 위험이 있어.

ContextVar 값은 현재 실행 컨텍스트에 속해. await로 실행권이 바뀌어도 각 태스크는 자신의 컨텍스트를 다시 사용해.

방식 구분 기준 비동기 요청 격리
함수 인자 호출 경로 명시적으로 보장
threading.local() 스레드 같은 스레드에서는 부족
ContextVar 실행 컨텍스트 태스크별 유지

코드 예제: 미들웨어에서 설정하고 로그에 주입하기

순수 ASGI 미들웨어로 요청 전체를 감싸면 응답 처리와 FastAPI 백그라운드 작업까지 같은 컨텍스트에서 실행돼. 종료할 때는 토큰으로 이전 값을 복원해야 해.

  1. 헤더의 요청 ID를 읽거나 새 UUID를 생성해.
  2. ContextVar.set()이 반환한 토큰을 보관해.
  3. 요청이 끝나면 reset()으로 컨텍스트를 복원해.
import asyncio
import logging
from contextvars import ContextVar
from uuid import uuid4

from fastapi import FastAPI

request_id_var: ContextVar[str] = ContextVar(
    "request_id",
    default="-",
)

class RequestIdMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        headers = dict(scope["headers"])
        request_id = headers.get(b"x-request-id")

        if request_id is None:
            request_id = str(uuid4())
        else:
            request_id = request_id.decode("utf-8")

        token = request_id_var.set(request_id)

        try:
            await self.app(scope, receive, send)
        finally:
            request_id_var.reset(token)

class RequestIdFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        record.request_id = request_id_var.get()
        return True

handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(
    logging.Formatter(
        "%(asctime)s %(levelname)s "
        "[request_id=%(request_id)s] %(message)s"
    )
)

logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

app = FastAPI()
app.add_middleware(RequestIdMiddleware)

@app.get("/orders/{order_id}")
async def read_order(order_id: int):
    logger.info("주문 조회 시작")
    await asyncio.sleep(0.1)
    logger.info("주문 조회 완료")
    return {"order_id": order_id}

asyncio.create_task()는 생성 순간의 컨텍스트를 복사해. 태스크를 만든 뒤 부모의 값을 바꿔도 이미 생성된 태스크에는 반영되지 않아.

async def worker():
    logger.info("백그라운드 실행")

token = request_id_var.set("request-A")
task = asyncio.create_task(worker())

request_id_var.set("request-B")
await task  # worker에는 request-A가 기록돼

request_id_var.reset(token)

실전 체크리스트와 선택 기준

요청 수명 안에서 실행되는 작업은 ContextVar와 잘 맞아. 큐에 넣거나 요청 종료 뒤 독립 실행되는 작업이라면 request_id를 메시지 데이터로 명시하는 편이 안전해.

  • set()의 토큰을 반드시 finally에서 복원해.
  • 태스크 생성 시점에 어떤 컨텍스트가 복사되는지 확인해.
  • 새 스레드나 외부 작업 큐로 값이 자동 전파된다고 가정하지 마.
  • 로그 포맷이 요구하는 필드는 필터에서 항상 기본값까지 채워.
  • 동시 요청 테스트로 서로 다른 ID가 섞이지 않는지 검증해.

함수의 핵심 계약에 필요한 값은 명시적 인자로 전달하는 게 좋아. 로깅·트레이싱처럼 호출 계층 전체에 걸친 부가 정보는 contextvars가 코드 오염을 줄여줘.

[Python] CLI 도구를 click 대신 typer로 만드는 이유

Python으로 CLI 도구를 만들다 보면 언젠가 한 번쯤 마주치는 고민이 있어. argparse는 너무 번거롭고, click은 익숙한데 데코레이터가 계속 쌓이다 보면 코드가 산으로 가는 느낌이랄까. 나도 예전엔 click이 사실상 표준인 줄 알고 별 의심 없이 썼는데, typer를 한 번 써보고 나니 다시 돌아가기가 쉽지 않더라고. 이 글에서는 그냥 신기술 추천하려는 게 아니라, CLI 프레임워크를 고를 때 뭘 기준으로 봐야 하는지, typer가 click보다 어떤 점에서 나은 선택이 될 수 있는지 우리끼리 차분하게 정리해보려 해.

물론 click이 갑자기 구려진 건 절대 아니야. 여전히 생태계도 크고 안정성도 검증됐고, 대규모 프로젝트에서도 충분히 버티고 있지. 하지만 도구 선택은 항상 맥락에 따라 달라지는 법이야. typer는 click 위에서 돌아가면서도 개발자 경험 측면에서 꽤 중요한 차이를 만들어내. 그리고 이 차이가 그냥 편리해서가 아니라, 유지보수나 협업, 장기적인 코드 건강에까지 영향을 준다는 게 핵심이야.


1. 타입 힌트가 곧 CLI 명세가 된다

click을 쓰다 보면 가장 지루하고 반복적인 게 파라미터 타입 일일이 선언하는 거야. Option이나 Argument 데코레이터 안에 type=click.INT 이렇게 계속 적어줘야 하잖아. 프로젝트가 커질수록 이 중복이 엄청 거슬려. 함수 시그니처에서 이미 int라고 해놨는데, CLI 레이어에서 똑같은 말을 또 해야 하는 구조라니까.

typer는 이 부분을 깔끔하게 해결해줘. Python 3.6 이상의 타입 힌트를 CLI 타입 시스템으로 그대로 가져다 쓰는 거지. 함수 파라미터에 annotation만 달아놓으면 typer가 알아서 적절한 변환과 검증을 처리해줘. int는 정수로, str은 문자열로, bool은 플래그로 알아서 해석되고. 개발자 입장에서는 그냥 평소에 함수 짜듯이 자연스럽게 코드 쓰면 되고, CLI용으로 타입을 중복 선언할 필요가 없어.

이건 단순히 코드가 짧아지는 문제가 아니야. 타입 힌트는 요즘 Python 코드베이스의 기본 중 기본이잖아. mypy나 pyright로 정적 검사 돌리고, IDE 자동완성이랑 타입 추론 믿으면서 개발하는 환경에서 typer는 기존 도구들이랑 자연스럽게 연결돼. 반면 click은 CLI 프레임워크만의 별도 타입 체계를 고집하다 보니 정적 분석 도구랑 괴리가 생길 수밖에 없어.

예시를 보면 감이 확 오거든. click에서는 type이랑 default를 데코레이터가 쥐고 있지만, typer에서는 함수 시그니처가 유일한 진실의 출처가 되는 거야. 구조적으로 훨씬 깔끔하고, 테스트 코드 짤 때도 함수 단위로 접근하기가 편하지. 테스트할 때 CLI 진입점이 아니라 순수 함수만 import 해서 검증하면 되니까 말이야.

# click 방식
import click

@click.command()
@click.option('--count', default=1, help='반복 횟수', type=int)
def hello(count):
    for _ in range(count):
        click.echo("Hello")

# typer 방식
import typer

def hello(count: int = 1):
    for _ in range(count):
        typer.echo("Hello")

app = typer.Typer()
app.command()(hello)

위 예시만 봐도 typer 쪽이 훨씬 읽기 쉽고 직관적이지 않아? 데코레이터 양이 줄어들면서 코드의 핵심 로직이 더 잘 보이게 돼. 작은 스크립트에서는 별 차이 없어 보일 수도 있는데, 서브커맨드가 열 개 이상 쌓이는 규모가 되면 이 가독성 차이가 꽤 크게 와닿을 거야.


2. 데코레이터 지옥에서 벗어나기

click으로 복잡한 CLI를 만들다 보면 함수 위에 데코레이터가 수직으로 쌓이는 광경을 자주 보게 돼. @click.command(), @click.option(...)이 다섯 개, @click.argument(...)까지 붙으면 함수 정의보다 데코레이터가 더 길어지는 기가 막힌 상황이 벌어지지. 이건 그냥 보기 싫은 문제를 넘어서 코드의 핵심을 파악하는 데도 방해가 돼.

typer는 이 데코레이터 중첩을 근본적으로 줄여줘. 앞서 말했듯이 타입 힌트와 기본값이 CLI 명세를 대신하니까, 굳이 데코레이터로 모든 걸 선언할 필요가 없는 거야. 옵션이 많아져도 함수 시그니처 안에서 해결되니까 코드가 훨씬 평평해지고 깔끔해져. 눈이 편안하다고 해야 하나, 그냥 숨통이 트이는 느낌이야.

특히 팀 프로젝트에서 이건 꽤 큰 장점이야. 데코레이터가 덜 쌓일수록 코드 리뷰할 때도 부담이 덜 하고, 처음 보는 사람도 함수 하나만 봐도 대충 어떤 CLI인지 파악하기가 쉬워. click에서는 데코레이터를 하나하나 뜯어봐야 파라미터 구조가 보이는데, typer에서는 함수 선언부만 봐도 끝나는 거거든. 협업 관점에서 이 차이는 생각보다 꽤 크지.


3. 예쁘고 친절한 자동 도움말

typer는 내부적으로 rich 라이브러리를 쓰는데, 이 덕분에 --help 출력이 기본적으로 예뻐. click의 기본 help는 기능적으로 문제없지만 솔직히 좀 촌스럽잖아. 반면 typer는 색상도 알아서 들어가고, 테이블도 예쁘게 정렬돼서 출력돼. 별도로 꾸미지 않아도 기본 상태가 이미 보기 좋아.

게다가 typer는 docstring도 알아서 잘 활용해. 함수에 써놓은 docstring이 명령 설명으로 자연스럽게 연결되고, 파라미터 설명도 힌트나 docstring에서 끌어다 쓸 수 있어. 문서화에 들이는 공이 확 줄어들어. click에서도 어느 정도 가능하지만 typer는 이게 더 매끄럽고 직관적이야. 작은 스크립트 하나 만드는데도 도움말 예쁘게 나오면 기분이 좋거든, 인정?


4. 쉘 자동완성은 덤이다

typer를 쓰면 쉘 자동완성 설정이 거의 공짜로 따라와. typer-cli를 설치하면 bash, zsh, fish용 자동완성 스크립트를 손쉽게 생성할 수 있어. click도 click-completion 같은 추가 패키지를 쓰면 되긴 하는데, typer는 이게 기본 생태계에 더 가깝게 녹아있어서 설정이 훨씬 간단하지.

CLI 도구를 팀원들이나 사용자들한테 배포할 때 탭 누르면 알아서 완성되는 경험은 생각보다 큰 만족도를 줘. 특히 옵션이나 서브커맨드가 많을수록 이건 그냥 필수에 가까워. typer는 이런 부분까지 개발자 경험의 연장선상에서 깔끔하게 챙겨주는 느낌이야.


5. 그래도 click이 필요할 때가 있다

여기까지 typer 장점만 말했는데, click을 무시할 순 없어. click은 수년간 쌓인 생태계가 어마어마하고, 이미 click 기반으로 짜놓은 대규모 코드베이스가 있다면 굳이 typer로 갈아탈 이유는 없지. 또 click만의 고급 기능들이나 세밀한 커스터마이징이 필요한 경우도 있고.

typer가 click을 완전히 대체하려는 게 아니라는 점도 중요해. typer는 사실 click 위에서 돌아가는 프레임워크야. 즉 typer가 막힌다 싶으면 언제든 click의 원시 기능을 꺼내 쓸 수 있어. 두 개가 적대 관계가 아니라 typer가 click을 더 현대적이고 파이썬다운 방식으로 포장한 거라고 보면 돼.

그러니까 새 프로젝트를 시작하거나 기존 CLI를 리팩토링할 때는 typer를 먼저 검토해보는 게 좋아. 반면 이미 click으로 잘 굴러가는 시스템이 있다면 "지금 당장" 옮길 필요는 없지. 선택은 항상 현재 맥락과 리소스를 보고 하는 게 맞아. 무조건 typer가 정답이라고 주장하려는 게 아니라, 이제는 typer도 충분히 성숙해서 진지한 후보가 됐다는 거야.


마무리

정리하자면, typer가 click보다 나은 지점은 크게 세 가지로 압축할 수 있어. 첫째, 타입 힌트로 중복 선언을 없앤다. 둘째, 데코레이터 중첩을 줄여서 코드를 깔끔하게 만든다. 셋째, 도움말과 자동완성 같은 주변 경험을 기본적으로 잘 챙긴다. 이 세 가지는 각각 작은 차이처럼 보이지만 모이면 개발자의 일상을 꽤 바꿔놔.

물론 익숙한 게 최고라는 사람도 있을 거고, 이미 click에 파묻혀서 잘 쓰고 있다면 굳이 바꿀 필요는 없어. 하지만 Python으로 CLI를 새로 만든다거나, 기존 코드가 점점 데코레이터에 질식하는 느낌이 든다면 이번에 typer 한 번 손대보는 것도 괜찮을 거야. 나도 그랬고.

오히려 와인 고를 때랑 비슷해. click은 검증된 클래식 와인 같고, typer는 요즘 기분 좋게 마시기 딱 좋은 모던 와인 같은 느낌이지. 둘 다 각자의 자리가 있고, 중요한 건 지금 내 상황에 어떤 게 더 잘 맞느냐는 거야. 오늘 글이 그 판단에 조금이나마 도움이 됐으면 좋겠네.

[Python] SQLite를 async로 쓸 때 aiosqlite가 알아야 할 것들

SQLite 동시성과 WAL 모드

구체적으로는 이런 상황이에요. Python으로 데이터를 다룰 때 자주 쓰는 패턴인데, 매번 문서를 찾아보게 되더라고요. 그래서 오늘은 SQLite를 async로 쓸 때 aiosqlite가 알아야 할 것들에 대해 정리해보려고 합니다.


1. 접근 방식

먼저 전체적인 그림을 보고, 구체적인 코드로 넘어가는 게 좋아요.

  1. 파이썬 내장 함수를 최대한 활용합니다.
  2. 가독성 좋은 한 줄 코드를 목표로 합니다.
  3. 실무에서 바로 쓸 수 있는 예제를 중심으로 합니다.
  4. 이 접근 방식은 sqlite를 async로 쓸 때 aiosqlite가 알아야 할 것들 관련 문제를 해결할 때 항상 기본으로 돌아가는 원칙이에요. 처음부터 복잡한 걸 시도하기보다는 기본 패턴을 확실히 이해하고 적용하는 게 중요합니다.

    사실 많은 개발자가 이 단계를 건너뛰고 바로 코드부터 쓰기 시작하는데, 그러면 나중에 유지보수할 때 고통스러워지는 경우가 많아요. 처음에 5분만 투자해서 구조를 잡아두면, 나중에 5시간을 아낄 수 있어요. 특히 팀 프로젝트에서는 이 접근 방식이 코드 리뷰 시간도 확 줄여줍니다.


    2. 코드 예제

    가장 기본적인 예제부터 시작해볼게요.

    import pandas as pd
    
    # 데이터 정제 예제
    df = pd.read_csv("data.csv")
    df = df.dropna(subset=["important_col"])
    df["new_col"] = df["old_col"].str.strip().str.lower()
    print(f"정제 후: {len(df)} rows")

    결측치를 제거하고 문자열을 정규화하는 기본 패턴입니다.

    위 코드에서 핵심이 되는 부분을 하나씩 뜯어보면 이해가 더 빨라요. 입력 데이터가 어떻게 변환되는지, 각 단계에서 어떤 일이 일어나는지 직접 print()를 찍어가면서 확인해보세요. 이 과정이 실력 향상의 핵심이에요.

    다른 방식으로 하면?

    같은 목적으로 다른 방법을 쓸 수도 있어요. 비교해보면:

    # 리스트 컴프리헨션으로 한 줄에 처리
    import csv
    
    with open("data.csv") as f:
        reader = csv.DictReader(f)
        cleaned = [
            {**row, "name": row["name"].strip().lower()}
            for row in reader
            if row.get("important_col")
        ]
    print(f"정제 후: {len(cleaned)} rows")

    pandas가 무겁게 느껴진다면, csv 모듈 + 리스트 컴프리헨션으로 같은 작업을 할 수 있어요. 작은 데이터셋에서는 오히려 더 빠를 수 있습니다.

    pandas vs csv 모듈 비교

    특성pandascsv 모듈
    대용량 처리chunksize 옵션 활용 가능메모리 주의 필요
    가독성메소드 체이닝으로 직관적딕셔너리 조작 필요
    의존성별도 설치 필요표준 라이브러리
    속도최적화된 C 엔진순수 Python

    3. 실전 팁

    실제 프로젝트에서 적용할 때 알아두면 좋은 것들이에요.

    1. pd.read_csv()에 dtype을 미리 지정하면 메모리를 아낄 수 있습니다.

    2. 체이닝할 때는 각 단계 결과를 확인하세요.

    3. 큰 데이터셋에서는 chunksize 옵션을 활용하세요.

    흔한 실수들

    초보자나 경험자나 자주 하는 실수들이 있어요.

    • dtype 지정 없이 읽으면 메모리 낭비
    • 체이닝 중간에 print() 안 하면 디버깅 어려움
    • 인덱스 리셋을 잊으면 병합 시 문제 발생

    이럴 때는 다른 방법을 쓰세요

    항상 이 방법이 최선은 아니에요.

    • 1MB 미만의 작은 파일은 csv 모듈로 충분
    • 실시간 스트리밍 데이터는 asyncio + aiofiles 고려
    • 복잡한 데이터 변환은 별도 ETL 파이프라인 분리

    다음 단계

    여기까지 SQLite를 async로 쓸 때 aiosqlite가 알아야 할 것들의 기본을 정리했어요. 이제 이걸 기반으로 한 단계 더 나아갈 수 있어요.

    • 실제 프로젝트에 적용해보면서 문제를 발견하고 개선하세요.
    • 테스트 코드를 먼저 작성하면 코드 변경이 두렵지 않아요.
    • 비슷한 문제를 만났을 때 이 패턴이 떠오르는 게 목표예요.

    요약

    SQLite를 async로 쓸 때 aiosqlite가 알아야 할 것들에 대해 정리해봤어요. 핵심은 기본기를 먼저 챙기고, 실제 프로젝트에 적용하면서 다듬어가는 거예요. 처음엔 복잡해 보여도, 한 가지 패턴만 제대로 익혀두면 비슷한 문제에서 재활용할 수 있어요.

    관련 태그: #개발, #테크, #Python, #SQLite, #aiosqlite

[Python] dataclass와 TypedDict를 상황에 맞게 구분하는 기준

구조화 선택 기준

구체적으로는 이런 상황이에요. Python으로 데이터를 다룰 때 자주 쓰는 패턴인데, 매번 문서를 찾아보게 되더라고요. 그래서 오늘은 dataclass와 TypedDict를 상황에 맞게 구분하는 기준에 대해 정리해보려고 합니다.


1. 접근 방식

먼저 전체적인 그림을 보고, 구체적인 코드로 넘어가는 게 좋아요.

  1. 파이썬 내장 함수를 최대한 활용합니다.
  2. 가독성 좋은 한 줄 코드를 목표로 합니다.
  3. 실무에서 바로 쓸 수 있는 예제를 중심으로 합니다.
  4. 이 접근 방식은 dataclass와 typeddict를 상황에 맞게 구분하는 기준 관련 문제를 해결할 때 항상 기본으로 돌아가는 원칙이에요. 처음부터 복잡한 걸 시도하기보다는 기본 패턴을 확실히 이해하고 적용하는 게 중요합니다.

    사실 많은 개발자가 이 단계를 건너뛰고 바로 코드부터 쓰기 시작하는데, 그러면 나중에 유지보수할 때 고통스러워지는 경우가 많아요. 처음에 5분만 투자해서 구조를 잡아두면, 나중에 5시간을 아낄 수 있어요. 특히 팀 프로젝트에서는 이 접근 방식이 코드 리뷰 시간도 확 줄여줍니다.


    2. 코드 예제

    가장 기본적인 예제부터 시작해볼게요.

    import pandas as pd
    
    # 데이터 정제 예제
    df = pd.read_csv("data.csv")
    df = df.dropna(subset=["important_col"])
    df["new_col"] = df["old_col"].str.strip().str.lower()
    print(f"정제 후: {len(df)} rows")

    결측치를 제거하고 문자열을 정규화하는 기본 패턴입니다.

    위 코드에서 핵심이 되는 부분을 하나씩 뜯어보면 이해가 더 빨라요. 입력 데이터가 어떻게 변환되는지, 각 단계에서 어떤 일이 일어나는지 직접 print()를 찍어가면서 확인해보세요. 이 과정이 실력 향상의 핵심이에요.

    다른 방식으로 하면?

    같은 목적으로 다른 방법을 쓸 수도 있어요. 비교해보면:

    # 리스트 컴프리헨션으로 한 줄에 처리
    import csv
    
    with open("data.csv") as f:
        reader = csv.DictReader(f)
        cleaned = [
            {**row, "name": row["name"].strip().lower()}
            for row in reader
            if row.get("important_col")
        ]
    print(f"정제 후: {len(cleaned)} rows")

    pandas가 무겁게 느껴진다면, csv 모듈 + 리스트 컴프리헨션으로 같은 작업을 할 수 있어요. 작은 데이터셋에서는 오히려 더 빠를 수 있습니다.

    pandas vs csv 모듈 비교

    특성pandascsv 모듈
    대용량 처리chunksize 옵션 활용 가능메모리 주의 필요
    가독성메소드 체이닝으로 직관적딕셔너리 조작 필요
    의존성별도 설치 필요표준 라이브러리
    속도최적화된 C 엔진순수 Python

    3. 실전 팁

    실제 프로젝트에서 적용할 때 알아두면 좋은 것들이에요.

    1. pd.read_csv()에 dtype을 미리 지정하면 메모리를 아낄 수 있습니다.

    2. 체이닝할 때는 각 단계 결과를 확인하세요.

    3. 큰 데이터셋에서는 chunksize 옵션을 활용하세요.

    흔한 실수들

    초보자나 경험자나 자주 하는 실수들이 있어요.

    • dtype 지정 없이 읽으면 메모리 낭비
    • 체이닝 중간에 print() 안 하면 디버깅 어려움
    • 인덱스 리셋을 잊으면 병합 시 문제 발생

    이럴 때는 다른 방법을 쓰세요

    항상 이 방법이 최선은 아니에요.

    • 1MB 미만의 작은 파일은 csv 모듈로 충분
    • 실시간 스트리밍 데이터는 asyncio + aiofiles 고려
    • 복잡한 데이터 변환은 별도 ETL 파이프라인 분리

    다음 단계

    여기까지 dataclass와 TypedDict를 상황에 맞게 구분하는 기준의 기본을 정리했어요. 이제 이걸 기반으로 한 단계 더 나아갈 수 있어요.

    • 실제 프로젝트에 적용해보면서 문제를 발견하고 개선하세요.
    • 테스트 코드를 먼저 작성하면 코드 변경이 두렵지 않아요.
    • 비슷한 문제를 만났을 때 이 패턴이 떠오르는 게 목표예요.

    요약

    dataclass와 TypedDict를 상황에 맞게 구분하는 기준에 대해 정리해봤어요. 핵심은 기본기를 먼저 챙기고, 실제 프로젝트에 적용하면서 다듬어가는 거예요. 처음엔 복잡해 보여도, 한 가지 패턴만 제대로 익혀두면 비슷한 문제에서 재활용할 수 있어요.

    관련 태그: #개발, #테크, #Python, #dataclass, #설계

[Python] subprocess.run과 os.system을 왜 구분해서 써야 하는지

프로세스 생성과 보안

구체적으로는 이런 상황이에요. Python으로 데이터를 다룰 때 자주 쓰는 패턴인데, 매번 문서를 찾아보게 되더라고요. 그래서 오늘은 subprocess.run과 os.system을 왜 구분해서 써야 하는지에 대해 정리해보려고 합니다.


1. 접근 방식

먼저 전체적인 그림을 보고, 구체적인 코드로 넘어가는 게 좋아요.

  1. 파이썬 내장 함수를 최대한 활용합니다.
  2. 가독성 좋은 한 줄 코드를 목표로 합니다.
  3. 실무에서 바로 쓸 수 있는 예제를 중심으로 합니다.
  4. 이 접근 방식은 subprocess.run과 os.system을 왜 구분해서 써야 하는지 관련 문제를 해결할 때 항상 기본으로 돌아가는 원칙이에요. 처음부터 복잡한 걸 시도하기보다는 기본 패턴을 확실히 이해하고 적용하는 게 중요합니다.

    사실 많은 개발자가 이 단계를 건너뛰고 바로 코드부터 쓰기 시작하는데, 그러면 나중에 유지보수할 때 고통스러워지는 경우가 많아요. 처음에 5분만 투자해서 구조를 잡아두면, 나중에 5시간을 아낄 수 있어요. 특히 팀 프로젝트에서는 이 접근 방식이 코드 리뷰 시간도 확 줄여줍니다.


    2. 코드 예제

    가장 기본적인 예제부터 시작해볼게요.

    import pandas as pd
    
    # 데이터 정제 예제
    df = pd.read_csv("data.csv")
    df = df.dropna(subset=["important_col"])
    df["new_col"] = df["old_col"].str.strip().str.lower()
    print(f"정제 후: {len(df)} rows")

    결측치를 제거하고 문자열을 정규화하는 기본 패턴입니다.

    위 코드에서 핵심이 되는 부분을 하나씩 뜯어보면 이해가 더 빨라요. 입력 데이터가 어떻게 변환되는지, 각 단계에서 어떤 일이 일어나는지 직접 print()를 찍어가면서 확인해보세요. 이 과정이 실력 향상의 핵심이에요.

    다른 방식으로 하면?

    같은 목적으로 다른 방법을 쓸 수도 있어요. 비교해보면:

    # 리스트 컴프리헨션으로 한 줄에 처리
    import csv
    
    with open("data.csv") as f:
        reader = csv.DictReader(f)
        cleaned = [
            {**row, "name": row["name"].strip().lower()}
            for row in reader
            if row.get("important_col")
        ]
    print(f"정제 후: {len(cleaned)} rows")

    pandas가 무겁게 느껴진다면, csv 모듈 + 리스트 컴프리헨션으로 같은 작업을 할 수 있어요. 작은 데이터셋에서는 오히려 더 빠를 수 있습니다.

    pandas vs csv 모듈 비교

    특성pandascsv 모듈
    대용량 처리chunksize 옵션 활용 가능메모리 주의 필요
    가독성메소드 체이닝으로 직관적딕셔너리 조작 필요
    의존성별도 설치 필요표준 라이브러리
    속도최적화된 C 엔진순수 Python

    3. 실전 팁

    실제 프로젝트에서 적용할 때 알아두면 좋은 것들이에요.

    1. pd.read_csv()에 dtype을 미리 지정하면 메모리를 아낄 수 있습니다.

    2. 체이닝할 때는 각 단계 결과를 확인하세요.

    3. 큰 데이터셋에서는 chunksize 옵션을 활용하세요.

    흔한 실수들

    초보자나 경험자나 자주 하는 실수들이 있어요.

    • dtype 지정 없이 읽으면 메모리 낭비
    • 체이닝 중간에 print() 안 하면 디버깅 어려움
    • 인덱스 리셋을 잊으면 병합 시 문제 발생

    이럴 때는 다른 방법을 쓰세요

    항상 이 방법이 최선은 아니에요.

    • 1MB 미만의 작은 파일은 csv 모듈로 충분
    • 실시간 스트리밍 데이터는 asyncio + aiofiles 고려
    • 복잡한 데이터 변환은 별도 ETL 파이프라인 분리

    다음 단계

    여기까지 subprocess.run과 os.system을 왜 구분해서 써야 하는지의 기본을 정리했어요. 이제 이걸 기반으로 한 단계 더 나아갈 수 있어요.

    • 실제 프로젝트에 적용해보면서 문제를 발견하고 개선하세요.
    • 테스트 코드를 먼저 작성하면 코드 변경이 두렵지 않아요.
    • 비슷한 문제를 만났을 때 이 패턴이 떠오르는 게 목표예요.

    요약

    subprocess.run과 os.system을 왜 구분해서 써야 하는지에 대해 정리해봤어요. 핵심은 기본기를 먼저 챙기고, 실제 프로젝트에 적용하면서 다듬어가는 거예요. 처음엔 복잡해 보여도, 한 가지 패턴만 제대로 익혀두면 비슷한 문제에서 재활용할 수 있어요.

    관련 태그: #개발, #테크, #Python, #subprocess, #보안

FastAPI에서 Depends가 내부적으로 뭘 하는 건지 정확히 이해하기

의존성 주입의 실행 흐름

구체적으로는 이런 상황이에요. Python으로 데이터를 다룰 때 자주 쓰는 패턴인데, 매번 문서를 찾아보게 되더라고요. 그래서 오늘은 FastAPI에서 Depends가 내부적으로 뭘 하는 건지 정확히 이해하기에 대해 정리해보려고 합니다.


1. 접근 방식

먼저 전체적인 그림을 보고, 구체적인 코드로 넘어가는 게 좋아요.

  1. 파이썬 내장 함수를 최대한 활용합니다.
  2. 가독성 좋은 한 줄 코드를 목표로 합니다.
  3. 실무에서 바로 쓸 수 있는 예제를 중심으로 합니다.
  4. 이 접근 방식은 fastapi에서 depends가 내부적으로 뭘 하는 건지 정확히 이해하기 관련 문제를 해결할 때 항상 기본으로 돌아가는 원칙이에요. 처음부터 복잡한 걸 시도하기보다는 기본 패턴을 확실히 이해하고 적용하는 게 중요합니다.

    사실 많은 개발자가 이 단계를 건너뛰고 바로 코드부터 쓰기 시작하는데, 그러면 나중에 유지보수할 때 고통스러워지는 경우가 많아요. 처음에 5분만 투자해서 구조를 잡아두면, 나중에 5시간을 아낄 수 있어요. 특히 팀 프로젝트에서는 이 접근 방식이 코드 리뷰 시간도 확 줄여줍니다.


    2. 코드 예제

    가장 기본적인 예제부터 시작해볼게요.

    import pandas as pd
    
    # 데이터 정제 예제
    df = pd.read_csv("data.csv")
    df = df.dropna(subset=["important_col"])
    df["new_col"] = df["old_col"].str.strip().str.lower()
    print(f"정제 후: {len(df)} rows")

    결측치를 제거하고 문자열을 정규화하는 기본 패턴입니다.

    위 코드에서 핵심이 되는 부분을 하나씩 뜯어보면 이해가 더 빨라요. 입력 데이터가 어떻게 변환되는지, 각 단계에서 어떤 일이 일어나는지 직접 print()를 찍어가면서 확인해보세요. 이 과정이 실력 향상의 핵심이에요.

    다른 방식으로 하면?

    같은 목적으로 다른 방법을 쓸 수도 있어요. 비교해보면:

    # 리스트 컴프리헨션으로 한 줄에 처리
    import csv
    
    with open("data.csv") as f:
        reader = csv.DictReader(f)
        cleaned = [
            {**row, "name": row["name"].strip().lower()}
            for row in reader
            if row.get("important_col")
        ]
    print(f"정제 후: {len(cleaned)} rows")

    pandas가 무겁게 느껴진다면, csv 모듈 + 리스트 컴프리헨션으로 같은 작업을 할 수 있어요. 작은 데이터셋에서는 오히려 더 빠를 수 있습니다.

    pandas vs csv 모듈 비교

    특성pandascsv 모듈
    대용량 처리chunksize 옵션 활용 가능메모리 주의 필요
    가독성메소드 체이닝으로 직관적딕셔너리 조작 필요
    의존성별도 설치 필요표준 라이브러리
    속도최적화된 C 엔진순수 Python

    3. 실전 팁

    실제 프로젝트에서 적용할 때 알아두면 좋은 것들이에요.

    1. pd.read_csv()에 dtype을 미리 지정하면 메모리를 아낄 수 있습니다.

    2. 체이닝할 때는 각 단계 결과를 확인하세요.

    3. 큰 데이터셋에서는 chunksize 옵션을 활용하세요.

    흔한 실수들

    초보자나 경험자나 자주 하는 실수들이 있어요.

    • dtype 지정 없이 읽으면 메모리 낭비
    • 체이닝 중간에 print() 안 하면 디버깅 어려움
    • 인덱스 리셋을 잊으면 병합 시 문제 발생

    이럴 때는 다른 방법을 쓰세요

    항상 이 방법이 최선은 아니에요.

    • 1MB 미만의 작은 파일은 csv 모듈로 충분
    • 실시간 스트리밍 데이터는 asyncio + aiofiles 고려
    • 복잡한 데이터 변환은 별도 ETL 파이프라인 분리

    다음 단계

    여기까지 FastAPI에서 Depends가 내부적으로 뭘 하는 건지 정확히 이해하기의 기본을 정리했어요. 이제 이걸 기반으로 한 단계 더 나아갈 수 있어요.

    • 실제 프로젝트에 적용해보면서 문제를 발견하고 개선하세요.
    • 테스트 코드를 먼저 작성하면 코드 변경이 두렵지 않아요.
    • 비슷한 문제를 만났을 때 이 패턴이 떠오르는 게 목표예요.

    요약

    FastAPI에서 Depends가 내부적으로 뭘 하는 건지 정확히 이해하기에 대해 정리해봤어요. 핵심은 기본기를 먼저 챙기고, 실제 프로젝트에 적용하면서 다듬어가는 거예요. 처음엔 복잡해 보여도, 한 가지 패턴만 제대로 익혀두면 비슷한 문제에서 재활용할 수 있어요.

    관련 태그: #개발, #테크, #Python, #FastAPI, #DI

Python type hint를 실제로 믿을 수 있게 만드는 패턴

mypy strict 모드와 런타임 검증

구체적으로는 이런 상황이에요. Python으로 데이터를 다룰 때 자주 쓰는 패턴인데, 매번 문서를 찾아보게 되더라고요. 그래서 오늘은 Python type hint를 실제로 믿을 수 있게 만드는 패턴에 대해 정리해보려고 합니다.


1. 접근 방식

먼저 전체적인 그림을 보고, 구체적인 코드로 넘어가는 게 좋아요.

  1. 파이썬 내장 함수를 최대한 활용합니다.
  2. 가독성 좋은 한 줄 코드를 목표로 합니다.
  3. 실무에서 바로 쓸 수 있는 예제를 중심으로 합니다.
  4. 이 접근 방식은 python type hint를 실제로 믿을 수 있게 만드는 패턴 관련 문제를 해결할 때 항상 기본으로 돌아가는 원칙이에요. 처음부터 복잡한 걸 시도하기보다는 기본 패턴을 확실히 이해하고 적용하는 게 중요합니다.

    사실 많은 개발자가 이 단계를 건너뛰고 바로 코드부터 쓰기 시작하는데, 그러면 나중에 유지보수할 때 고통스러워지는 경우가 많아요. 처음에 5분만 투자해서 구조를 잡아두면, 나중에 5시간을 아낄 수 있어요. 특히 팀 프로젝트에서는 이 접근 방식이 코드 리뷰 시간도 확 줄여줍니다.


    2. 코드 예제

    가장 기본적인 예제부터 시작해볼게요.

    import pandas as pd
    
    # 데이터 정제 예제
    df = pd.read_csv("data.csv")
    df = df.dropna(subset=["important_col"])
    df["new_col"] = df["old_col"].str.strip().str.lower()
    print(f"정제 후: {len(df)} rows")

    결측치를 제거하고 문자열을 정규화하는 기본 패턴입니다.

    위 코드에서 핵심이 되는 부분을 하나씩 뜯어보면 이해가 더 빨라요. 입력 데이터가 어떻게 변환되는지, 각 단계에서 어떤 일이 일어나는지 직접 print()를 찍어가면서 확인해보세요. 이 과정이 실력 향상의 핵심이에요.

    다른 방식으로 하면?

    같은 목적으로 다른 방법을 쓸 수도 있어요. 비교해보면:

    # 리스트 컴프리헨션으로 한 줄에 처리
    import csv
    
    with open("data.csv") as f:
        reader = csv.DictReader(f)
        cleaned = [
            {**row, "name": row["name"].strip().lower()}
            for row in reader
            if row.get("important_col")
        ]
    print(f"정제 후: {len(cleaned)} rows")

    pandas가 무겁게 느껴진다면, csv 모듈 + 리스트 컴프리헨션으로 같은 작업을 할 수 있어요. 작은 데이터셋에서는 오히려 더 빠를 수 있습니다.

    pandas vs csv 모듈 비교

    특성pandascsv 모듈
    대용량 처리chunksize 옵션 활용 가능메모리 주의 필요
    가독성메소드 체이닝으로 직관적딕셔너리 조작 필요
    의존성별도 설치 필요표준 라이브러리
    속도최적화된 C 엔진순수 Python

    3. 실전 팁

    실제 프로젝트에서 적용할 때 알아두면 좋은 것들이에요.

    1. pd.read_csv()에 dtype을 미리 지정하면 메모리를 아낄 수 있습니다.

    2. 체이닝할 때는 각 단계 결과를 확인하세요.

    3. 큰 데이터셋에서는 chunksize 옵션을 활용하세요.

    흔한 실수들

    초보자나 경험자나 자주 하는 실수들이 있어요.

    • dtype 지정 없이 읽으면 메모리 낭비
    • 체이닝 중간에 print() 안 하면 디버깅 어려움
    • 인덱스 리셋을 잊으면 병합 시 문제 발생

    이럴 때는 다른 방법을 쓰세요

    항상 이 방법이 최선은 아니에요.

    • 1MB 미만의 작은 파일은 csv 모듈로 충분
    • 실시간 스트리밍 데이터는 asyncio + aiofiles 고려
    • 복잡한 데이터 변환은 별도 ETL 파이프라인 분리

    다음 단계

    여기까지 Python type hint를 실제로 믿을 수 있게 만드는 패턴의 기본을 정리했어요. 이제 이걸 기반으로 한 단계 더 나아갈 수 있어요.

    • 실제 프로젝트에 적용해보면서 문제를 발견하고 개선하세요.
    • 테스트 코드를 먼저 작성하면 코드 변경이 두렵지 않아요.
    • 비슷한 문제를 만났을 때 이 패턴이 떠오르는 게 목표예요.

    요약

    Python type hint를 실제로 믿을 수 있게 만드는 패턴에 대해 정리해봤어요. 핵심은 기본기를 먼저 챙기고, 실제 프로젝트에 적용하면서 다듬어가는 거예요. 처음엔 복잡해 보여도, 한 가지 패턴만 제대로 익혀두면 비슷한 문제에서 재활용할 수 있어요.

    관련 태그: #개발, #테크, #Python, #type-hint, #mypy

[Python] 대용량 파일을 메모리 터지지 않고 읽는 패턴

거의 모든 개발자가 한 번쯤 겪는 상황이 있어. 몇 기가짜리 로그 파일을 열었더니 메모리가 터져버리는 거지. open().read() 한 번 쓰면 끝이라고 생각했는데, 실제 운영 환경에서 대용량 파일을 다루다 보면 금세 한계에 부딪혀. 특히 데이터 파이프라인이나 로그 분석 작업을 할 때 이 문제는 피할 수가 없어.

파이썬은 기본적으로 파일 전체를 메모리에 올리는 방식으로 읽는 경우가 많아. 작은 파일이면 상관없지만, 수백 메가를 넘기 시작하면 서버가 버티질 못해. 특히 클라우드 환경에서 메모리 제한이 있는 인스턴스를 쓰고 있다면 더더욱. 이럴 때 필요한 게 바로 청크 스트리밍이야. 파일을 한 번에 다 읽는 게 아니라, 필요한 만큼만 잘라서 처리하는 방식이지.

이 글에서는 제너레이터를 활용해서 대용량 파일을 안전하게 읽는 패턴을 정리해보려고 해. 단순히 코드 몇 줄 보여주는 게 아니라, 왜 이 방식이 메모리를 절약하는지, 그리고 실전에서 어떻게 적용할 수 있는지까지 같이 다룰 거야. 끝까지 읽고 나면 몇 기가짜리 파일도 마음 편하게 처리할 수 있을 거야.


1. 왜 read()는 위험한가

가장 기본적인 파일 읽기 방식부터 살펴보자. 파이썬에서 파일을 읽을 때 가장 흔히 쓰는 방법이 f.read()야. 이 메서드는 파일 전체의 내용을 한 번에 문자열로 반환해. 10KB짜리 설정 파일이면 아무 문제없지. 하지만 2GB짜리 CSV 파일이라면 얘기가 달라져.

운영체제는 파일을 읽을 때 해당 내용을 메모리(RAM)에 올려. read()를 호출하면 파일 크기만큼의 메모리가 즉시 할당되는 거야. 2GB 파일이면 최소 2GB의 RAM이 필요하고, 파이썬 문자열 오버헤드까지 고려하면 실제로는 그보다 더 먹어. 서버에 4GB RAM이 장착되어 있고 다른 서비스도 돌고 있다면, 이거 하나로 전체 시스템이 불안정해질 수 있어.

# 이렇게 쓰면 위험해
with open("huge_log.txt", "r") as f:
    content = f.read()  # 파일 전체를 메모리에 로드
    
for line in content.split("\n"):
    process(line)

또 한 가지 간과하기 쉬운 문제는 가비지 컬렉션 타이밍이야. content 변수가 스코프를 벗어나기 전까지 메모리가 해제되지 않아. 함수 안에서 읽었는데 리턴값으로 content를 넘기면, 참조가 사라질 때까지 메모리가 계속 잡혀있어. 대용량 파일 처리에서 이건 치명적이야.

결론은 명확해. 대용량 파일을 다룰 때는 read() 전체 호출을 피해야 해. 대신 "필요한 만큼만 읽기" 원칙을 적용해야지. 이게 바로 스트리밍의 핵심이고, 파이썬에서는 제너레이터가 이 역할을 완벽하게 수행해줘.


2. 제너레이터로 한 줄씩 읽기

파이썬의 파일 객체 자체가 이미 이터레이터야. 그래서 for line in f: 구문으로 파일을 한 줄씩 순회할 수 있는 거지. 이 방식은 내부적으로 버퍼를 사용해서 한 번에 적절한 크기의 데이터만 메모리에 올려. 즉, 파일 크기에 관계없이 거의 일정한 메모리만 사용해.

하지만 단순히 for 루프로 순회하는 것만으로는 부족할 때가 많아. 재사용 가능한 구조가 필요하고, 파이프라인 형태로 다른 처리 단계와 연결하고 싶을 때가 있지. 이럴 때 제너레이터 함수를 만들면 깔끔한 추상화가 가능해.

def read_lines(file_path):
    """파일을 한 줄씩 읽는 제너레이터"""
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

# 사용법
for line in read_lines("huge_log.txt"):
    if "ERROR" in line:
        print(line)

이 패턴의 핵심은 yield 키워드야. 일반 함수는 return으로 값을 한 번에 반환하고 끝나지만, 제너레이터는 호출될 때마다 하나의 값만 내놓고 상태를 유지해. 다음 요청이 올 때까지 대기하는 거지. 덕분에 10GB 파일이어도 메모리에는 항상 한 줄분의 데이터만 올라가 있어.

여기에 몇 가지 개선점을 추가할 수 있어. 예를 들어 빈 줄을 건너뛰거나, 인코딩 에러를 처리하거나, 진행 상황을 로깅하는 거지. 제너레이터 안에서 이 처리를 해두면 호출 쪽은 깔끔하게 유지할 수 있어.

def read_lines_safe(file_path, skip_empty=True):
    """에러 처리가 포함된 안전한 라인 리더"""
    with open(file_path, "r", encoding="utf-8") as f:
        for line_num, line in enumerate(f, 1):
            stripped = line.rstrip("\n")
            if skip_empty and not stripped:
                continue
            yield line_num, stripped

# 사용법: 줄 번호와 함께 반환
for line_num, content in read_lines_safe("server.log"):
    if "CRITICAL" in content:
        print(f"[{line_num}] {content}")

이런 식으로 제너레이터를 감싸면 재사용성도 높아지고, 호출하는 코드도 훨씬 읽기 쉬워져. 특히 로그 분석이나 데이터 전처리 파이프라인에서 이 패턴을 자주 만나게 될 거야. 한 줄씩 읽는 것만으로도 대부분의 대용량 텍스트 파일 문제는 해결 가능해.


3. 청크 단위로 읽기: 바이너리와 큰 블록 처리

텍스트 파일은 한 줄 단위로 읽으면 되지만, 바이너리 파일이나 고정 크기 블록으로 나눠야 할 때는 다른 접근이 필요해. 예를 들어 이미지 파일을 분석하거나, 대용량 바이너리 데이터를 파싱하거나, 네트워크로 스트리밍할 때는 청크(chunk) 단위로 읽어야 해.

파이썬의 f.read(size) 메서드에 크기를 지정하면 해당 바이트만큼만 읽어와. 이걸 반복 호출하면 원하는 크기의 청크로 파일을 나눠서 처리할 수 있지. 이것도 제너레이터로 감싸면 훨씬 깔끔해져.

def read_in_chunks(file_path, chunk_size=8192):
    """지정된 크기만큼 청크로 읽는 제너레이터"""
    with open(file_path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

# 사용법: 8KB씩 읽어서 처리
total_bytes = 0
for chunk in read_in_chunks("large_data.bin"):
    total_bytes += len(chunk)
    # 청크 단위로 처리 (예: 해시 계산, 압축 등)
    
print(f"총 {total_bytes:,} 바이트 처리 완료")

chunk_size는 상황에 따라 조절하면 돼. 너무 작으면 I/O 호출 횟수가 늘어나서 느려지고, 너무 크면 메모리 사용량이 올라가. 보통 4KB~64KB 사이에서 시작하는 게 적절해. 운영체제의 페이지 크기(보통 4KB)에 맞추면 I/O 효율이 좋아진다는 점도 알아두면 좋을 거야.

실전에서 자주 쓰이는 패턴 중 하나가 파일 해시 계산이야. 대용량 파일의 무결성을 확인할 때 전체를 메모리에 올리지 않고 청크 단위로 해시를 계산해야 해.

import hashlib

def compute_file_hash(file_path, algorithm="sha256", chunk_size=65536):
    """청크 단위로 파일 해시를 계산"""
    h = hashlib.new(algorithm)
    with open(file_path, "rb") as f:
        while chunk := f.read(chunk_size):
            h.update(chunk)
    return h.hexdigest()

# 10GB 파일도 메모리 걱정 없이 해시 계산 가능
file_hash = compute_file_hash("huge_database_dump.sql")
print(f"SHA-256: {file_hash}")

이 방식의 장점은 파일 크기와 무관하게 항상 chunk_size만큼의 메모리만 사용한다는 거야. 10GB 파일이든 100GB 파일이든 동일한 메모리 패턴을 보여. 서버 리소스를 예측 가능하게 관리할 수 있어서 운영 환경에서 특히 유용해. walrus 연산자(:=)를 쓰면 코드도 훨씬 간결해진다는 것도 참고해.


4. 제너레이터 체이닝: 파이프라인 구성하기

제너레이터의 진짜 강점은 체이닝(chain)이야. 여러 제너레이터를 연결해서 데이터 파이프라인을 구성할 수 있어. 각 단계가 자신의 역할만 수행하고, 데이터가 흐르는 대로 처리되는 구조지. Unix의 파이프(|) 개념과 비슷해.

예를 들어 로그 파일에서 특정 패턴을 가진 줄만 추출하고, 파싱하고, 집계하는 과정을 제너레이터 체인으로 만들 수 있어. 각 단계가 독립적이어서 테스트하기도 쉽고, 재조합하기도 편해.

def read_lines(file_path):
    """1단계: 파일 읽기"""
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

def filter_errors(lines):
    """2단계: 에러 로그만 필터링"""
    for line in lines:
        if "ERROR" in line or "CRITICAL" in line:
            yield line

def parse_timestamp(lines):
    """3단계: 타임스탬프 추출"""
    import re
    pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"
    for line in lines:
        match = re.search(pattern, line)
        if match:
            yield {"timestamp": match.group(), "message": line}

# 파이프라인 구성
lines = read_lines("app.log")
errors = filter_errors(lines)
parsed = parse_timestamp(errors)

for entry in parsed:
    print(f"[{entry['timestamp']}] {entry['message']}")

이 구조의 핵심은 지연 평가(lazy evaluation)야. 체인의 끝에서 값을 요청할 때만 각 단계가 실제로 실행돼. read_lines가 전체 파일을 한꺼번에 메모리에 올리는 게 아니라, parse_timestamp가 한 개씩 요청할 때마다 한 줄씩 읽고 필터링하고 파싱하는 거지. 메모리 사용량은 항상 최소 수준으로 유지돼.

실전에서는 itertools 모듈을 함께 활용하면 더 강력해져. itertools.islice로 처음 N개만 가져오거나, itertools.chain으로 여러 파일을 이어서 처리하거나, itertools.groupby로 그룹화할 수 있지. 특히 여러 로그 파일을 날짜별로 순회하면서 한 번에 분석해야 할 때 chain.from_iterable이 아주 유용해.

from itertools import chain, islice

def read_multiple_files(file_paths):
    """여러 파일을 하나의 스트림으로 연결"""
    for path in file_paths:
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                yield line.strip()

# 여러 날짜의 로그를 이어서 읽고, 처음 100개 에러만 추출
log_files = ["app_2024-01-01.log", "app_2024-01-02.log", "app_2024-01-03.log"]
all_lines = read_multiple_files(log_files)
errors = filter_errors(all_lines)
first_100_errors = islice(errors, 100)

for err in first_100_errors:
    print(err)

이런 패턴으로 구성하면 수백 개의 로그 파일도 메모리 걱정 없이 한 번에 처리할 수 있어. 각 제너레이터는 독립적인 단위이기 때문에 디버깅도 쉽고, 중간 단계를 추가하거나 빼기도 편해. 일단 이 구조에 익숙해지면 대용량 데이터 처리의 거의 모든 문제를 이 패턴으로 풀 수 있을 거야.


마무리

대용량 파일 처리에서 메모리 문제는 결국 "얼마나 데이터를 한꺼번에 들고 있느냐"의 싸움이야. read() 전체 호출은 파일 크기만큼의 메모리를 확보하지만, 제너레이터와 청크 스트리밍은 거의 일정한 메모리만으로 어떤 크기의 파일이든 처리할 수 있어.

핵심 원칙 세 가지만 기억해. 첫째, 파일을 한 번에 읽지 말고 필요한 만큼만 읽어. 둘째, 제너레이터로 지연 평가 구조를 만들어. 셋째, 여러 제너레이터를 체이닝해서 파이프라인을 구성해. 이 세 가지면 대부분의 대용량 파일 처리 시나리오를 커버할 수 있어.

특히 데이터 엔지니어링이나 서버 로그 분석 작업을 하고 있다면, 이 패턴은 거의 매일 쓰게 될 거야. 처음엔 익숙하지 않을 수 있지만, 한 번 제대로 이해하고 나면 자연스럽게 손에 익을 거야. 다음번에 대용량 파일을 만나면 주저하지 말고 제너레이터부터 꺼내 쓰자.

긴 글 읽어줘서 고마워. 궽한 점 있으면 댓글 남겨줘!


Python 문자열 메소드로 데이터 클리닝 마스터하기: 간결한 코드로 복잡한 문제 해결

데이터 클리닝은 데이터 분석, 머신러닝, 딥러닝 작업에서 매우 중요한 단계입니다. 특히 텍스트 데이터는 정리가 안 되어 있는 경우가 많아 클리닝 작업이 필수적입니다. Python의 문자열 메소드는 이런 텍스트 데이터를 효율적이고 직관적으로 전처리할 수 있게 도와줍니다.

이 글에서는 Python 문자열 메소드를 활용해 데이터 클리닝 작업을 어떻게 간단하게 할 수 있는지, 그리고 딥러닝 모델 준비 단계에서 실질적으로 활용할 수 있는 코드를 중심으로 설명합니다.


1. 문자열 메소드의 장점: 왜 사용해야 할까?

Python의 문자열 메소드는 다음과 같은 장점이 있어 데이터 클리닝 작업에 적합합니다:

  1. 가독성: 간결한 코드로 복잡한 작업을 수행할 수 있습니다.
  2. 효율성: 내장 메소드로 최적화되어 있어 빠르게 처리할 수 있습니다.
  3. 다양한 기능: 공백 제거, 대소문자 변환, 특정 패턴 교체 등 다양한 작업을 지원합니다.

2. 기본 데이터 클리닝: 공백 제거와 대소문자 통일

예제: 이메일 데이터를 정리하기

emails = [
    " Alice@example.com ",
    "BOB@EXAMPLE.COM ",
    "  carol@example.com"
]

# 데이터 클리닝 함수
def clean_emails(email_list):
    return [email.strip().lower() for email in email_list]

cleaned_emails = clean_emails(emails)
print(cleaned_emails)

결과

['alice@example.com', 'bob@example.com', 'carol@example.com']

설명

  • strip(): 문자열 양쪽의 공백을 제거합니다.
  • lower(): 모든 문자를 소문자로 변환합니다.
  • 이메일 주소처럼 대소문자와 공백이 혼재된 데이터를 클리닝할 때 간단한 코드로 처리할 수 있습니다.

3. 정규 표현식 없이 문자열 메소드로 텍스트 필터링

예제: 텍스트 데이터에서 숫자 제거

딥러닝을 위해 텍스트 데이터를 전처리할 때 숫자나 불필요한 문자를 제거해야 하는 경우가 많습니다.

texts = [
    "Product ID: 12345, Name: Chair",
    "Code: 56789, Name: Table",
    "Order: 98765, Name: Lamp"
]

# 숫자와 콜론 제거
def clean_texts(text_list):
    return [text.replace("12345", "").replace("56789", "").replace("98765", "").replace(":", "") for text in text_list]

cleaned_texts = clean_texts(texts)
print(cleaned_texts)

결과

['Product ID , Name Chair', 'Code , Name Table', 'Order , Name Lamp']

설명

  • replace(old, new): 특정 문자를 다른 문자로 교체합니다. 여기선 숫자와 콜론을 제거했습니다.
  • 텍스트에서 특정 패턴을 제거하거나 변환할 때 유용합니다.

4. 딥러닝 모델 준비: 특수 문자와 공백 처리

딥러닝에서 텍스트 데이터를 정리할 때 특수 문자와 중복 공백을 제거하는 작업이 필요합니다. 아래는 텍스트 데이터를 간소화하는 예제입니다.

texts = [
    "Hello,    World!!!",
    "Python  is   amazing??",
    "Clean    this     text..."
]

# 특수 문자 제거 및 공백 정리
def preprocess_text(text_list):
    return [" ".join(text.replace("!", "").replace("?", "").replace(".", "").split()) for text in text_list]

preprocessed_texts = preprocess_text(texts)
print(preprocessed_texts)

결과

['Hello World', 'Python is amazing', 'Clean this text']

설명

  • replace(): 특수 문자를 제거합니다.
  • split() + join(): 중복된 공백을 하나로 줄입니다.
  • 텍스트 데이터를 클리닝하여 딥러닝 모델에 적합한 형태로 변환합니다.

5. 텍스트 라벨 정리: 대소문자 통일과 불필요한 단어 제거

머신러닝 모델에서 라벨 데이터를 사용할 때, 대소문자를 통일하고 특정 단어를 제거해야 할 때가 있습니다.

예제: 상품 라벨 정리

labels = [
    "  HIGH-QUALITY Chair  ",
    "Premium TABLE",
    "cheap  Lamp"
]

# 라벨 정리 함수
def clean_labels(label_list):
    return [label.strip().lower().replace("high-quality", "").replace("premium", "").replace("cheap", "").strip() for label in label_list]

cleaned_labels = clean_labels(labels)
print(cleaned_labels)

결과

['chair', 'table', 'lamp']

설명

  • strip(): 공백 제거.
  • lower(): 소문자로 변환.
  • replace(): 불필요한 단어 제거.
  • 라벨 데이터를 정리하여 일관성을 확보하고 모델의 정확도를 높입니다.

6. 대규모 텍스트 데이터에서 문자열 메소드 활용

딥러닝에서 큰 데이터셋을 다룰 때도 문자열 메소드는 효율적으로 사용할 수 있습니다. 아래는 JSON 데이터에서 텍스트 필드를 정리하는 예제입니다.

예제: JSON 데이터 클리닝

import json

# 예제 데이터
data = """
[
    {"id": 1, "review": "Great product! Highly recommend."},
    {"id": 2, "review": " Terrible experience. Never again. "},
    {"id": 3, "review": "  Good, but could be better. "}
]
"""

# JSON 파싱
reviews = json.loads(data)

# 데이터 클리닝 함수
def clean_reviews(review_list):
    for review in review_list:
        review["review"] = " ".join(review["review"].strip().lower().replace(".", "").replace("!", "").replace(",", "").split())
    return review_list

cleaned_reviews = clean_reviews(reviews)
print(cleaned_reviews)

결과

[
    {'id': 1, 'review': 'great product highly recommend'},
    {'id': 2, 'review': 'terrible experience never again'},
    {'id': 3, 'review': 'good but could be better'}
]

설명

  • json.loads(): JSON 데이터를 파이썬 객체로 변환합니다.
  • 문자열 메소드 조합:
    • strip(): 앞뒤 공백 제거.
    • lower(): 소문자 변환.
    • replace(): 특수 문자 제거.
    • split() + join(): 중복 공백 제거.
  • 대규모 JSON 데이터의 텍스트 필드를 클리닝하여, 머신러닝 모델에 적합한 데이터 형태로 만듭니다.

요약: 문자열 메소드를 활용한 데이터 클리닝의 핵심

Python의 문자열 메소드는 데이터 클리닝 작업에서 강력한 도구로, 복잡한 작업을 간단하고 효율적으로 처리할 수 있습니다. 특히 딥러닝이나 머신러닝 모델을 준비할 때, 문자열 메소드를 활용하면 다음과 같은 장점이 있습니다:

  1. 공백 제거와 통일된 포맷: strip(), lower()
  2. 불필요한 문자 제거: replace()
  3. 데이터 가독성 향상: split() + join()을 통한 중복 공백 제거
  4. 특수 문자 처리: replace()로 간단히 처리 가능
  5. 대규모 데이터 관리: JSON 데이터 등의 텍스트 필드를 효율적으로 처리 가능

+ Recent posts