GROWTH LOG STORIES

함께 만든 성장을,
기록으로 연결합니다.

AI와 함께 배우고, 직접 만들고, 서로 나눈
그로스로그 멤버들의 생생한 성장 기록을 만나보세요.

성장 기록
5기+이어진 커뮤니티
HEALTHIO
PROJECT

사진 한 장으로 시작하는 건강 루틴, Health IO

대표 이야기 읽기

ALL STORIES

성장의 순간을
한곳에서 만나보세요.

프로젝트의 시작부터 모임에서 나눈 인사이트까지,
그로스로그가 함께 만든 기록을 모았습니다.
개의 성장 기록

성장일지

[GL 4기 2회차 ②] 일을 떼어내 다른 데로 보내기 — 서브프로그램과 모듈, Celery 작업 큐, PySpark 🌱

GROWTH LOG🌱 2026. 8. 11. 00:07

안녕하세요! 그로스로그 입니다!😊 🌱

코드 한 덩어리를 떼어 이름 붙이는 일, 오래 걸리는 작업을 다른 프로세스에 넘기는 일, 무거운 연산을 여러 대에 나누는 일. 4기 2회차 세 편입니다.

4기 2회차는 2025년 10월입니다. 세 편이 서로 다른 층위에 있는데, 놓고 보면 같은 동작을 규모만 바꿔 하고 있습니다. 일을 떼어내 다른 데로 보내는 것이요.

 

01

🧩 프로그래밍 언어에 대한 이해

강휘윤 님

이런 내용이에요
서브프로그램모듈을 정리한 글입니다. 전공 과목의 개념을 자기 예시로 다시 쓴 기록이에요.
커피 가격과 수량을 받아 총액을 돌려주는 함수를 만들어 "메인이 시키고 서브가 대신 계산해 돌려준다" 로 설명합니다.
모듈은 "같은 이름을 가진 변수나 함수가 서로 충돌하지 않도록, 구분된 방에 저장하는 구조" 라고 풀었습니다.

교재의 정의를 그대로 옮기지 않고 한 번 자기 문장으로 바꿔 쓴 게 이 글의 핵심입니다. 특히 이름 공간을 "구분된 방"으로 옮긴 대목이 그래요.

이 글이 다루는 건 프로그래밍의 가장 기본 동작입니다. 그런데 뒤의 두 편이 하는 일도 결국 같은 동작이에요. 규모만 다릅니다.

조금 더 들어가 보면

📛 이름을 붙이는 순간 생기는 것

서브프로그램을 "코드 블록에 이름을 붙인 것"이라고 정의한 게 정확합니다. 그리고 이름을 붙이면 세 가지가 따라옵니다.

· 부를 수 있게 됩니다 — 이름이 있어야 다른 데서 호출합니다

· 안이 안 보여도 됩니다 — 부르는 쪽은 무엇을 넣으면 무엇이 나오는지만 알면 됩니다

· 바꿔 끼울 수 있습니다 — 이름과 입출력이 같으면 안을 통째로 갈아도 부르는 쪽은 모릅니다

세 번째가 가장 큽니다. 이걸 인터페이스와 구현의 분리라고 부르는데, 소프트웨어에서 규모를 키울 때 쓰는 거의 유일한 방법이에요.

저자가 든 예시로 보면 이렇습니다. 계산하기(커피가격, 수량)을 부르는 쪽은 안에서 곱셈을 하는지 할인율을 적용하는지 모릅니다. 알 필요도 없고요. 나중에 할인 정책이 생겨도 부르는 쪽 코드는 그대로입니다.

🗂️ 함수와 프로시저를 굳이 나누는 이유

값을 돌려주면 함수, 안 돌려주면 프로시저. 이 구분이 요즘 언어에서는 흐려졌지만, 알아두면 쓸모가 있습니다. "이 서브프로그램은 무엇을 하는가"를 두 갈래로 나눠 보게 하기 때문이에요.

· 값을 만들어 돌려주는 것 — 입력이 같으면 출력도 같습니다. 테스트하기 쉽고, 여러 번 불러도 안전합니다

· 무언가를 바꾸는 것 — 파일을 쓰거나 화면에 찍거나 DB를 고칩니다. 두 번 부르면 두 번 일어납니다

이 구분이 함수형 프로그래밍에서 말하는 부수 효과와 같은 이야기입니다. 그리고 실무에서 버그가 나는 자리는 거의 항상 두 번째 쪽이에요. 한 함수가 값도 만들고 상태도 바꾸면, 나중에 그 함수를 다시 쓸 때 예상 못한 일이 같이 일어납니다.

그래서 좋은 습관 하나가 여기서 나옵니다. 계산하는 함수와 바꾸는 함수를 섞지 않기. 저자가 정리한 두 유형이 그 습관의 이론적 근거입니다.

🚪 이름 공간이 없으면 생기는 일

모듈 설명에서 "독자적인 이름 공간을 차지한다"는 대목이 중요합니다. 왜 필요한지 구체적으로 보면 이렇습니다.

프로그램이 커지면 이름이 반드시 겹칩니다. add, parse, Config 같은 이름은 누구나 씁니다. 이름 공간이 없으면 나중에 정의된 게 앞의 것을 덮어써요. 그리고 덮어썼다는 사실이 어디에도 안 나타납니다. 그냥 엉뚱한 게 실행됩니다.

