shutil: 고수준 파일 복사·이동·압축

Python shutil 모듈로 파일과 디렉토리를 다루는 법을 설명합니다. copy, copy2, copytree, move, rmtree, make_archive, disk_usage 등 실무 패턴을 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 sys 모듈로 인터프리터와 상호작용하는 방법을 다뤘습니다. 이번에는 shutil(shell utility) 모듈을 살펴봅니다. os 모듈이 기본적인 파일/디렉토리 조작을 담당한다면, shutil은 파일 복사·이동·삭제·압축처럼 실제 쉘 명령어 수준의 고수준 작업을 처리합니다.

shutil 모듈 개요

shutilos.rename(), os.remove() 등으로는 불편한 작업들을 간단하게 처리합니다. 파일시스템 경계를 넘나드는 이동, 디렉토리 트리 통째로 복사, zip/tar 파일 생성 등이 대표적입니다.

import shutil

# 지원 포맷 확인
print(shutil.get_archive_formats())
# [('bztar', ...), ('gztar', ...), ('tar', ...), ('xztar', ...), ('zip', ...)]

shutil 모듈 기능 개요

파일 복사

import shutil

# copy: 파일 내용 + 권한 복사 (타임스탬프는 현재 시각)
shutil.copy("src.txt", "dst.txt")
shutil.copy("src.txt", "backup/")  # 디렉토리를 대상으로 지정 가능

# copy2: 파일 내용 + 권한 + 타임스탬프 모두 보존 (cp -p 같은 효과)
shutil.copy2("src.txt", "dst.txt")

# copyfile: 순수 내용만 복사 (권한·메타 무시)
shutil.copyfile("src.txt", "dst.txt")

# 파일 권한만 복사
shutil.copymode("src.txt", "dst.txt")

# 메타데이터(타임스탬프·권한)만 복사
shutil.copystat("src.txt", "dst.txt")

백업 목적이라면 copy2()를 쓰는 것이 안전합니다. 원본 파일의 수정 시각까지 보존되어 나중에 원본인지 복사본인지 구분하기 쉽습니다.

파일/디렉토리 이동

import shutil

# 파일 이동 (이름 변경 효과도 있음)
shutil.move("old_name.txt", "new_name.txt")

# 다른 디렉토리로 이동
shutil.move("file.txt", "/backup/")

# 다른 파일시스템으로도 이동 (os.rename과 달리)
shutil.move("/tmp/data.txt", "/mnt/external/data.txt")

os.rename()은 같은 파일시스템 내에서만 동작합니다. 마운트 포인트를 넘어야 하는 경우 shutil.move()를 사용합니다. 내부적으로 copy2() + os.remove()를 수행합니다.

디렉토리 트리 복사 — copytree

import shutil

# src_dir 전체를 dst_dir로 복사 (dst_dir이 없어야 함)
shutil.copytree("src_dir", "dst_dir")

# 기존 대상 디렉토리에 병합 (Python 3.8+)
shutil.copytree("src_dir", "existing_dir", dirs_exist_ok=True)

# 특정 패턴 제외
shutil.copytree(
    "project",
    "project_backup",
    ignore=shutil.ignore_patterns("*.pyc", "__pycache__", ".git")
)

dirs_exist_ok=True 옵션이 추가된 것은 Python 3.8입니다. 이전 버전에서는 dst_dir이 이미 존재하면 예외가 발생합니다.

디렉토리 트리 삭제 — rmtree

import shutil

# 비어있지 않은 디렉토리도 통째로 삭제
shutil.rmtree("old_project")

# 삭제 실패 시 처리 (예: 읽기 전용 파일)
def on_error(func, path, exc_info):
    import os, stat
    os.chmod(path, stat.S_IWRITE)
    func(path)

shutil.rmtree("protected_dir", onerror=on_error)

rmtree()는 매우 강력합니다. 실수로 잘못된 경로를 넣으면 돌이킬 수 없습니다. 프로덕션 코드에서는 삭제 전 경로를 검증하는 로직을 반드시 추가합니다.

shutil 코드 패턴

압축 파일 생성과 해제

import shutil

# zip 파일 생성 (backup.zip)
# base_name: 파일명(확장자 제외), format: 'zip'|'tar'|'gztar'|'bztar'|'xztar'
shutil.make_archive(
    "backup",      # 생성할 파일 이름 (backup.zip)
    "zip",         # 포맷
    root_dir=".",  # 아카이브의 루트 디렉토리
    base_dir="src" # 실제로 압축할 대상
)

# 압축 해제 (확장자 보고 포맷 자동 감지)
shutil.unpack_archive("backup.zip", "extracted/")

# tar.gz 생성
shutil.make_archive("backup", "gztar", root_dir=".", base_dir="src")

zipfile, tarfile 모듈보다 단순한 케이스에 훨씬 편리합니다. 세밀한 제어가 필요할 때는 해당 모듈을 직접 씁니다.

디스크 사용량 확인

import shutil

total, used, free = shutil.disk_usage("/")
print(f"Total: {total // 2**30} GB")
print(f"Used:  {used  // 2**30} GB")
print(f"Free:  {free  // 2**30} GB")

which — 실행 파일 경로 찾기

import shutil

python_path = shutil.which("python3")
print(python_path)  # /usr/bin/python3

if shutil.which("git") is None:
    print("git이 설치되지 않았습니다.")

shutil.which()는 쉘의 which 명령과 동일하게 PATH 환경변수를 탐색합니다.

shutil vs os 정리

작업osshutil
파일 이름 변경os.rename() (같은 fs만)shutil.move() (fs 경계 OK)
파일 복사직접 지원 없음shutil.copy2()
디렉토리 복사없음shutil.copytree()
재귀 삭제없음shutil.rmtree()
압축 생성없음shutil.make_archive()

지난 글: sys 모듈: Python 인터프리터와 직접 대화하기

다음 글: subprocess: 외부 프로세스 실행하기


읽어주셔서 감사합니다. 😊