파이썬129번5 MIN
mmap: 메모리 맵 파일로 대용량 처리
Python mmap 모듈로 파일을 메모리처럼 직접 접근하는 방법을 설명합니다. OS 페이지 캐시 활용, 복사 없는 읽기·쓰기, 대용량 파일 고속 검색, 정규식 적용, 공유 메모리 패턴까지 다룹니다.
이 글은 PALDYN Tech Blog의 글입니다.원문 보기
지난 글에서 pathlib로 파일을 읽고 쓰는 현대적인 방법을 살펴봤다. 이번에는 한 단계 더 낮은 레벨, OS의 가상 메모리 시스템을 직접 활용하는 mmap을 다룬다. 수 GB 짜리 파일을 복사 없이 접근하거나, 여러 프로세스가 같은 파일을 공유할 때 mmap은 강력한 도구다.
mmap이 특별한 이유
일반 read()는 디스크 → OS 페이지 캐시 → 앱 버퍼로 두 번 복사된다. mmap은 파일을 프로세스의 가상 주소 공간에 직접 매핑한다. 파일의 일부를 읽으면 OS가 해당 페이지만 디스크에서 로드하고, 앱은 그 메모리에 직접 접근한다.
이 방식의 장점은 다음과 같다.
- 제로 복사: 앱 버퍼로의 복사가 없다
- 필요한 부분만 로드: 10GB 파일이라도 접근한 페이지만 메모리에 올라간다
- OS 페이지 캐시 재사용: 이미 읽힌 페이지는 캐시에서 즉시 반환된다
- 슬라이싱·인덱싱 지원:
bytes처럼mm[100:200]으로 접근 가능
기본 사용
import mmap
# 읽기 전용
with open("large_data.bin", "rb") as f:
mm = mmap.mmap(
f.fileno(), # 파일 디스크립터
0, # 0 = 파일 전체 매핑
access=mmap.ACCESS_READ
)
# bytes처럼 사용
print(mm[0:4]) # 처음 4바이트
print(mm.find(b"MAGIC")) # 패턴 위치 찾기
mm.seek(1000)
chunk = mm.read(50) # seek+read도 가능
mm.close()
# with 문으로 자동 close
with open("data.bin", "rb") as f:
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
data = mm[:] # 전체 복사 (일반 bytes로)
대용량 파일 검색
import mmap
import re
def grep_file(path: str, pattern: bytes) -> list[bytes]:
"""파일 전체를 mmap으로 열어 정규식 검색"""
with open(path, "rb") as f:
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
return re.findall(pattern, mm)
# 10GB 로그 파일에서 에러 라인 추출
errors = grep_file("server.log", rb"ERROR.*?\n")
print(f"에러 수: {len(errors)}")
# 바이너리 시그니처 검색
with open("firmware.bin", "rb") as f:
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
pos = mm.find(b"\x7fELF") # ELF 헤더 찾기
if pos != -1:
print(f"ELF 섹션 위치: {pos}")
쓰기 가능 mmap
import mmap
# r+b로 열어야 쓰기 가능
with open("database.bin", "r+b") as f:
with mmap.mmap(f.fileno(), 0) as mm:
# 읽기
header = mm[:8]
# 쓰기 (즉시 메모리에 반영, 나중에 디스크로)
mm[0] = 0x89 # 1바이트 수정
mm[4:8] = b"\x00\x00\x00\x01" # 4바이트 슬라이스 교체
# seek/write 인터페이스
mm.seek(100)
mm.write(b"updated data")
mm.flush() # OS 페이지 캐시 → 디스크 동기화
부분 매핑
import mmap
# 파일의 특정 영역만 매핑 (대형 파일의 일부 처리)
PAGE_SIZE = mmap.PAGESIZE # 일반적으로 4096
with open("huge.bin", "r+b") as f:
# 오프셋은 PAGE_SIZE 배수여야 함
offset = PAGE_SIZE * 100 # 100번째 페이지 = 409600 바이트
length = PAGE_SIZE * 10 # 10 페이지 = 40960 바이트
with mmap.mmap(f.fileno(), length, offset=offset) as mm:
print(mm.size()) # 40960
data = mm[:] # 매핑된 영역 전체
프로세스 간 공유 메모리 (Unix)
import mmap
import os
# Unix: MAP_SHARED + 익명 파일로 IPC
# 부모가 만들고 자식이 공유
mm = mmap.mmap(-1, 4096) # -1 = 익명 메모리
pid = os.fork()
if pid == 0: # 자식
mm[0:5] = b"hello"
os._exit(0)
else: # 부모
os.waitpid(pid, 0)
print(mm[0:5]) # b"hello" — 자식이 쓴 값
mm.close()
언제 mmap을 쓰나
| 적합 | 부적합 |
|---|---|
| 대용량 파일 일부 랜덤 접근 | 순차적으로 한 번만 읽는 경우 |
| 이진 파일 패턴 검색 | 소용량 파일 (<100MB) |
| 파일의 특정 위치 수정 | 텍스트 파일 줄 단위 처리 |
| 프로세스 간 공유 메모리 | 간단한 읽기·쓰기 작업 |
대용량 파일을 순차적으로 처리할 때는 for line in f 스트리밍이 더 단순하다. mmap의 진가는 랜덤 접근과 이진 패턴 검색에서 나타난다.
# 성능 비교 개요
# read() 전체: 메모리 = 파일 크기, 시간 = 복사 포함
# mmap: 메모리 = 접근한 페이지만, 시간 = 복사 없음
# for line: 메모리 = O(한 줄), 시간 = 순차 최적화
지난 글: pathlib로 파일 읽고 쓰기: 현대적인 파일 I/O
다음 글: 줄 끝 문자: \n·\r\n·\r과 크로스 플랫폼 처리
읽어주셔서 감사합니다. 😊