이름 공간은 이걸 calculator.add처럼 어느 방의 것인지 붙여 부르게 해서 막습니다. 저자의 "구분된 방" 비유가 정확한 이유예요.

파이썬에서 import calculator 하고 calculator.add(3, 5)로 부르는 것과, from calculator import *로 전부 끌어오는 것의 차이가 여기 있습니다. 후자는 방문을 떼어버리는 것과 같아서, 편한 대신 충돌을 눈에 안 보이게 만듭니다.

🧭 경로가 왜 환경변수에 들어가는가

저자가 마지막에 붙인 대목이 은근히 실용적입니다. 컴파일러가 모듈을 찾으려면 어디를 뒤질지 알아야 하고, 그래서 그 목록을 환경변수에 둔다는 이야기요.

이게 "왜 내 컴퓨터에서만 안 되지"의 아주 흔한 원인입니다.

· C에서 헤더를 못 찾을 때 — 헤더 검색 경로에 그 폴더가 없는 것

· 파이썬에서 ModuleNotFoundError가 날 때 — sys.path에 그 위치가 없는 것

· 자바에서 클래스를 못 찾을 때 — 클래스패스에 안 걸린 것

셋 다 같은 문제입니다. 물건은 있는데 찾는 곳에 없는 것이요. 이걸 알고 나면 에러 메시지를 보고 "설치가 안 됐나" 대신 "어디를 뒤지고 있나"를 먼저 확인하게 됩니다. 훨씬 빨리 끝나요.

🌐 전역변수가 왜 미움받는가

이름 공간 이야기가 나온 김에 하나 더 붙이겠습니다. 서브프로그램을 배우면 곧바로 만나는 조언이 있어요. "전역변수를 쓰지 마라." 그런데 왜 안 되는지는 잘 안 알려줍니다.

이유가 이 글의 두 개념에서 그대로 나옵니다.

서브프로그램의 값어치는 "무엇을 넣으면 무엇이 나오는지"가 정해져 있다는 데 있습니다. 그래서 따로 테스트할 수 있고, 다른 데 갖다 쓸 수 있고, 안을 갈아도 됩니다.

전역변수를 읽는 순간 이게 깨집니다. 입력이 매개변수만이 아니게 되거든요. 같은 인자를 넣어도 전역변수 값에 따라 결과가 달라집니다. 그러면 이 함수를 테스트하려면 전역 상태를 먼저 맞춰야 하고, 다른 프로그램에 가져가려면 그 전역변수도 같이 가져가야 합니다.

떼어낸 것이 다시 붙어버리는 것이죠. 이름을 붙여 분리한 의미가 없어집니다.

그래서 조언을 이렇게 바꿔 기억하는 게 낫습니다. "전역변수를 쓰지 마라"가 아니라 "함수가 필요로 하는 것은 전부 매개변수로 받아라" 로요. 금지가 아니라 이유가 되면 예외 상황에서도 판단할 수 있습니다.

📥 넘긴 값이 바뀌는 경우

매개변수를 이야기했으니 한 가지만 더 짚겠습니다. 초보자가 가장 많이 놀라는 자리예요.

숫자나 문자열을 함수에 넘기고 안에서 바꿔도, 바깥의 원래 값은 그대로입니다. 그런데 리스트나 딕셔너리를 넘기고 안에서 항목을 고치면 바깥 것도 같이 바뀝니다.

넘어가는 게 값 자체가 아니라 그 물건을 가리키는 주소이기 때문입니다. 함수 안팎이 같은 물건을 보고 있는 것이죠.

이게 "대신 계산하고 반환한다"는 구도를 조용히 깨뜨립니다. 반환값 말고 다른 경로로도 결과가 새어 나가는 것이니까요. 그래서 목록을 받아 처리하는 함수를 쓸 때는 원본을 고칠지 새로 만들어 돌려줄지 정하고, 그 사실을 이름이나 주석에 남겨두는 게 좋습니다.

✂️ 얼마나 잘게 나눌 것인가

"코드를 떼어 이름 붙인다"는 걸 알고 나면 다음 질문이 옵니다. 어디서 자를 것인가.

너무 안 자르면 한 함수가 수백 줄이 되고, 너무 잘게 자르면 함수 이름만 따라다니다 정작 무슨 일이 일어나는지 안 보입니다. 둘 다 읽기 어려워요.

기준을 몇 개 세워두면 편합니다.

· 이름이 잘 안 지어지면 아직 한 가지 일이 아닙니다처리하기, 관리하기 같은 이름밖에 안 떠오르면 안에 두 가지 이상이 섞여 있다는 신호예요

· "그리고"가 들어가면 둘로 나눌 수 있습니다저장하고알림보내기는 두 함수입니다

· 주석을 달고 싶어지는 자리가 자를 자리입니다 — "// 여기서부터 검증"이라고 쓰고 싶었다면, 그 덩어리를 검증하기로 빼면 주석이 필요 없어집니다

세 번째가 특히 실용적입니다. 주석으로 구간을 설명하고 있다면 그 구간에 이미 이름이 있는 셈이거든요. 그 이름을 함수 이름으로 올리면 됩니다.

