외부 API 응답을 곧이곧대로 믿으면 안 되는 이유

새벽 배포 직후 알림이 울리고, 로그를 열어보니 결제 API가 갑자기 amount 필드를 문자열 “1,000”으로 보내서 파서가 통째로 죽어 있는 상황, 한 번쯤 겪어보셨을 거예요. 외부 API 응답을 그대로 믿으면 안 되는 이유는 간단합니다. 상대 서버가 문서와 다르게 필드를 바꾸거나, 일부 데이터만 누락된 채 200 OK를 돌려주는 순간 내 서비스가 함께 무너지기 때문이에요. 이 글에서는 스키마 … Read more

크롤링이 403으로 막힐 때 확인할 순서: UA·robots·속도

크롤링이 403으로 막히면 다들 제일 먼저 프록시부터 바꾸는데, 사실 원인의 상당수는 IP가 아니라 요청 헤더와 속도에 있어요. User-Agent 하나만 제대로 채워도 뚫리는 경우가 많고, robots.txt를 무시한 경로만 골라 요청했다가 걸리는 경우도 흔합니다. 이 글에서는 403의 흔한 원인 네 가지를 실제 점검 순서대로 정리하고, 어디까지가 “고치면 되는 문제”이고 어디부터는 우회가 아니라 포기해야 하는 영역인지 구분해 드릴게요. … Read more

Windows에서 파이썬 cp949 오류 없애기: 로그·파일·서브프로세스 3곳 총정리

Windows에서 파이썬 스크립트를 돌리다가 UnicodeDecodeError나 UnicodeEncodeError 메시지에 ‘cp949’라는 단어가 찍혀 있어서 검색하셨다면, 답은 명확합니다. 표준 출력(로그·print)은 스트림 인코딩을 UTF-8로 재설정하고, 파일은 open()에 encoding=’utf-8’을 항상 명시하며, 서브프로세스는 자식 프로세스가 실제로 쓰는 인코딩에 맞춰 encoding 인자를 지정해야 세 곳 모두에서 오류가 사라집니다. 원인은 하나입니다. 한국어 Windows의 로컬 코드페이지가 cp949로 잡혀 있고, 파이썬이 인코딩을 따로 지정하지 않으면 이 … Read more

크론 대신 앱 안에서 스케줄을 돌릴 때, 재시작 중복 실행 막는 법

크론 대신 앱 안에서 스케줄을 돌릴 때 재시작 중복 실행을 막으려면, 스케줄러 라이브러리가 아니라 디스크나 데이터베이스처럼 프로세스 바깥에 남는 곳에 “이미 실행했다”는 흔적을 남겨야 합니다. node-cron 같은 라이브러리는 프로세스 메모리 안에서 타이머를 돌릴 뿐이라, 앱이 재시작되는 순간 그 상태가 통째로 사라지기 때문입니다. 특히 저장소를 1분마다 pull 받아 자동 배포하면서 프로세스를 재시작하는 서버라면, 배포 타이밍과 스케줄 … Read more

무중단 배포인데 진행 중이던 작업이 끊기는 이유, drain 시간과 긴 작업 분리로 해결하기

무중단 배포인데 진행 중이던 작업이 갑자기 끊기는 이유는 뭘까요? 배포 도구가 새 요청은 막아주지만, 이미 실행 중인 요청이나 잡(job)을 끝까지 기다려주는 시간(drain 타임아웃)이 짧거나 아예 설정돼 있지 않기 때문입니다. 몇 초짜리 API 요청은 이 시간 안에 대부분 끝나지만, 파일 변환이나 리포트 생성처럼 오래 걸리는 작업은 애초에 배포 파이프라인 안에서 처리하면 안 됩니다. 이 글에서는 왜 … Read more

Playwright를 서버에서 오래 돌리면 메모리가 계속 느는 이유

Playwright를 서버에 올려서 오래 돌리다 보면 “이 라이브러리는 원래 메모리가 새는구나”라고 오해하기 쉽습니다. 하지만 실제로는 라이브러리 자체의 결함보다 브라우저·컨텍스트·페이지라는 세 계층의 수명을 어떻게 관리하느냐가 메모리 증가 폭을 결정합니다. Playwright를 서버에서 오래 돌리면 메모리가 계속 늘어나는 대부분의 사례는 browser.close()를 안 해서가 아니라, context를 재사용하거나 방치하는 패턴에서 비롯됩니다. 이 글에서는 어떤 계층에서 메모리가 쌓이는지, 왜 close()를 호출해도 … Read more

서브프로세스가 좀비로 남을 때, async generator를 안 닫으면 벌어지는 일

이 글에서는 async generator로 subprocess 출력을 스트리밍하다가 도중에 순회를 멈췄을 때 서브프로세스가 좀비로 남는 정확한 조건과, ps 명령으로 직접 확인하는 방법, 코드 한 줄로 막는 방법까지 확인할 수 있습니다. 원인을 먼저 짚으면, async generator의 GeneratorExit은 async for 루프가 break로 끊기는 순간 곧바로 전달되지 않습니다. 그 generator를 참조하는 곳이 하나라도 남아 있으면 파이썬 가비지 컬렉터가 회수할 … Read more

httpx AsyncClient를 매번 새로 만들면 커넥션 풀이 사라지는 이유

이 글에서는 요청마다 httpx.AsyncClient()를 새로 생성하는 코드가 실제로 어떤 비용을 치르는지, 그리고 트래픽이 늘어날 때 왜 소켓 고갈로 이어지는지 확인할 수 있습니다. 핵심만 먼저 말씀드리면, httpx AsyncClient를 매번 새로 만들면 매 요청마다 TCP 연결과 TLS 협상을 처음부터 다시 진행하게 되고, 연결을 재사용할 기회 자체가 사라집니다. 요청량이 늘어나면 운영체제가 열어줄 수 있는 소켓 수를 넘어서면서 타임아웃이나 … Read more

SQLAlchemy 비동기 세션에서 greenlet_spawn 오류가 나는 이유와 해법

결론부터 말씀드리면, SQLAlchemy 비동기 세션에서 만나는 greenlet_spawn 오류는 거의 대부분 관계(relationship) 속성을 지연 로딩(lazy loading)하려는 순간, 그 코드가 SQLAlchemy의 비동기 브리지 바깥에서 실행되고 있어서 생깁니다. 세션이 닫힌 뒤뿐 아니라 세션이 열려 있어도 발생할 수 있다는 점이 핵심입니다. 이 글에서는 MissingGreenlet 예외가 실제로 뜨는 코드를 재현해 보고, expire_on_commit·selectinload·awaitable_attrs 세 가지 대응법을 SQLAlchemy 2.0 기준으로 비교하겠습니다. greenlet_spawn … Read more

FastAPI BackgroundTasks가 조용히 죽을 때 — 워커를 분리해야 하는 순간

FastAPI BackgroundTasks가 조용히 죽는 지점은 크게 세 가지입니다. 워커 프로세스 재시작, 작업 안에서 발생한 처리되지 않은 예외, 그리고 이미 실행 중인 이벤트 루프 안에서 asyncio.run()을 호출하는 실수입니다. 셋 다 클라이언트에게는 200 응답이 정상적으로 돌아가기 때문에, 문제를 눈치채는 시점이 한참 늦어진다는 공통점이 있습니다. 이 글은 Starlette 기반 FastAPI(백그라운드 작업 처리 구조가 여러 버전에 걸쳐 바뀌지 않은 … Read more