본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 30. · 38 Views
대규모 데이터셋 처리 완벽 가이드
초급 개발자를 위한 대용량 데이터 처리 입문서입니다. 실무에서 만나는 메모리 부족, 처리 시간 폭주 문제를 샘플링, 분산 처리, 스트리밍 기법으로 해결하는 방법을 배웁니다.
목차
1. 대용량 데이터셋의 도전 과제
신입 개발자 김개발 씨가 처음으로 실제 서비스 데이터를 다루게 되었습니다. 로컬에서는 잘 돌아가던 코드가 프로덕션 데이터를 만나자마자 메모리 오류를 뿜으며 죽어버렸습니다.
"왜 개발 환경에서는 문제가 없었는데..."
대용량 데이터셋 처리의 핵심 도전 과제는 메모리 제약과 처리 시간입니다. 마치 작은 트럭으로 대형 건물의 자재를 한 번에 옮기려는 것과 같습니다.
데이터가 커질수록 전략적인 접근이 필요합니다.
다음 코드를 살펴봅시다.
# 잘못된 방법: 전체 데이터를 메모리에 로드
# 문제: 메모리 부족으로 프로그램 종료
with open('huge_dataset.csv', 'r') as f:
all_data = f.readlines() # 수십 GB 데이터를 한 번에!
for line in all_data:
process(line)
# 올바른 방법: 스트리밍으로 한 줄씩 처리
# 메모리 사용량이 일정하게 유지됨
with open('huge_dataset.csv', 'r') as f:
for line in f: # 한 줄씩 읽어서 처리
process(line)
# 처리 후 즉시 메모리에서 해제됨
김개발 씨는 입사 한 달 차 신입 개발자입니다. 오늘 처음으로 실제 프로덕션 데이터를 분석하는 작업을 맡았습니다.
개발 환경에서는 샘플 데이터 1만 건으로 테스트했을 때 완벽하게 작동했습니다. 자신감 넘치게 프로덕션 서버에서 코드를 실행했습니다.
그런데 5분도 지나지 않아 "MemoryError" 메시지와 함께 프로그램이 죽어버렸습니다. 당황한 김개발 씨는 선배 개발자 박시니어 씨를 찾아갔습니다.
"선배님, 개발 환경에서는 잘 돌아갔는데 프로덕션에서는 왜 안 될까요?" 박시니어 씨가 코드를 보더니 빙그레 웃으며 말했습니다. "아, 전형적인 초보 실수네요.
데이터 크기의 함정에 빠진 거예요." 그렇다면 대용량 데이터셋 처리가 왜 어려운 걸까요? 쉽게 비유하자면, 대용량 데이터 처리는 마치 작은 트럭으로 대형 건물의 자재를 운반하는 것과 같습니다.
자재 전체를 한 번에 실을 수는 없습니다. 여러 번 나누어 실어야 하고, 어떤 순서로 실을지도 고민해야 합니다.
때로는 여러 대의 트럭을 동시에 투입해야 할 수도 있습니다. 데이터 처리도 마찬가지입니다.
컴퓨터의 메모리라는 트럭은 크기가 제한되어 있습니다. 수십 GB, 수백 GB의 데이터를 한 번에 실을 수 없습니다.
개발 환경에서는 왜 문제가 없었을까요? 김개발 씨가 사용한 샘플 데이터는 1만 건, 약 10MB 정도였습니다.
이 정도는 메모리에 전부 올려도 아무 문제가 없습니다. 마치 작은 짐을 트럭에 싣는 것처럼 쉬운 일이었습니다.
하지만 프로덕션 데이터는 1억 건, 약 50GB였습니다. 회사 서버의 메모리가 32GB인데, 데이터만 50GB를 차지하려고 하니 당연히 터질 수밖에 없었습니다.
게다가 운영체제와 다른 프로그램도 메모리를 사용하고 있었습니다. 첫 번째 도전 과제는 바로 메모리 제약입니다.
데이터를 한 번에 메모리에 올릴 수 없다면 어떻게 해야 할까요? 조금씩 나누어서 처리해야 합니다.
위의 코드 예제에서 볼 수 있듯이, readlines()로 전체를 읽는 대신 for 루프로 한 줄씩 읽으면 메모리 사용량이 급격히 줄어듭니다. 두 번째 도전 과제는 처리 시간입니다.
1만 건 처리에 1초가 걸렸다면, 1억 건은 단순 계산으로 1만 초, 즉 약 3시간이 걸립니다. 실제로는 데이터가 커지면서 디스크 I/O, 캐시 미스 등의 오버헤드가 추가되어 더 오래 걸릴 수 있습니다.
고객이 분석 결과를 30분 내에 받아야 한다면 어떻게 할까요? 단순히 한 줄씩 처리하는 것만으로는 부족합니다.
여러 대의 컴퓨터로 나누어 처리하거나, 꼭 필요한 데이터만 선별해서 처리해야 합니다. 세 번째 도전 과제는 저장 공간입니다.
텍스트 형식으로 저장된 100GB 데이터를 처리하려면 읽는 것만으로도 시간이 오래 걸립니다. 더 효율적인 바이너리 포맷을 사용하면 용량을 10분의 1로 줄이고 읽기 속도를 10배 빠르게 할 수 있습니다.
박시니어 씨가 설명을 마치자 김개발 씨는 고개를 끄덕였습니다. "그렇군요.
그럼 어떻게 해결하나요?" "방법은 여러 가지가 있어요. 샘플링, 분산 처리, 스트리밍, 샤딩...
이제부터 하나씩 배워봅시다." 대용량 데이터 처리는 단순히 코드를 잘 작성하는 것을 넘어서 시스템적 사고가 필요합니다. 메모리, CPU, 디스크, 네트워크 등 모든 자원을 효율적으로 활용하는 전략을 세워야 합니다.
실전 팁
💡 - 개발 환경에서 테스트할 때는 프로덕션 데이터 크기의 최소 10% 이상으로 테스트하세요
- 메모리 사용량을 모니터링하는 도구(memory_profiler 등)를 사용하여 병목 지점을 찾으세요
- 처음부터 완벽한 최적화를 시도하지 말고, 프로파일링 후 병목을 찾아 개선하세요
2. 데이터 샘플링 기법
김개발 씨가 머신러닝 모델을 학습시키려고 합니다. 전체 데이터는 1억 건인데, 모델 실험을 위해 매번 전체 데이터로 학습하면 하루가 걸립니다.
"빠르게 여러 번 실험하려면 어떻게 해야 할까요?"
데이터 샘플링은 전체 데이터에서 대표성 있는 일부를 추출하는 기법입니다. 마치 여론조사에서 전 국민이 아닌 1000명만 조사해도 전체 의견을 파악할 수 있는 것과 같습니다.
올바른 샘플링으로 시간과 비용을 절약할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 1억 건의 데이터가 있다고 가정
# 방법 1: 랜덤 샘플링 (10% 추출)
sample_random = pd.read_csv('huge_data.csv',
skiprows=lambda i: i>0 and np.random.random() > 0.1)
# 방법 2: 계층적 샘플링 (카테고리별 비율 유지)
df = pd.read_csv('huge_data.csv')
sample_stratified = df.groupby('category').apply(
lambda x: x.sample(frac=0.1) # 각 카테고리에서 10%씩
).reset_index(drop=True)
# 방법 3: 시간 기반 샘플링 (최근 1개월 데이터만)
df['date'] = pd.to_datetime(df['date'])
recent_data = df[df['date'] >= '2024-12-01']
김개발 씨는 이번에 추천 시스템을 개선하는 프로젝트를 맡았습니다. 새로운 알고리즘을 테스트하려면 머신러닝 모델을 학습시켜야 합니다.
문제는 전체 사용자 행동 데이터가 1억 건이나 된다는 것입니다. 첫 번째 실험을 돌려봤습니다.
학습이 끝나기까지 무려 18시간이 걸렸습니다. 결과를 보니 하이퍼파라미터를 조정해야 할 것 같습니다.
"이 속도로는 10번 실험하는 데 일주일이 넘게 걸리겠는걸?" 고민하던 김개발 씨는 다시 박시니어 씨를 찾아갔습니다. "선배님, 실험을 빠르게 여러 번 돌리고 싶은데 방법이 없을까요?" 박시니어 씨가 대답했습니다.
"샘플링을 사용해보세요. 전체 데이터가 꼭 필요한 건 아니에요." 그렇다면 샘플링이란 무엇일까요?
쉽게 비유하자면, 샘플링은 마치 국을 맛볼 때 국자로 한 번만 떠서 맛을 보는 것과 같습니다. 냄비 전체를 다 마셔볼 필요는 없습니다.
국이 고루 섞여 있다면 한 국자만 맛봐도 전체의 맛을 알 수 있습니다. 데이터도 마찬가지입니다.
잘 섞여 있고 편향되지 않았다면, 전체의 10%만 추출해도 전체 데이터의 특성을 파악할 수 있습니다. 샘플링이 없던 시절에는 어땠을까요?
데이터 과학자들은 모델 실험을 한 번 하는 데 며칠씩 기다려야 했습니다. 하이퍼파라미터를 바꿔가며 실험하는 것은 꿈도 꾸지 못했습니다.
결과가 나올 때까지 기다렸다가, 결과가 좋지 않으면 다시 며칠을 기다려야 했습니다. 더 큰 문제는 개발 사이클이 느려진다는 점이었습니다.
빠르게 시도하고 실패하고 배우는 것이 데이터 과학의 핵심인데, 한 번 시도하는 데 너무 오래 걸리니 혁신이 더뎌졌습니다. 바로 이런 문제를 해결하기 위해 샘플링 기법이 발전했습니다.
첫 번째 방법은 랜덤 샘플링입니다. 전체 데이터에서 무작위로 일정 비율을 추출하는 가장 단순한 방법입니다.
위의 코드에서 skiprows를 사용하면 파일을 읽으면서 바로 샘플링할 수 있어 메모리도 절약됩니다. 두 번째 방법은 계층적 샘플링입니다.
만약 데이터에 카테고리별 비율이 중요하다면 어떻게 할까요? 예를 들어 남성 70%, 여성 30%인 데이터에서 랜덤 샘플링을 하면 우연히 남성 90%가 뽑힐 수도 있습니다.
계층적 샘플링은 각 카테고리에서 동일한 비율로 추출합니다. 전체에서 10%를 뽑을 때, 남성 그룹에서 10%, 여성 그룹에서 10%를 뽑는 것입니다.
이렇게 하면 원본 데이터의 분포가 그대로 유지됩니다. 세 번째 방법은 시간 기반 샘플링입니다.
시계열 데이터의 경우, 오래된 데이터보다 최근 데이터가 더 중요할 수 있습니다. 사용자 행동 패턴은 계속 변하기 때문에 3년 전 데이터보다 최근 1개월 데이터가 더 정확한 예측을 만들어냅니다.
실제 현업에서는 어떻게 활용할까요? 넷플릭스 같은 회사에서 추천 알고리즘을 개선한다고 가정해봅시다.
전체 사용자 시청 기록은 수십억 건입니다. 새로운 아이디어를 테스트할 때마다 전체 데이터로 학습하면 비용이 엄청납니다.
대신 계층적 샘플링으로 국가별, 연령대별 비율을 유지하면서 1%만 추출합니다. 이 데이터로 빠르게 실험하고, 유망한 모델만 전체 데이터로 최종 검증합니다.
이렇게 하면 실험 속도는 100배 빨라지고 비용은 100분의 1로 줄어듭니다. 하지만 주의할 점도 있습니다.
초보자들이 흔히 하는 실수는 샘플이 너무 작은 경우입니다. 전체 데이터가 1억 건인데 100건만 샘플링하면 대표성이 없습니다.
통계적으로 의미 있으려면 최소한 수천 건 이상은 되어야 합니다. 또 다른 실수는 편향된 샘플링입니다.
예를 들어 파일의 첫 10%만 읽는다면 어떻게 될까요? 데이터가 날짜순으로 정렬되어 있다면 특정 기간의 데이터만 가져오게 됩니다.
반드시 랜덤하게 추출해야 합니다. 김개발 씨는 계층적 샘플링으로 1%의 데이터를 추출했습니다.
학습 시간이 18시간에서 10분으로 줄어들었습니다. 하루에 10번도 넘게 실험할 수 있게 되었고, 빠르게 최적의 모델을 찾아냈습니다.
"샘플링 하나로 이렇게 달라지다니!" 김개발 씨는 감탄했습니다. 박시니어 씨가 말했습니다.
"데이터가 크다고 해서 항상 전부 쓸 필요는 없어요. 목적에 맞게 똑똑하게 사용하는 게 중요하죠."
실전 팁
💡 - 실험 단계에서는 1-10% 샘플로 빠르게 반복하고, 최종 검증 시에만 전체 데이터를 사용하세요
- 샘플 크기는 통계적 유의성을 고려하여 최소 수천 건 이상으로 설정하세요
- 데이터가 시간순으로 정렬되어 있다면 반드시 랜덤 샘플링을 사용하세요
3. 분산 데이터 처리
김개발 씨가 로그 분석 작업을 맡았습니다. 하루치 로그가 500GB인데, 한 대의 서버로 처리하면 하루가 걸립니다.
"내일까지 보고서를 내야 하는데 어떻게 하지?" 박시니어 씨가 말했습니다. "여러 대의 서버로 나누어 처리하면 됩니다."
분산 처리는 큰 작업을 여러 대의 컴퓨터로 나누어 동시에 처리하는 기법입니다. 마치 큰 짐을 혼자 옮기는 대신 여러 명이 나누어 옮기는 것과 같습니다.
Apache Spark, Dask 같은 프레임워크가 이를 쉽게 만들어줍니다.
다음 코드를 살펴봅시다.
from pyspark.sql import SparkSession
# Spark 세션 생성 (여러 노드에 분산 처리)
spark = SparkSession.builder \
.appName("LogAnalysis") \
.config("spark.executor.instances", "10") \
.getOrCreate()
# 대용량 로그 파일 읽기 (자동으로 분산됨)
logs = spark.read.json("logs/*.json")
# 각 노드에서 병렬로 집계 처리
result = logs.groupBy("user_id") \
.agg({"action": "count", "duration": "sum"}) \
.filter("count > 100")
# 결과 저장 (분산 저장)
result.write.parquet("output/user_stats.parquet")
김개발 씨는 월요일 아침 출근하자마자 급한 요청을 받았습니다. "지난주 사용자 행동 로그를 분석해서 오늘 오후까지 보고서를 만들어주세요." 확인해보니 하루치 로그가 500GB, 일주일이면 3.5TB입니다.
시험 삼아 하루치 로그를 처리해봤습니다. 한 대의 서버로 돌렸더니 무려 24시간이 걸릴 것 같습니다.
일주일치를 처리하려면 일주일이 걸립니다. "이건 불가능한 작업이잖아..." 절망하던 김개발 씨에게 박시니어 씨가 다가왔습니다.
"왜 한 대로 하려고 해요? 회사에 서버가 몇 대인데요.
분산 처리를 사용하세요." 그렇다면 분산 처리란 무엇일까요? 쉽게 비유하자면, 분산 처리는 마치 대형 피자를 여러 명이 나누어 먹는 것과 같습니다.
혼자 먹으면 30분 걸리지만, 10명이 나누어 먹으면 3분이면 끝납니다. 각자 자기 몫을 동시에 먹기 때문에 전체 시간이 크게 줄어듭니다.
데이터 처리도 마찬가지입니다. 3.5TB 데이터를 10대의 서버로 나누면 각 서버는 350GB만 처리하면 됩니다.
모두 동시에 처리하므로 전체 시간이 10분의 1로 줄어듭니다. 분산 처리가 없던 시절에는 어땠을까요?
대용량 데이터를 처리하려면 슈퍼컴퓨터가 필요했습니다. 메모리 1TB, CPU 100개짜리 고가의 장비를 구매해야 했습니다.
중소기업은 엄두도 내지 못하는 비용이었습니다. 더 큰 문제는 확장성이었습니다.
데이터가 두 배로 늘어나면 컴퓨터도 두 배로 성능이 좋은 것을 사야 했습니다. 하지만 성능이 두 배인 컴퓨터는 가격이 세 배, 네 배였습니다.
구글이 이 문제를 혁신적으로 해결했습니다. 2004년 구글은 MapReduce 논문을 발표했습니다.
핵심 아이디어는 간단했습니다. "비싼 슈퍼컴퓨터 한 대 대신, 저렴한 일반 서버 1000대를 사용하자." 데이터를 잘게 쪼개서 각 서버에 분산하고, 결과를 모으는 것입니다.
이후 Apache Hadoop, Apache Spark 같은 오픈소스 프레임워크가 등장했습니다. 이제 누구나 분산 처리를 쉽게 사용할 수 있게 되었습니다.
위의 코드를 살펴보겠습니다. 첫 번째 줄에서 Spark 세션을 생성합니다.
spark.executor.instances를 10으로 설정하면 10대의 서버(정확히는 Executor)가 작업을 나누어 처리합니다. 이것만으로 분산 처리 환경이 준비됩니다.
두 번째 단계에서 로그 파일을 읽습니다. 일반 파이썬 코드와 비슷해 보이지만, 내부에서는 엄청난 일이 벌어집니다.
Spark가 자동으로 파일을 여러 조각으로 나누어 각 Executor에 분배합니다. 세 번째 단계는 집계 처리입니다.
각 Executor는 자기가 맡은 데이터만 처리합니다. 사용자별로 그룹핑하고, 액션 개수와 사용 시간을 계산합니다.
모든 Executor가 동시에 일하므로 속도가 빠릅니다. 마지막으로 결과를 저장합니다.
Parquet은 컬럼 기반 저장 포맷으로, 압축률이 높고 읽기 속도가 빠릅니다. 이것도 분산 저장되어 나중에 빠르게 읽을 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 넷플릭스는 매일 수백 TB의 시청 로그를 처리합니다.
어떤 영화가 인기 있는지, 어느 시점에 사용자가 영화를 끄는지 등을 분석합니다. 이 모든 것을 수천 대의 서버로 분산 처리합니다.
우버는 실시간으로 수백만 건의 위치 데이터를 처리합니다. 승객과 가장 가까운 기사를 찾고, 예상 도착 시간을 계산합니다.
Spark Streaming으로 데이터를 초 단위로 처리하여 빠른 응답을 제공합니다. 하지만 주의할 점도 있습니다.
초보자들이 흔히 하는 실수는 너무 작은 데이터에 분산 처리를 사용하는 것입니다. 분산 처리는 오버헤드가 있습니다.
데이터를 나누고, 네트워크로 전송하고, 결과를 모으는 데 시간이 걸립니다. 만약 데이터가 1GB밖에 안 된다면 어떻게 될까요?
한 대의 서버로 1분이면 처리할 수 있는데, 분산 처리를 위한 준비에만 30초가 걸릴 수 있습니다. 오히려 더 느려지는 것입니다.
일반적으로 10GB 이상의 데이터부터 분산 처리의 효과가 나타납니다. 데이터가 크고, 처리 로직이 복잡할수록 효과가 큽니다.
김개발 씨는 Spark로 일주일치 로그를 처리했습니다. 10대의 서버를 사용하니 24시간 걸릴 작업이 3시간 만에 끝났습니다.
여유롭게 오후 전에 보고서를 완성했습니다. "분산 처리가 이렇게 강력한 거였군요!" 김개발 씨가 감탄하자, 박시니어 씨가 말했습니다.
"클라우드 시대에는 필수 기술이에요. 서버를 필요한 만큼 빌려서 쓰고 반납하면 되니까요."
실전 팁
💡 - 데이터가 10GB 미만이라면 단일 서버 처리가 더 효율적일 수 있습니다
- Spark를 처음 사용한다면 로컬 모드로 먼저 테스트해보고 클러스터로 확장하세요
- Parquet, ORC 같은 컬럼 기반 포맷을 사용하면 I/O 성능이 크게 개선됩니다
4. 샤딩 및 병렬화 전략
김개발 씨가 데이터베이스 성능 문제를 겪고 있습니다. 사용자 테이블에 1억 건의 레코드가 있는데, 조회 쿼리가 점점 느려집니다.
"인덱스도 다 걸었는데 왜 이렇게 느릴까요?" 박시니어 씨가 대답했습니다. "테이블을 샤딩해야 할 시점이네요."
샤딩은 데이터를 여러 개의 데이터베이스로 나누어 저장하는 기법입니다. 마치 도서관 책을 주제별로 다른 건물에 분산 보관하는 것과 같습니다.
병렬화는 여러 작업을 동시에 처리하여 전체 시간을 단축합니다.
다음 코드를 살펴봅시다.
import multiprocessing as mp
from functools import partial
# 샤딩 키를 기준으로 데이터베이스 선택
def get_shard_db(user_id, num_shards=10):
shard_id = user_id % num_shards
return f"database_shard_{shard_id}"
# 병렬 처리를 위한 워커 함수
def process_chunk(chunk_data, operation):
results = []
for item in chunk_data:
# 각 아이템을 처리
result = operation(item)
results.append(result)
return results
# 데이터를 청크로 나누고 병렬 처리
def parallel_process(data, operation, num_workers=4):
chunk_size = len(data) // num_workers
chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]
# 멀티프로세싱으로 병렬 실행
with mp.Pool(num_workers) as pool:
results = pool.map(partial(process_chunk, operation=operation), chunks)
# 결과 병합
return [item for sublist in results for item in sublist]
김개발 씨가 운영하는 서비스가 빠르게 성장했습니다. 처음 런칭했을 때는 사용자가 만 명이었는데, 이제는 천만 명을 돌파했습니다.
데이터베이스의 사용자 테이블도 1억 건이 넘어갔습니다. 문제는 조회 속도였습니다.
예전에는 사용자 정보를 가져오는 데 0.1초면 충분했는데, 이제는 5초씩 걸립니다. 인덱스도 다 만들었고, 쿼리 최적화도 했는데 여전히 느립니다.
"도대체 뭐가 문제일까?" 고민하던 김개발 씨는 박시니어 씨에게 조언을 구했습니다. 박시니어 씨가 데이터베이스를 살펴보더니 말했습니다.
"단일 데이터베이스의 한계에 도달한 거예요. 이제 샤딩을 해야 합니다." 그렇다면 샤딩이란 무엇일까요?
쉽게 비유하자면, 샤딩은 마치 대형 도서관을 여러 건물로 나누는 것과 같습니다. 모든 책을 한 건물에 보관하면 책을 찾는 데 시간이 오래 걸립니다.
하지만 주제별로 다른 건물에 나누어 보관하면 어떻게 될까요? 문학은 A동, 과학은 B동, 역사는 C동에 보관합니다.
이제 역사책을 찾는 사람은 C동으로만 가면 됩니다. 전체 책의 3분의 1만 뒤지면 되니 훨씬 빠릅니다.
데이터베이스도 마찬가지입니다. 1억 건의 사용자 데이터를 10개의 데이터베이스로 나누면 각 데이터베이스는 1000만 건만 관리합니다.
조회 속도가 10배 빨라집니다. 샤딩이 없던 시절에는 어떻게 했을까요?
수직 확장만 가능했습니다. 데이터가 늘어나면 더 좋은 서버로 업그레이드했습니다.
CPU를 더 빠른 것으로, 메모리를 더 큰 것으로, 디스크를 더 빠른 SSD로 교체했습니다. 하지만 수직 확장에는 한계가 있습니다.
성능이 두 배인 서버는 가격이 네 배입니다. 어느 순간부터는 돈을 아무리 써도 성능 향상이 미미합니다.
그리고 한 대의 서버가 죽으면 전체 서비스가 멈춥니다. 샤딩은 수평 확장을 가능하게 만들었습니다.
위의 코드에서 get_shard_db 함수를 보겠습니다. 사용자 ID를 10으로 나눈 나머지를 사용합니다.
사용자 ID가 12345라면 12345 % 10 = 5이므로 database_shard_5에 저장됩니다. 이렇게 하면 사용자가 고르게 분산됩니다.
사용자 ID가 0-9는 shard_0에, 10-19는 shard_0에, 20-29는 shard_0에... 잠깐, 이것도 shard_0이네요?
그렇습니다. 1의 자리 숫자로 나누기 때문에 0,10,20,30...
모두 shard_0에 갑니다. 이것이 해시 샤딩의 장점입니다.
사용자가 어떤 순서로 가입하든, 어떤 ID를 받든, 자동으로 고르게 분산됩니다. 샤딩과 함께 사용하는 것이 병렬화입니다.
parallel_process 함수를 보면 데이터를 청크로 나누고 여러 프로세스가 동시에 처리합니다. 1만 건의 데이터를 4개 프로세스로 나누면 각자 2500건씩 처리합니다.
CPU 코어가 4개라면 실제로 4배 빠르게 처리할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
인스타그램은 전 세계 10억 명 이상의 사용자를 서비스합니다. 사용자 데이터를 수천 개의 샤드로 나누어 저장합니다.
지역별, ID 범위별로 나누어 각 샤드가 부담을 나누어 집니다. 또한 피드를 생성할 때 병렬 처리를 사용합니다.
팔로우하는 100명의 최신 게시물을 가져오는 작업을 10개 스레드가 나누어 처리합니다. 순차적으로 하면 10초 걸릴 일을 1초 만에 끝냅니다.
하지만 주의할 점도 있습니다. 첫 번째 함정은 샤드 키 선택입니다.
잘못된 키를 선택하면 데이터가 한쪽에 몰립니다. 예를 들어 가입 날짜로 샤딩하면 어떻게 될까요?
최근에 가입한 사용자들이 모두 같은 샤드에 몰려 부하가 집중됩니다. 좋은 샤드 키는 고르게 분산되고, 변하지 않으며, 조회 패턴과 일치합니다.
사용자 ID가 대표적인 좋은 샤드 키입니다. 두 번째 함정은 조인 쿼리입니다.
여러 샤드에 걸친 데이터를 조인하려면 어떻게 해야 할까요? 각 샤드에서 데이터를 가져와서 애플리케이션에서 합쳐야 합니다.
복잡하고 느립니다. 따라서 샤딩 설계 시에는 조인이 필요 없도록 데이터를 배치하는 것이 중요합니다.
자주 함께 조회되는 데이터는 같은 샤드에 두어야 합니다. 김개발 씨는 사용자 ID 기준으로 10개의 샤드를 만들었습니다.
조회 속도가 5초에서 0.3초로 줄어들었습니다. 병렬 처리도 적용하여 배치 작업 시간이 10시간에서 2시간으로 단축되었습니다.
"샤딩과 병렬화로 이렇게 달라지다니!" 김개발 씨가 놀라워하자, 박시니어 씨가 말했습니다. "대용량 서비스의 핵심은 잘 나누는 것입니다.
Divide and Conquer죠."
실전 팁
💡 - 샤드 키는 변하지 않고 고르게 분산되는 값(사용자 ID 등)을 선택하세요
- 샤드 개수를 나중에 바꾸기는 어려우니 충분한 여유를 두고 설계하세요
- 병렬 처리 시 프로세스 개수는 CPU 코어 수와 비슷하게 설정하는 것이 효율적입니다
5. 효율적인 저장 형식
김개발 씨가 CSV 파일로 데이터를 저장하고 있습니다. 파일 크기가 100GB나 되고, 읽는 데만 30분이 걸립니다.
"왜 이렇게 느린 거죠?" 박시니어 씨가 대답했습니다. "CSV는 사람이 읽기 편한 포맷이지, 컴퓨터가 읽기 편한 포맷이 아니에요."
효율적인 저장 형식은 데이터 크기를 줄이고 읽기 속도를 높입니다. Parquet, Avro, ORC 같은 바이너리 포맷은 CSV보다 10배 작고 10배 빠릅니다.
컬럼 기반 저장으로 필요한 데이터만 읽을 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import pyarrow.parquet as pq
# CSV로 저장 (비효율적)
df = pd.read_csv('data.csv')
# 파일 크기: 10GB, 읽기 시간: 3분
# Parquet로 저장 (효율적)
df.to_parquet('data.parquet', compression='snappy')
# 파일 크기: 1GB (90% 감소!), 읽기 시간: 10초
# 특정 컬럼만 읽기 (컬럼 기반 저장의 장점)
# CSV는 전체를 읽어야 함
df_csv = pd.read_csv('data.csv', usecols=['user_id', 'purchase_amount'])
# Parquet는 필요한 컬럼만 읽음 (훨씬 빠름)
df_parquet = pd.read_parquet('data.parquet', columns=['user_id', 'purchase_amount'])
# 조건 필터링과 함께 읽기
table = pq.read_table('data.parquet',
filters=[('date', '>=', '2024-01-01')])
김개발 씨는 매일 생성되는 로그 데이터를 CSV 파일로 저장하고 있었습니다. 처음에는 하루치가 100MB 정도였으니 문제가 없었습니다.
파일을 열어서 눈으로 확인할 수 있어 편했습니다. 6개월이 지나자 상황이 달라졌습니다.
서비스가 성장하면서 하루치 로그가 100GB를 넘어섰습니다. 이 파일을 판다스로 읽으려고 하면 30분이 걸리고, 메모리도 150GB나 사용합니다.
"대체 왜 이렇게 느리고 무거운 거죠?" 김개발 씨가 박시니어 씨에게 물었습니다. 박시니어 씨가 CSV 파일을 보더니 고개를 저었습니다.
"CSV는 대용량 데이터에는 적합하지 않아요. Parquet 같은 포맷으로 바꿔보세요." 그렇다면 왜 CSV는 비효율적일까요?
쉽게 비유하자면, CSV는 마치 손으로 쓴 편지와 같습니다. 사람이 읽기에는 편하지만 컴퓨터가 처리하기에는 비효율적입니다.
숫자 "12345"를 저장할 때 5바이트나 사용합니다. 각 글자마다 1바이트씩입니다.
반면 바이너리 포맷은 컴퓨터 친화적입니다. 숫자 12345를 2바이트만으로 저장할 수 있습니다.
압축을 사용하면 더 줄어듭니다. 마치 압축 파일처럼 공간을 효율적으로 사용합니다.
CSV의 문제는 이것만이 아닙니다. 로우 기반 저장 방식이라는 것이 더 큰 문제입니다.
데이터를 한 줄씩 저장합니다. 100개 컬럼이 있는 테이블에서 2개 컬럼만 필요해도 전체를 다 읽어야 합니다.
마치 책 한 권에서 2페이지만 보려는데 전체를 다 펼쳐야 하는 것과 같습니다. Parquet 같은 포맷은 컬럼 기반 저장을 사용합니다.
각 컬럼을 별도로 저장합니다. user_id 컬럼, purchase_amount 컬럼, date 컬럼...
이렇게 나누어 저장합니다. 이제 2개 컬럼만 필요하면 그 2개만 읽으면 됩니다.
읽는 데이터 양이 50분의 1로 줄어듭니다. 또 다른 장점은 압축 효율입니다.
같은 타입의 데이터가 모여 있으면 압축이 훨씬 잘 됩니다. 날짜 컬럼에는 날짜만 있고, 숫자 컬럼에는 숫자만 있으니 패턴을 찾기 쉽습니다.
위의 코드를 살펴보겠습니다. 첫 번째 부분에서 CSV로 저장하면 10GB가 나옵니다.
같은 데이터를 Parquet로 저장하면 압축(snappy) 덕분에 1GB밖에 안 됩니다. 90%나 줄어든 것입니다.
읽기 속도는 3분에서 10초로, 18배 빨라집니다. 두 번째 부분에서 특정 컬럼만 읽는 예제를 봅니다.
CSV는 usecols를 지정해도 내부적으로는 전체를 읽고 필요한 것만 추립니다. 하지만 Parquet는 정말로 해당 컬럼만 디스크에서 읽습니다.
세 번째 부분은 필터 푸시다운입니다. 날짜 조건을 주면 Parquet는 메타데이터를 보고 해당하는 블록만 읽습니다.
1년치 데이터에서 1개월치만 필요하면 12분의 1만 읽으면 됩니다. 실제 현업에서는 어떻게 활용할까요?
AWS의 S3에 저장된 데이터를 분석하는 서비스 Athena를 생각해봅시다. CSV로 저장하면 1TB 데이터를 스캔하는 데 5달러가 듭니다.
Parquet로 저장하고 컬럼 선택으로 스캔량을 줄이면 0.5달러만 듭니다. 비용이 10분의 1이 됩니다.
넷플릭스는 시청 로그를 Parquet로 저장합니다. 수백 TB의 데이터를 효율적으로 관리하고, 분석 쿼리 응답 시간을 대폭 단축했습니다.
저장 공간 절약으로 연간 수억 원을 아낍니다. 하지만 주의할 점도 있습니다.
Parquet는 쓰기가 느립니다. CSV는 한 줄씩 추가하면 되지만, Parquet는 컬럼별로 재구성해야 합니다.
따라서 실시간으로 조금씩 추가하는 용도로는 적합하지 않습니다. 대신 배치 작업에 적합합니다.
하루치 로그를 모아서 한 번에 Parquet로 저장하는 식입니다. 한 번 저장하면 여러 번 읽으므로 충분히 가치가 있습니다.
또 다른 주의점은 사람이 읽을 수 없다는 것입니다. CSV는 텍스트 에디터로 열어볼 수 있지만, Parquet는 바이너리라 깨져 보입니다.
디버깅할 때는 파이썬이나 Spark 같은 도구로 읽어야 합니다. 언제 CSV를 쓰고 언제 Parquet를 쓸까요?
CSV는 작은 데이터(1GB 이하), 사람이 확인할 데이터, 외부 시스템과 주고받을 데이터에 적합합니다. 간단하고 호환성이 좋습니다.
Parquet는 큰 데이터(10GB 이상), 분석용 데이터, 자주 읽는 데이터에 적합합니다. 성능과 비용 면에서 압도적으로 유리합니다.
김개발 씨는 로그 저장 형식을 Parquet로 바꿨습니다. 100GB였던 파일이 8GB로 줄었고, 읽기 시간이 30분에서 1분으로 단축되었습니다.
S3 저장 비용도 월 1000달러에서 100달러로 줄었습니다. "포맷 하나 바꿨을 뿐인데 이렇게 달라지다니!" 김개발 씨가 놀라워하자, 박시니어 씨가 말했습니다.
"적재적소에 맞는 도구를 쓰는 게 중요해요. 대용량 데이터는 대용량에 맞는 포맷이 필요하죠."
실전 팁
💡 - 1GB 이상 데이터는 Parquet, 미만은 CSV 사용을 고려하세요
- Parquet 압축은 snappy(빠름)나 gzip(작음) 중 용도에 맞게 선택하세요
- 자주 필터링하는 컬럼은 파티셔닝하여 저장하면 읽기 속도가 더 빨라집니다
6. 스트리밍 처리와 메모리 최적화
김개발 씨가 10GB짜리 JSON 파일을 파싱하려고 합니다. 파일을 통째로 읽으니 메모리가 부족해 프로그램이 죽습니다.
"이렇게 큰 파일은 어떻게 처리하나요?" 박시니어 씨가 대답했습니다. "한 번에 다 읽지 말고, 조금씩 스트리밍으로 읽으세요."
스트리밍 처리는 데이터를 조금씩 읽어서 처리하고 버리는 방식입니다. 마치 컨베이어 벨트에서 물건을 하나씩 처리하는 것과 같습니다.
메모리 사용량이 일정하게 유지되어 아무리 큰 파일도 처리할 수 있습니다.
다음 코드를 살펴봅시다.
import json
# 잘못된 방법: 전체 파일을 메모리에 로드
# 10GB 파일이면 메모리 20GB 이상 사용
with open('huge.json', 'r') as f:
data = json.load(f) # 전체를 한 번에!
for item in data:
process(item)
# 올바른 방법: 스트리밍으로 한 줄씩 처리
# 메모리 사용량 일정 (수십 MB)
def process_large_file(filename):
with open(filename, 'r') as f:
for line in f:
# JSON Lines 형식: 한 줄에 하나의 JSON 객체
item = json.loads(line.strip())
process(item)
# 처리 후 즉시 메모리에서 해제
# Generator를 사용한 메모리 효율적 처리
def read_chunks(filename, chunk_size=1000):
chunk = []
with open(filename, 'r') as f:
for line in f:
chunk.append(json.loads(line.strip()))
if len(chunk) >= chunk_size:
yield chunk # 청크 반환
chunk = [] # 메모리 해제
if chunk:
yield chunk # 마지막 청크
# 청크 단위로 처리
for chunk in read_chunks('huge.json'):
batch_process(chunk) # 1000건씩 묶어서 처리
김개발 씨는 외부 API에서 받은 데이터를 처리하는 작업을 맡았습니다. 파일을 열어보니 10GB짜리 JSON 파일이었습니다.
"일단 읽어보자" 하고 json.load()를 실행했습니다. 프로그램이 메모리를 계속 먹더니 어느 순간 "MemoryError"를 뱉으며 죽어버렸습니다.
회사 서버 메모리가 16GB인데, 파일이 10GB니 당연한 일이었습니다. 게다가 JSON 파싱 과정에서 추가 메모리가 필요해 실제로는 20GB 이상 사용했습니다.
"이렇게 큰 파일은 어떻게 처리하죠?" 절망하던 김개발 씨에게 박시니어 씨가 조언했습니다. "전체를 한 번에 읽으려고 하지 마세요.
스트리밍으로 조금씩 읽으세요." 그렇다면 스트리밍 처리란 무엇일까요? 쉽게 비유하자면, 스트리밍 처리는 마치 공장의 컨베이어 벨트와 같습니다.
제품이 끊임없이 흘러오지만, 작업자는 눈앞의 제품 하나만 처리합니다. 처리가 끝나면 다음 제품이 옵니다.
한 번에 수천 개를 쌓아두지 않습니다. 데이터 처리도 마찬가지입니다.
10GB 파일 전체를 메모리에 올리지 않습니다. 한 줄을 읽고, 처리하고, 버립니다.
다음 줄을 읽고, 처리하고, 버립니다. 메모리는 항상 한 줄 크기만 사용합니다.
스트리밍이 없던 시절에는 어땠을까요? 일괄 처리만 가능했습니다.
파일을 전부 읽고, 전부 처리하고, 전부 쓰는 방식이었습니다. 메모리가 부족하면 처리할 수 없었습니다.
더 큰 메모리를 가진 서버를 사야 했습니다. 또 다른 문제는 응답 시간이었습니다.
10GB 파일을 전부 읽는 데 5분이 걸린다면, 첫 번째 결과를 보려면 5분을 기다려야 했습니다. 스트리밍을 사용하면 1초 만에 첫 번째 결과를 볼 수 있습니다.
스트리밍 처리가 이 문제를 해결했습니다. 위의 코드를 살펴보겠습니다.
첫 번째 예제는 json.load()로 전체를 읽습니다. 10GB 파일이면 메모리가 20GB 이상 필요합니다.
대부분의 컴퓨터에서 죽습니다. 두 번째 예제는 for 루프로 한 줄씩 읽습니다.
파일이 JSON Lines 형식(한 줄에 하나의 JSON 객체)이라고 가정합니다. 각 줄은 보통 수 KB밖에 안 되므로 메모리는 수십 MB만 사용합니다.
핵심은 처리 후 즉시 메모리에서 해제된다는 것입니다. 파이썬의 가비지 컬렉터가 자동으로 메모리를 정리합니다.
따라서 파일이 아무리 커도 메모리 사용량은 일정합니다. 세 번째 예제는 Generator를 사용합니다.
파이썬의 강력한 기능 중 하나입니다. yield 키워드를 사용하면 값을 하나씩 반환하면서 실행을 멈췄다가 다시 시작합니다.
read_chunks 함수는 1000건씩 묶어서 반환합니다. 왜 이렇게 할까요?
때로는 한 건씩 처리하는 것보다 여러 건을 묶어서 배치 처리하는 것이 효율적이기 때문입니다. 데이터베이스에 삽입할 때 1000건씩 bulk insert하면 훨씬 빠릅니다.
실제 현업에서는 어떻게 활용할까요? 트위터(현 X)는 초당 수천 개의 트윗을 처리합니다.
이것을 모아서 한 번에 처리하면 메모리가 터집니다. 대신 Kafka 같은 스트리밍 플랫폼을 사용하여 실시간으로 처리합니다.
트윗이 들어오는 즉시 감정 분석을 하고, 스팸을 필터링하고, 트렌드를 계산합니다. 데이터가 끊임없이 흘러가지만 메모리는 일정하게 유지됩니다.
로그 분석 도구 Elasticsearch도 스트리밍을 사용합니다. 수십 GB의 로그 파일을 인덱싱할 때 한 번에 읽지 않습니다.
청크로 나누어 읽고, 처리하고, 인덱스에 저장합니다. 메모리 사용량을 제한하면서도 빠르게 처리합니다.
하지만 주의할 점도 있습니다. 첫 번째 제약은 순차 접근만 가능하다는 것입니다.
스트리밍은 앞에서 뒤로 한 방향으로만 읽습니다. 뒤로 돌아가거나 랜덤 액세스는 불가능합니다.
만약 전체 데이터를 여러 번 읽어야 한다면 스트리밍이 적합하지 않을 수 있습니다. 두 번째 제약은 집계 연산의 어려움입니다.
평균을 계산하려면 모든 값을 더하고 개수로 나눠야 합니다. 스트리밍에서는 합계와 개수를 누적하면서 마지막에 계산해야 합니다.
중앙값이나 퍼센타일 같은 통계는 더 복잡합니다. 언제 스트리밍을 사용하고 언제 일괄 처리를 사용할까요?
스트리밍은 큰 파일(1GB 이상), 실시간 처리, 메모리 제약이 있을 때 적합합니다. ETL 파이프라인, 로그 처리, 데이터 변환 등에 사용합니다.
일괄 처리는 작은 데이터, 복잡한 분석, 여러 번 읽어야 할 때 적합합니다. 머신러닝 학습, 통계 분석 등에 사용합니다.
김개발 씨는 스트리밍 방식으로 코드를 고쳤습니다. 10GB 파일이 메모리 50MB만 사용하며 처리되었습니다.
이제 100GB 파일도 문제없이 처리할 수 있습니다. "스트리밍이 이렇게 강력한 거였군요!" 김개발 씨가 감탄하자, 박시니어 씨가 말했습니다.
"큰 데이터를 다룰 때는 항상 '한 번에 전부'가 아니라 '조금씩 흘려가며'를 생각하세요." 김개발 씨는 이제 대용량 데이터를 다루는 자신감이 생겼습니다. 샘플링으로 빠르게 실험하고, 분산 처리로 시간을 단축하고, 샤딩으로 데이터베이스를 확장하고, 효율적인 포맷으로 비용을 절약하고, 스트리밍으로 메모리 문제를 해결했습니다.
"이제 어떤 크기의 데이터가 와도 두렵지 않아요!" 김개발 씨의 말에 박시니어 씨가 미소 지었습니다. "그래도 항상 프로파일링을 먼저 하세요.
최적화는 병목을 찾고 나서 하는 거예요."
실전 팁
💡 - JSON Lines 형식(한 줄에 하나의 JSON)을 사용하면 스트리밍 처리가 쉬워집니다
- Generator는 메모리 효율적이지만 한 번만 순회 가능하니 주의하세요
- 배치 크기(chunk_size)는 메모리와 처리 속도를 고려하여 조정하세요 (보통 100-10000)
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.