저자가 서브프로그램의 이점으로 꼽은 "프로그램 구조가 명확해진다" 가 이 이야기입니다. 나누는 게 목적이 아니라, 나눈 결과 이름만 읽어도 흐름이 보이는 것이 목적이에요.

🧱 그리고 모듈로 올라가는 순간

함수를 여러 개 만들다 보면 자연히 다음 단계가 옵니다. 관련된 것들을 한 파일에 모으는 것이요. 저자가 정리한 모듈이 그 자리입니다.

여기서도 같은 질문이 반복됩니다. 무엇을 같이 둘 것인가요.

기준은 함께 바뀌는 것끼리 모으는 것입니다. 계산 관련 함수들이 한 파일에 있으면 계산 규칙이 바뀔 때 그 파일만 열면 되거든요. 반대로 화면 코드와 계산 코드가 한 파일에 섞여 있으면, 둘 중 하나만 바뀌어도 그 파일을 건드리게 됩니다.

함수를 나눌 때 쓴 기준이 파일 단위에서 그대로 반복되고, 나중에는 폴더와 서비스 단위에서 또 반복됩니다. 이번 회차의 뒤 두 편이 그 위쪽 단계를 보여줍니다.

📝 원문에서는 이렇게 씁니다

개념을 커피 계산으로 옮겼습니다.

# 서브 프로그램(= 함수) 정의
def 계산하기(가격, 수량):
    합계 = 가격 * 수량
    return 합계

# 메인 프로그램
커피가격 = 4500
수량 = 2
총금액 = 계산하기(커피가격, 수량)   # 서브 프로그램 호출
print("총 결제금액:", 총금액)
"메인 프로그램이 커피가격과 수량을 계산해줘라고 명령하면, 서브 프로그램이 계산을 대신 수행하고 결과를 반환한다."

"대신 수행하고 반환한다." 이 한 문장이 이번 회차 세 편을 관통합니다.

큐레이터 노트

전공 개념을 교재 문장이 아니라 자기 문장으로 옮긴 기록이라 뽑았습니다. 특히 이름 공간을 "구분된 방"으로 바꿔 쓴 대목이요. 남의 정의를 외운 것과 자기 말로 바꿔본 것은 나중에 꺼내 쓸 때 차이가 납니다.

짧은 글이지만 이번 회차 안에서 자리가 분명합니다. 뒤의 두 편이 다루는 작업 분산이 결국 이 글의 개념을 프로세스 밖으로 확장한 것이거든요. 길이는 선정 기준이 아닙니다.

원문 읽으러 가기  ↗essence00.tistory.com/1
 

02

🧵 Django에 Celery 사용하기(1)

박나은 님

이런 내용이에요
대용량 파일 업로드·다운로드·일괄 변경 같은 작업에 Celery를 쓴 경험을 정리한 글입니다.
구조를 클라이언트 / 메시지 브로커 / 워커 / 결과 백엔드 넷으로 나눠 설명하고, 다이어그램까지 그렸어요.
그리고 기준을 숫자로 적어뒀습니다. "100개 이상 / 총 용량 500MB 이상이면 Celery를 쓴다"고요.
단점도 분명히 씁니다. "단점은 하나다. 어플리케이션 구조가 정말 복잡해진다."

"안 써놓으면 까먹을 것 같아서 정리해보았다"로 시작하는 글입니다. 그런데 결과물은 개인 메모를 넘어섭니다. 직접 굴려본 사람만 쓸 수 있는 판단 기준이 들어 있어서요.

특히 "100개 이상 / 500MB 이상" 이라는 임계값이 그렇습니다. 문서를 읽어서는 나올 수 없는 숫자예요.

조금 더 들어가 보면

⏱️ 왜 요청 안에서 처리하면 안 되는가

웹 요청은 짧아야 합니다. 사용자가 버튼을 누르고 응답을 기다리는 시간이니까요. 그런데 파일 500MB를 처리하는 일은 짧게 끝나지 않습니다.

이때 요청 안에서 그냥 처리하면 세 가지가 한꺼번에 무너집니다.

· 타임아웃 — 웹 서버와 프록시에는 응답 제한 시간이 걸려 있습니다. 넘으면 작업이 끝나기도 전에 연결이 끊깁니다

· 워커 점유 — 웹 서버가 동시에 처리할 수 있는 요청 수는 정해져 있습니다. 무거운 요청 몇 개가 오래 잡고 있으면 다른 사용자의 가벼운 요청까지 밀립니다

· 재시작에 취약 — 배포하느라 서버를 내리면 진행 중이던 작업이 그대로 사라집니다

세 번째가 특히 아픕니다. 30분짜리 작업이 29분에서 날아가면 처음부터 다시 해야 하니까요.

그래서 답은 "요청은 접수만 하고 끝낸다" 입니다. 저자가 그린 흐름이 정확히 그것이에요. 요청을 받아 작업을 큐에 넣고, 사용자에게는 즉시 응답합니다. 실제 처리는 뒤에서 워커가 합니다.

📬 브로커가 있어야 하는 이유

여기서 한 발 더 들어가 보겠습니다. "그냥 스레드 하나 띄워서 백그라운드로 돌리면 안 되나" 싶을 수 있거든요.

됩니다. 다만 웹 서버 프로세스가 죽으면 같이 죽습니다. 배포할 때마다 진행 중인 작업이 사라져요.

메시지 브로커는 이 문제를 작업을 프로세스 바깥에 적어두는 것으로 풉니다. 요청을 받은 쪽은 "이런 일을 해달라"는 메시지를 브로커에 넣고 끝냅니다. 워커는 그걸 꺼내 실행하고요. 그래서 웹 서버를 내렸다 올려도, 워커를 재시작해도 큐에 있는 일은 남아 있습니다.

저자가 장점으로 적은 "작업이 유실되지 않고 복구 후 재처리될 수 있다" 가 이 이야기입니다.

여기서 실무 함정이 하나 붙습니다. 작업이 두 번 실행될 수 있다는 것이요. 워커가 일을 처리하다 중간에 죽으면, 브로커는 그 일이 끝났는지 모릅니다. 그래서 다시 꺼내 실행하게 되고, 앞선 시도가 절반쯤 진행돼 있었다면 그 절반이 두 번 일어납니다.

그래서 큐로 넘기는 작업은 여러 번 실행돼도 결과가 같도록 짜는 게 원칙입니다. "파일을 만든다"보다 "이 이름의 파일이 없으면 만든다"로요. 이걸 멱등성이라고 부릅니다.

🎚️ "100개 / 500MB"라는 기준선의 값어치

이 글에서 가장 실용적인 대목입니다.

Celery를 쓰면 좋은 건 맞지만, 모든 작업을 큐로 넘기면 손해입니다. 큐에 넣고 워커가 꺼내 실행하고 결과를 돌려받는 데도 시간이 듭니다. 100밀리초면 끝날 일을 큐로 보내면 오히려 느려져요.

그래서 실무에서는 "어디부터 큐로 보낼 것인가" 를 정해야 하는데, 이게 문서에는 안 나옵니다. 환경마다 다르니까요.

저자는 그 선을 직접 그었습니다. 100개 이상, 총 500MB 이상. 그리고 그 선을 글에 적어뒀어요. 이 숫자를 그대로 쓸 사람은 없겠지만, "선을 그어야 한다"는 것과 "선을 긋는 방식" 은 그대로 가져갈 수 있습니다.

「숫자로 확인했다」는 선정 신호가 붙는 자리입니다.

⚖️ 단점을 한 줄로 적은 정직함

"단점은 하나다. 어플리케이션 구조가 정말 복잡해진다."

이 문장이 이 글을 소개 글에서 판단 자료로 바꿉니다. 실제로 Celery를 들이면 늘어나는 게 이만큼입니다.

늘어나는 것 내용
설치할 것 Redis나 RabbitMQ 같은 브로커가 하나 더 필요합니다
띄울 것 웹 서버와 별도로 워커 프로세스를 돌리고 관리해야 합니다
볼 것 로그가 두 군데로 나뉩니다. 요청 로그와 워커 로그요
배포할 것 코드가 바뀌면 워커도 같이 재시작해야 합니다

"시스템에 새로운 계층 하나가 더 추가되는 것" 이라는 저자의 표현 그대로입니다. 그래서 작은 서비스에서는 이 비용이 이득보다 클 수 있어요. 그 판단을 하려면 비용이 얼마인지 알아야 하고, 이 글이 그걸 알려줍니다.

🚦 큐를 하나만 쓰면 생기는 일

이 글이 "다음에 자세히"로 미뤄둔 자리 중에 실무에서 가장 먼저 부딪히는 게 하나 있습니다. 큐를 몇 개 둘 것인가요.

처음에는 큐 하나로 시작합니다. 그러다 작업 종류가 늘어나면 문제가 생겨요. 30분짜리 대용량 변환 작업 열 건이 큐에 먼저 들어가 있으면, 그 뒤에 들어온 3초짜리 알림 발송이 다섯 시간을 기다립니다. 워커는 순서대로 꺼내니까요.

그래서 실무에서는 성격이 다른 작업을 다른 큐로 나눕니다. 무거운 배치용 큐와 가벼운 즉시 처리용 큐를 따로 두고, 워커도 큐별로 나눠 붙이는 식이요. 저자가 그린 다이어그램에 큐가 셋으로 그려져 있는 게 그 구조입니다.

나누는 기준은 "얼마나 오래 걸리는가"와 "얼마나 급한가" 입니다. 이 둘이 다르면 같은 줄에 세우면 안 됩니다.

⏰ Celery Beat — 미루는 것과 정해진 때에 하는 것

저자가 장점으로 꼽은 스케줄링도 짚고 넘어갈 만합니다. 지금까지 이야기한 건 "지금 시키되 나중에 처리하기"였는데, 이건 조금 다릅니다. "매일 새벽 3시에 하기" 요.

리눅스의 크론으로도 되는 일인데, 굳이 Celery Beat를 쓰는 이유가 있습니다.

· 작업 코드가 애플리케이션 안에 있습니다 — 크론은 서버에 스크립트를 따로 두고 관리해야 하는데, Beat는 이미 있는 작업을 그대로 시간에 걸어둡니다

· 서버가 여러 대여도 됩니다 — 크론을 서버 세 대에 걸어두면 같은 일이 세 번 돕니다. Beat는 스케줄러가 큐에 한 번만 넣고 워커 하나가 가져갑니다

· 실행 기록이 같은 자리에 남습니다 — 예약 작업과 즉시 작업이 같은 워커 로그에 쌓입니다

두 번째가 특히 큽니다. 서버를 늘리는 순간 크론은 중복 실행 문제가 생기는데, 큐 구조는 그 문제를 구조적으로 안 만듭니다.

📮 결과 백엔드가 왜 선택 사항인가

저자가 "백엔드는 선택 사항에 가깝다"고 적어둔 대목도 정확합니다. 이유를 붙이면 이렇습니다.

작업 중에는 결과를 돌려받아야 하는 것그냥 끝나면 되는 것이 있습니다. 파일 변환 결과를 화면에 보여줘야 한다면 결과가 필요하지만, 알림 메일을 보내는 작업은 보내고 끝나면 됩니다.

결과 백엔드를 켜두면 모든 작업의 결과가 저장소에 쌓입니다. 안 쓰는 결과까지요. 그래서 트래픽이 많은 서비스에서는 이게 조용히 저장 공간을 잡아먹는 원인이 됩니다. 필요한 작업에만 켜거나, 보관 기간을 짧게 두는 게 보통입니다.

"선택 사항에 가깝다"는 한마디에 이 판단이 들어 있어요.

👀 진행 상황을 어떻게 보여줄 것인가

이 구조에서 사용자 쪽에 남는 문제가 하나 있습니다. "즉시 응답"을 받은 다음 사용자는 뭘 보고 있나요.

파일 500MB를 올렸는데 "접수했습니다"만 나오고 끝나면, 사용자는 언제 끝나는지 알 수 없습니다. 그래서 대개 세 가지 중 하나를 붙입니다.

· 주기적으로 물어보기 — 화면이 몇 초마다 "끝났나요?"를 확인합니다. 구현이 가장 쉽습니다

· 끝나면 알려주기 — 서버가 완료 시점에 알림을 밀어줍니다. 즉각적인 대신 연결을 유지해야 합니다

· 다 끝나고 메일이나 알림으로 — 아주 오래 걸리는 작업에 맞습니다

저자가 그린 흐름의 마지막이 "결과 저장/알림" 인데, 그 자리가 이 선택이 들어가는 지점입니다.

그리고 어느 쪽이든 작업에 식별자가 필요합니다. 접수할 때 번호를 하나 만들어 돌려주고, 그 번호로 상태를 조회하거나 알림을 보내는 구조요. 큐로 넘기는 순간 "이 요청이 어떻게 됐는지"를 따라갈 끈이 필요해지는데, 그게 그 번호입니다.

동기 호출에서는 필요 없던 것이 비동기로 가면 반드시 생깁니다. 앞에서 이야기한 복잡성의 실체가 이런 것들이에요.

📝 원문에서는 이렇게 씁니다

Celery를 붙이는 최소 단위를 세 조각으로 보여줍니다.

from celery import Celery

app = Celery(
    "my_app",
    broker="redis://localhost:6379/0",    # 작업을 넣어둘 곳
    backend="redis://localhost:6379/1",   # 결과를 받아둘 곳
)

@app.task                # 평범한 함수를 큐에 넣을 수 있는 작업으로 바꾼다
def add(x, y):
    return x + y
result = add.delay(4, 6)          # 지금 실행하지 않고 큐에 넣는다
final = result.get(timeout=10)    # 결과가 필요할 때 꺼낸다

add(4, 6)add.delay(4, 6)의 차이가 이 글 전체입니다. 앞은 여기서 실행되고, 뒤는 다른 데서 실행됩니다. 데코레이터 한 줄과 .delay() 하나로 실행 장소가 바뀌어요.

저자가 .get()timeout을 걸고 예외 처리를 붙여둔 것도 눈여겨볼 만합니다. 결과를 기다리는 코드는 반드시 기다리다 포기할 줄 알아야 하거든요.

큐레이터 노트

직접 굴려본 사람만 쓸 수 있는 숫자와 단점이 함께 있는 글이라 뽑았습니다. "100개 이상 / 500MB 이상"이라는 임계값은 문서를 읽어서는 안 나오고, "단점은 하나다, 구조가 복잡해진다"는 문장은 써보지 않으면 못 씁니다.

「숫자로 확인했다」와 「단점이나 한계까지 적었다」 두 신호가 같이 붙는 자리예요. 도구를 소개하는 글은 많은데, 도입할지 말지 판단할 수 있게 해주는 글은 드뭅니다. 4회차에 이어지는 편이 다시 나옵니다.

원문 읽으러 가기  ↗world970511.github.io/blog/posts/2025-10-02-2-1.html
 

03

⚡ Pyspark: Python에서 SQL 사용하기

장욱일 님

장욱일 님 원문에 실린 이미지
이런 내용이에요
PySpark로 파이썬 안에서 SQL을 그대로 쓰는 방법을 정리했습니다.
도입부가 특이합니다. 도구 소개로 시작하지 않고 알고리즘과 메모리의 맞교환 이야기로 열어요.
spark.sql()에 SQL을 그대로 넣어 결과를 받고, toPandas()로 익숙한 형태로 바꾸는 흐름까지 보여줍니다.

이 글의 첫 문단이 좋습니다. 도구 이름을 꺼내기 전에 왜 이런 도구가 필요해졌는지를 먼저 적었거든요.

"Memory를 고려하지 않고 단순한 알고리즘으로 코드를 구현하면 Memory의 낭비가 심하게 되고, 작업에 턱없이 부족한 Memory를 제공받게 될 경우 단순한 작업이라도 호출과 반환이 빈번하여 코드가 복잡하게 구현하게 됩니다."

시간과 공간의 맞교환을 실무 언어로 적은 문장입니다. 자원이 넉넉하면 코드가 단순해지고, 자원이 모자라면 코드가 복잡해진다는 것이요.

조금 더 들어가 보면

🧱 판다스가 멈추는 지점

파이썬으로 데이터를 다루면 대개 판다스에서 시작합니다. 그리고 대개 어느 순간 멈춥니다. 한 대의 메모리에 안 들어가는 순간이요.

판다스는 데이터를 전부 메모리에 올려놓고 다룹니다. 빠르고 편한 대신, 데이터가 메모리보다 크면 그 자리에서 끝납니다. 게다가 실제로 쓰는 메모리는 파일 크기의 몇 배가 되는 일이 흔해요. 중간 결과가 계속 새 객체로 만들어지기 때문입니다.

여기서 갈림길이 나옵니다.

· 쪼개서 돌린다 — 파일을 나눠 순서대로 처리합니다. 간단하지만 전체를 한 번에 봐야 하는 연산(정렬, 조인, 그룹 집계)에서 막힙니다

· 여러 대에 나눈다 — 데이터를 조각내 여러 대가 각자 처리하고 결과를 합칩니다

스파크는 두 번째 길입니다. 그리고 이게 앞의 두 편과 같은 동작이에요. 강휘윤 님 글이 일을 함수로 떼어냈고, 박나은 님 글이 프로세스 밖으로 보냈다면, 이 글은 기계 밖으로 보냅니다.

🐢 미루었다가 한 번에 — 지연 실행

스파크를 쓸 때 가장 먼저 놀라는 지점이 있습니다. 코드를 실행했는데 아무 일도 안 일어나는 것이요.

스파크는 연산을 시키는 대로 바로 하지 않습니다. "이렇게 할 것"이라는 계획만 쌓아둡니다. 그러다 결과를 실제로 꺼내는 명령이 오면 그제야 계획 전체를 보고 최적화한 다음 한 번에 실행해요.

이게 왜 이득이냐면, 전체를 미리 알면 중간을 건너뛸 수 있기 때문입니다.

예를 들어 "전부 읽고 → 필터로 1%만 남기고 → 컬럼 두 개만 쓴다"는 계획이 있으면, 실행 시점에 순서를 바꿔 처음부터 필요한 컬럼만, 필요한 행만 읽습니다. 한 줄씩 즉시 실행했다면 다 읽고 나서 버렸을 일이에요.

저자가 쓴 toPandas()가 바로 그 "실제로 꺼내는" 명령 중 하나입니다. 그때까지는 아무것도 안 움직입니다.

⚠️ toPandas()가 편한 만큼 위험한 이유

그런데 이 함수에는 함정이 하나 있습니다. 분산돼 있던 결과를 전부 한 대로 끌어모으는 동작이거든요.

스파크로 큰 데이터를 처리한 다음 toPandas()를 부르면, 그 결과가 지금 코드를 돌리는 한 대의 메모리에 통째로 올라옵니다. 여러 대에 나눠 처리한 이유가 마지막 한 줄에서 사라지는 것이죠.

그래서 실무에서는 이렇게 씁니다.

· 집계가 끝나 작아진 결과에만 toPandas()를 쓴다 — 그래프를 그리거나 보고서에 넣을 정도의 크기

· 큰 결과는 파일로 바로 내보낸다 — 한 대를 거치지 않고 분산된 채로 저장합니다

편한 함수일수록 어디서 무엇이 한곳에 모이는지 알고 쓰는 게 낫습니다. 1회차의 JPA 이야기와 같은 자리예요.

🔤 SQL을 그대로 쓸 수 있다는 것의 의미

이 글의 제목이 정확합니다. "Python에서 SQL 사용하기"요.

여기에는 실질적인 이득이 있습니다. SQL은 무엇을 원하는지만 적는 언어라서요. 어떻게 처리할지는 엔진이 정합니다. 그래서 같은 SQL이 한 대에서도, 백 대에서도 돌아갑니다. 조인 순서를 어떻게 잡을지, 어느 단계에서 데이터를 옮길지는 엔진이 알아서 정해요.

파이썬으로 처리 절차를 직접 쓰면 그 최적화를 사람이 해야 합니다. 선언적으로 적으면 최적화를 넘길 수 있다는 게 이 조합의 값어치예요.

저자가 spark.sql() 안에 SQL을 그대로 넣는 방식을 택한 것도 그래서 합리적입니다. 팀에 SQL을 아는 사람이 많다면 새 문법을 배우지 않고 그대로 가져다 쓸 수 있다는 이점도 있고요.

🔀 분산의 진짜 비용은 옮기는 데 있습니다

여기까지 보면 "여러 대에 나누면 그만큼 빨라진다"로 읽히기 쉬운데, 실제로는 그렇게 안 됩니다. 나누는 것보다 옮기는 게 비쌉니다.

스파크에서 데이터는 조각으로 나뉘어 여러 대에 흩어져 있습니다. 각자 자기 조각만 보면 되는 연산 — 필터링이나 컬럼 계산 같은 것 — 은 옮길 게 없으니 빠릅니다. 대수 늘린 만큼 그대로 빨라져요.

문제는 조각을 넘나들어야 하는 연산입니다. 그룹별 집계, 정렬, 조인이요. "같은 키를 가진 데이터"가 여기저기 흩어져 있으니 한자리에 모아야 하고, 그러려면 기계 사이로 데이터가 이동합니다. 이 과정을 셔플이라고 부릅니다.

셔플은 디스크에 쓰고 네트워크로 보내고 다시 읽는 일이라, 메모리 안에서 끝나는 연산보다 훨씬 느립니다. 분산 처리에서 느린 구간을 찾으면 거의 항상 여기예요.

그래서 실무 요령이 몇 개 나옵니다.

· 필터를 먼저 겁니다 — 옮길 양 자체를 줄이는 게 가장 확실합니다. 지연 실행이 이걸 자동으로 해주기도 하지만, 쿼리를 그렇게 쓰는 편이 안전합니다

· 조인할 때 한쪽이 작으면 그쪽을 복사합니다 — 작은 표를 모든 기계에 통째로 보내두면 큰 쪽을 옮길 필요가 없어집니다

· 키가 한쪽에 몰리지 않는지 봅니다 — 특정 값이 전체의 절반이면 그 조각을 맡은 기계 한 대만 계속 일하고 나머지는 놉니다

"대수를 늘렸는데 왜 안 빨라지지"의 답이 대개 이 셋 중 하나입니다. 저자가 도입부에 적은 알고리즘과 메모리의 맞교환이, 분산 환경에서는 연산과 데이터 이동의 맞교환으로 한 번 더 나타나는 셈이에요.

🏗️ 언제 이걸 안 써도 되는가

한 가지를 분명히 해두는 게 좋겠습니다. 분산 처리 도구는 데이터가 정말 클 때만 이득입니다.

시작하고, 계획을 짜고, 조각을 나누고, 결과를 모으는 데 고정 비용이 듭니다. 그래서 몇십만 건 정도라면 한 대에서 판다스로 도는 게 훨씬 빠릅니다. 클러스터를 띄우는 시간에 이미 끝나 있어요.

기준을 대략 세우면 이렇습니다.

· 메모리에 들어간다 — 판다스나 그냥 SQL로 충분합니다

· 한 대 메모리는 넘는데 디스크에는 들어간다 — 요즘은 한 대에서 디스크를 활용해 처리하는 도구들이 좋아졌습니다. 이 구간이 예전보다 넓어졌어요

· 한 대에 안 들어간다 — 그때가 분산입니다

"큰 데이터"의 기준선이 계속 올라가고 있다는 게 중요합니다. 예전에 클러스터가 필요했던 규모가 지금은 노트북에서 돌기도 하거든요.

그래서 저자가 도입부에 세운 틀이 오래갑니다. 도구 이름이 아니라 "메모리와 알고리즘 중 무엇이 부족한가"를 먼저 보는 방식이라서요. 도구는 바뀌어도 그 질문은 그대로입니다.

📖 SQL을 그대로 쓰는 것의 또 다른 이득

마지막으로 하나만 더 짚겠습니다. 파이썬 코드로 처리 절차를 쓰는 것과 SQL로 쓰는 것의 차이 중에 읽는 사람과 관련된 부분이요.

데이터 작업은 혼자 하지 않습니다. 분석하는 사람, 검증하는 사람, 나중에 유지보수하는 사람이 있어요. 그리고 그 사람들이 전부 파이썬을 잘 아는 건 아닙니다.

SQL은 이 집단에서 가장 넓게 공유되는 언어입니다. 기획이나 마케팅 쪽에서도 읽는 경우가 많고요. 그래서 로직을 SQL로 적어두면 검토받을 수 있는 범위가 넓어집니다.

숫자가 이상할 때 "이 계산이 맞나요"를 물어볼 수 있는 사람이 몇 명이냐가, 데이터 작업의 신뢰도를 크게 좌우합니다. 저자가 택한 방식에는 그 이점도 붙어 있어요.

📝 원문에서는 이렇게 씁니다

도구 이야기 전에 판단의 틀을 먼저 세웁니다.

"데이터 분석 작업의 질과 수행속도를 향상시킴에 있어 크게 알고리즘과 데이터베이스 관리 2가지를 중점적으로 살펴보게 됩니다. 대개 둘은 경쟁관계에 있습니다."
"따라서 작업의 목적과 내용을 수립했을 때, 알고리즘과 데이터베이스 관리 모두를 고려하는 행위는 필수적입니다."

그리고 스파크 설정에 대해 이렇게 적어뒀습니다.

"spark 설정에 대해서는 가용 메모리, core 개수 등을 바꿔 최적화할 수 있지만 대개는 관리자가 내려준 설정을 따르게 됩니다."

한계를 정확히 적은 문장입니다. 이론상 조절 가능한 것과 실제로 조절할 수 있는 것을 나눠 적었어요.

큐레이터 노트

도구를 소개하기 전에 왜 그 도구가 필요해졌는지부터 적은 글이라 뽑았습니다. 알고리즘과 메모리가 맞교환 관계라는 도입부가 그래요. 이 틀이 있으면 스파크뿐 아니라 다른 도구를 고를 때도 같은 질문을 하게 됩니다.

「단점이나 한계까지 적었다」도 붙습니다. 설정을 실제로는 관리자가 정해준다는 대목이요. 할 수 있는 것과 실제로 하게 되는 것을 나눠 적는 건 써본 사람의 문장입니다.

원문 읽으러 가기  ↗mystory22240.tistory.com/1

💡 세 편을 겹쳐 읽으며

같은 동작이 세 층에서 반복됩니다

세 편의 주제가 전혀 달라 보이는데, 하는 일은 하나입니다. 떼어내서 다른 데로 보내는 것이요.

  무엇을 떼어내는가 어디로 보내는가 대신 생기는 문제
서브프로그램 코드 한 덩어리 같은 프로세스 안의 다른 이름 이름 충돌 → 모듈로 방을 나눔
Celery 오래 걸리는 작업 다른 프로세스(워커) 유실·중복 실행 → 브로커와 멱등성
PySpark 무거운 연산 다른 기계(클러스터) 데이터 이동 비용 → 지연 실행과 최적화

규모가 커질 때마다 같은 대가가 붙습니다. 떼어낸 것과 원래 자리 사이에 경계가 생기고, 그 경계를 넘나드는 비용이 새로 발생하죠. 함수 호출은 거의 공짜지만, 프로세스를 넘으면 직렬화가 필요하고, 기계를 넘으면 네트워크가 필요합니다.

그래서 "나누면 좋다"가 아니라 "나눌 만큼 무거운가"가 매번 질문이 됩니다. 박나은 님이 100개·500MB라는 선을 그은 게 정확히 그 질문에 대한 답이고요.

셋 다 "안 보여도 되게" 만듭니다

강휘윤 님 글에서 메인 프로그램은 계산이 어떻게 되는지 모릅니다. 박나은 님 글에서 사용자는 파일이 언제 처리되는지 기다리지 않습니다. 장욱일 님 글에서 분석하는 사람은 몇 대가 도는지 신경 쓰지 않습니다.

추상화가 하는 일이 매번 같습니다. 몰라도 되게 만드는 것이요.

그리고 매번 같은 대가를 치릅니다. 몰라도 되게 만든 것이 새면 원인을 못 찾습니다. toPandas()에서 메모리가 터지거나, 워커가 죽어 작업이 두 번 실행되거나, 모듈 경로를 못 찾거나요. 이번 회차 세 편이 각자 그 지점을 한 번씩 짚습니다.

판단 기준을 글에 남겼습니다

세 편 모두 "이렇게 하면 된다"에서 멈추지 않습니다.

장욱일 님은 알고리즘과 메모리가 경쟁 관계라는 틀을 세우고 시작했고, 박나은 님은 임계값과 단점을 함께 적었고, 강휘윤 님은 왜 서브프로그램을 쓰는지를 이유로 정리했습니다.

방법만 적힌 글은 그 상황에서만 쓰이고, 기준이 적힌 글은 다른 상황에서도 쓰입니다. 큐레이션에서 계속 같은 것을 보고 있는데, 이 회차에서 특히 선명했어요.

이번 회차에서 가져갈 것

1. 계산하는 함수와 상태를 바꾸는 함수를 섞지 않기.

값을 돌려주는 것과 무언가를 바꾸는 것은 성격이 다릅니다. 섞어두면 재사용할 때 예상 못한 일이 같이 일어납니다.

2. "모듈을 못 찾겠다"는 대부분 설치 문제가 아니라 경로 문제.

헤더 검색 경로, sys.path, 클래스패스 — 전부 같은 이야기입니다. 설치를 다시 하기 전에 어디를 뒤지고 있는지 확인하세요.

3. 큐로 넘길 기준을 숫자로 정해두기.

모든 작업을 비동기로 보내면 오히려 느려집니다. "몇 건 이상, 몇 MB 이상"처럼 선을 긋고, 그 선을 코드 옆에 적어두세요.

4. 큐에 넣는 작업은 두 번 실행돼도 괜찮게 짜기.

워커는 죽을 수 있고, 죽으면 그 작업은 다시 실행됩니다. "만든다"보다 "없으면 만든다"로 쓰는 습관이 사고를 막습니다.

5. toPandas()는 결과가 충분히 작아진 뒤에.

분산 처리한 결과를 한 대로 끌어모으는 동작입니다. 큰 결과는 분산된 채로 파일에 내보내세요.


4기 2회차는 2025년 10월이었습니다.

전공 과목 정리와 실무 도구 정리와 데이터 분석이라는 서로 다른 자리에서, 세 분이 나란히 일을 떼어내 다른 데로 보내는 이야기를 남겼습니다. 함수에서 워커로, 워커에서 클러스터로 규모만 커졌을 뿐이에요.

성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.

좋은 글 남겨주신 강휘윤 님, 박나은 님, 장욱일 님께 감사드립니다. 그리고 4기를 함께 채워주신 모든 멤버분들께도요! 😊

다음 큐레이션으로 다시 찾아오겠습니다! 🌱

- 에디터 · 성장일지 큐레이터 -


※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.

WRITE YOUR GROWTH

오늘의 배움을
내일의 성장으로.

기록은 나를 돌아보게 하고, 공유는 우리를 연결합니다.
당신의 다음 성장 이야기도 그로스로그에서 시작해보세요.

커뮤니티 둘러보기