리눅스 명령어

find로 파일 찾아 한 번에 처리하기, -exec와 xargs 실전 정리

find로 파일 찾아 한 번에 처리하기, -exec와 xargs 실전 정리

리눅스 서버를 좀 오래 굴리다 보면 꼭 이런 순간이 옵니다. 디스크가 90%를 넘겼는데 원인을 뒤져 보니 /var/log 아래에 몇 달 치 로그가 수백 개씩 쌓여 있는 겁니다. 하나씩 ls로 보고 rm으로 지우자니 며칠 걸릴 판이고, rm *.log를 치자니 어제 만들어진 로그까지 같이 날아갑니다. “30일보다 오래된 것만” 같은 조건을 걸 방법이 필요한 거죠.

이때 쓰는 게 find입니다. 이름만 보면 파일 검색 도구 같지만, 실제로는 찾은 결과를 그대로 다음 명령에 넘겨서 일괄 처리하는 데 더 많이 씁니다. 오래된 파일 삭제, 권한 일괄 변경, 특정 확장자만 골라 압축 같은 작업이 전부 find 한 줄로 끝납니다. 서버 정리나 배포 스크립트를 만들다 보면 결국 마주치게 되는 명령어입니다.

한눈에 · 조건으로 목록 확인 → -ok나 echo로 예행연습 → -exec 또는 xargs로 실행 → 범위 제한(-maxdepth, -xdev)으로 사고 예방

find는 왜 ‘찾기’ 명령이 아니라 ‘처리’ 명령인가

비슷해 보이는 명령으로 grep이 있는데 역할이 다릅니다. grep은 파일 “안의 내용”을 보고, find는 파일 “자체”의 속성(이름, 크기, 수정 시각, 권한)을 봅니다. 로그 안에 500 에러가 있는 줄을 뽑고 싶으면 grep이고, 30일 지난 로그 파일을 지우고 싶으면 find입니다.

ls로는 왜 안 되냐면, ls는 조건을 걸 수 없어서 그렇습니다. ls -lt로 정렬해서 눈으로 세는 건 파일 10개일 때 얘기고, 하위 디렉터리까지 재귀로 뒤지면서 “30일 이상 지난 것만”을 뽑는 건 find 말고는 마땅한 방법이 없습니다.

이 글을 다 읽고 나면 아래 한 줄이 무슨 뜻인지, 그리고 이걸 안전하게 치려면 앞에 뭘 먼저 해봐야 하는지 알게 됩니다.

find . -name "*.log" -mtime +30 -exec rm {} \;

지금은 {}도 \;도 낯설 겁니다. 순서대로 하나씩 풀어 보겠습니다.

시작 전에: find 기본 문법과 조건 옵션부터

위 한 줄을 이해하려면 앞쪽 조건 부분부터 읽을 줄 알아야 합니다. find는 크게 세 덩어리로 이루어집니다.

find [탐색할 경로] [조건] [동작]

find . -name ".log" -mtime +30 -exec rm {} \;를 이 구조로 끊으면 경로는 .(현재 디렉터리), 조건은 -name ".log" -mtime +30, 동작은 -exec rm {} \;입니다. 동작을 생략하면 기본값인 -print가 붙어서 찾은 경로를 화면에 출력합니다. 그래서 조건만 쓰면 목록만 나옵니다.

이름과 종류로 찾기

가장 많이 쓰는 조건들부터 정리합니다.

옵션의미예시
-name파일명 패턴 (대소문자 구분)-name "*.log"
-iname파일명 패턴 (대소문자 무시)-iname "*.LOG"
-type f일반 파일만-type f
-type d디렉터리만-type d
-type l심볼릭 링크만-type l
-user소유자 기준-user www-data
-perm권한 기준-perm 777

-name 뒤의 패턴은 반드시 따옴표로 감싸세요. 안 감싸면 셸이 *를 먼저 풀어버려서 find한테는 이미 펼쳐진 파일명이 넘어갑니다. 이게 뒤에 나오는 paths must precede expression 에러의 단골 원인입니다.

시간과 크기로 찾기

시간 조건은 -mtime(내용 수정), -atime(접근), -ctime(속성 변경)이 있고, 실무에서는 -mtime을 제일 많이 씁니다. 단위는 하루이고, 부호가 헷갈리는 지점입니다.

표기의미실제 범위
-mtime +3030일보다 오래된 파일31일 전 이상
-mtime 30딱 30일 전 파일30~31일 사이
-mtime -3030일 이내 수정된 파일0~30일

정리 작업에서 쓰는 건 거의 항상 +30 쪽입니다. -를 쓰면 최근 파일이 걸려서 정반대 결과가 나옵니다. 분 단위로 잡고 싶으면 -mmin +60(60분보다 오래됨)처럼 씁니다.

크기는 -size입니다. -size +100M은 100MB 초과, -size -1k는 1KB 미만, -size 0 또는 -empty는 빈 파일입니다. 디스크가 꽉 찼을 때는 디스크 용량 부족, df와 du로 범인 찾아내는 순서로 큰 디렉터리를 먼저 좁힌 다음 find -size로 파고드는 게 빠릅니다.

조건 조합과 괄호 이스케이프

조건을 여러 개 나열하면 기본이 AND입니다. -type f -name "*.log"는 “일반 파일이면서 이름이 .log”입니다. OR은 -o를 씁니다.

# .log 또는 .gz 파일 찾기
find /var/log \( -name "*.log" -o -name "*.gz" \) -type f

괄호를 그냥 쓰면 셸이 문법 오류를 냅니다. 그래서 \(, \)처럼 백슬래시로 이스케이프하거나 '(', ')'처럼 따옴표로 감쌉니다. 괄호 앞뒤로 공백도 반드시 있어야 합니다. 부정은 ! 또는 -not입니다. ! -name "*.gz"는 gz가 아닌 것만 고릅니다.

찾은 파일을 실제로 처리하기: -delete, -exec, xargs

조건이 잡혔으면 이제 뒤에 동작을 붙일 차례입니다. 여기서부터는 파일이 실제로 사라지기 때문에, 명령을 치는 순서가 문법보다 중요합니다. 저는 항상 이 순서를 지킵니다.

삭제 명령을 치는 안전한 순서
삭제 명령을 치는 안전한 순서

-delete: 가장 짧지만 가장 위험한 방법

find에는 삭제 전용 동작인 -delete가 내장돼 있습니다.

find /var/log/old -type f -name "*.log" -mtime +30 -delete

짧고 빠릅니다. 외부 프로세스를 띄우지 않아서 성능도 제일 좋습니다. 문제는 확인 절차가 전혀 없다는 점입니다. 조건을 하나 잘못 쓰면 그대로 다 지워집니다. -delete는 반드시 -delete를 뺀 상태로 먼저 실행해서 목록을 눈으로 본 다음에 붙이세요.

또 하나, -delete는 옵션 위치에 민감합니다. find . -delete -name "*.log"처럼 앞에 쓰면 조건과 무관하게 전부 지우려 듭니다. 항상 맨 뒤에 붙이는 습관을 들이는 게 안전합니다.

-exec {} \; 와 {} + 제대로 구분하기

-exec는 찾은 파일마다 임의의 명령을 실행합니다. {}가 찾은 파일 경로로 치환되고, 명령의 끝을 \; 또는 +로 알려줍니다.

# 파일 하나마다 rm을 한 번씩 실행
find . -name "*.log" -mtime +30 -exec rm {} \;

# 찾은 파일들을 최대한 묶어서 rm을 한 번만 실행
find . -name "*.log" -mtime +30 -exec rm {} +

둘의 차이는 프로세스 실행 횟수입니다. 파일이 1,000개라면 \;는 rm 프로세스를 1,000번 띄우고, +는 인자를 몰아서 한두 번만 띄웁니다. 파일이 많을수록 + 쪽이 압도적으로 빠릅니다.

형태실행 방식언제 쓰나
-exec 명령 {} \;파일 1개당 1회 실행명령 중간에 {}가 들어가야 할 때, 파일별로 다른 처리
-exec 명령 {} +여러 파일을 묶어 1회 실행파일 수가 많고 명령이 여러 인자를 받을 때

+는 {}가 명령의 맨 끝에 있어야만 쓸 수 있습니다. mv {} /backup/처럼 {} 뒤에 뭔가 더 오는 경우는 \;를 써야 합니다.

권한 일괄 정리가 +의 대표적인 활용처입니다. 웹 디렉터리 권한이 엉망일 때 이 두 줄이면 정리됩니다.

find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +

디렉터리는 755, 파일은 644로 나눠 거는 이유가 궁금하면 chmod 755와 644의 실제 차이, 왜 폴더는 755여야 하나를 같이 보시면 됩니다.

삭제 작업을 처음 해본다면 -exec 대신 -ok를 쓰세요. 파일마다 실행 여부를 y/n으로 물어봅니다.

find . -name "*.tmp" -mtime +7 -ok rm {} \;
# < rm ... ./cache/sess_a3f9.tmp > ?
find + exec 한 줄 처리
find + exec 한 줄 처리

xargs 로 넘길 때와 -exec 쓸 때

xargs는 표준 입력으로 들어온 문자열을 명령의 인자로 바꿔주는 도구입니다. find의 출력을 파이프로 넘겨서 처리하는 방식이죠.

find /var/log -type f -name '*.log' -mtime +30 -print0 | xargs -0 rm -f

여기서 -print0과 xargs -0은 세트입니다. 기본 find는 결과를 줄바꿈으로 구분하는데, 파일명에 공백이나 줄바꿈이 들어 있으면 xargs가 그걸 파일 경계로 착각해서 엉뚱하게 쪼갭니다. my report.log가 my와 report.log 두 개로 갈라지는 식입니다. -print0은 구분자를 널 문자(\0)로 바꾸고, xargs -0이 그걸 그대로 읽습니다. 파이프로 넘길 때는 예외 없이 -print0 | xargs -0 조합을 쓰세요.

-exec ... +와 xargs -0은 성능이 거의 비슷합니다. 굳이 나누자면 이렇습니다.

상황권장
단순 처리, 스크립트 안-exec ... {} + (파이프 없어 안전)
병렬 처리 필요xargs -0 -P 4
중간에 grep 등을 끼워야 함xargs

실전에서 자주 쓰는 조합 몇 개를 모아 둡니다.

# 30일 지난 로그 삭제
find /var/log/app -type f -name "*.log" -mtime +30 -delete

# 빈 파일 정리
find /tmp -type f -empty -delete

# 7일 지난 빈 디렉터리 정리
find /tmp -type d -empty -mtime +7 -delete

# 특정 확장자만 골라 tar로 묶기
find . -name "*.conf" -print0 | tar czvf conf-backup.tar.gz --null -T -

# 100MB 넘는 파일 크기순 확인
find /home -type f -size +100M -exec ls -lh {} + | sort -k5 -h

tar 옵션이 헷갈린다면 tar 압축과 해제 옵션 정리, czvf xzvf 순서까지 헷갈리지 않게를 참고하시면 됩니다.

여기서 자주 막힙니다

위 예시를 그대로 쳤는데 에러가 났다면 대개 아래 몇 가지 중 하나입니다. 하나씩 짚어 보겠습니다.

find: missing argument to '-exec' — 세미콜론 이스케이프를 빼먹은 경우입니다. -exec rm {} ;라고 치면 셸이 ;를 명령 구분자로 먹어버려서 find한테 도달하지 않습니다. \; 또는 ';'로 써야 합니다.

find . -name "*.log" -exec rm {} ;     # 에러
find . -name "*.log" -exec rm {} \;    # 정상

find: paths must precede expression: report.log — 경로와 조건 순서가 바뀌었거나, 패턴을 따옴표로 감싸지 않았을 때 나옵니다. 후자가 훨씬 흔합니다. find . -name .log를 치면 셸이 현재 디렉터리의 log 파일을 먼저 펼쳐서 find . -name a.log report.log처럼 넘어가고, find는 두 번째 파일명을 경로로 해석하려다 실패합니다. 따옴표만 붙이면 해결됩니다.

Permission denied가 화면을 도배할 때는 표준 에러를 버릴 수 있습니다.

find / -name "*.conf" 2>/dev/null

다만 이건 권한 에러뿐 아니라 모든 에러를 숨깁니다. 정리 스크립트에서 남발하면 “왜 안 지워졌는지” 단서까지 사라집니다. 원인을 봐야 할 때는 빼고 돌리세요. 권한 문제 자체는 sudo로 실행하거나, 애초에 접근 가능한 경로만 지정하는 게 낫습니다.

가장 위험한 건 탐색 범위입니다. /에서 시작하면 /proc, /sys, 네트워크 마운트까지 전부 뒤집니다. 시간도 오래 걸리고 사고 위험도 커집니다.

# 현재 디렉터리와 바로 아래 한 단계까지만
find . -maxdepth 2 -name "*.log"

# 다른 파일시스템으로 넘어가지 않음 (NFS, 외장 마운트 보호)
find / -xdev -type f -size +500M

-maxdepth는 반드시 다른 조건보다 앞에 써야 경고 없이 동작합니다. 정리 명령은 /가 아니라 항상 구체적인 경로에서 시작하세요. /var/log/nginx처럼요.

실수를 되돌릴 수 없으니, 습관으로 만들 것

여기까지 문법과 에러를 다 넘겼다면 마지막은 안전장치 이야기입니다. rm으로 지운 파일은 휴지통이 없습니다. 다음 순서만 몸에 익혀 두면 사고 확률이 크게 줄어듭니다.

삭제 전 예행연습 3단계 체크리스트

  1. 조건만 실행해 목록을 눈으로 본다. find /var/log/app -type f -name "*.log" -mtime +30 여기까지만 치고, 지워지면 안 될 파일이 섞여 있지 않은지 확인합니다.
  2. 개수를 센다. 뒤에 | wc -l을 붙입니다. 예상이 200개인데 15,000개가 나오면 조건이 잘못된 겁니다. 이 단계에서 대부분의 사고가 걸러집니다.
  3. echo로 예행한다. -exec echo rm {} \;로 실행해 실제로 어떤 명령이 만들어지는지 봅니다. 파일명 공백 문제도 여기서 드러납니다. 처음 하는 작업이면 -ok로 하나씩 확인하며 지우는 것도 방법입니다.

다음에 할 일도 정리해 둡니다.

  • cron에 등록할 때는 모든 경로를 절대 경로로 씁니다. cron은 로그인 셸과 작업 디렉터리가 달라서, find . -delete를 등록하면 엉뚱한 위치를 청소합니다. find /var/log/app ...처럼 명시하고, find도 /usr/bin/find로 적는 게 확실합니다. 등록했는데 안 도는 경우는 crontab 작성법 정리, 등록해도 안 돌아가는 이유까지에 원인별로 정리해 뒀습니다.
  • 파일 내용까지 조건에 넣고 싶으면 find ... -print0 | xargs -0 grep -l "패턴" 조합으로 넘어갑니다. grep 옵션은 grep으로 로그에서 원하는 줄만 뽑기를 보세요.
  • 애플리케이션 로그를 주기적으로 정리하는 목적이라면 find + cron보다 logrotate가 낫습니다. 압축, 보관 개수, 재시작 후크까지 설정 파일 하나로 관리됩니다. find는 logrotate가 커버하지 못하는 임시 파일이나 캐시 정리에 쓰는 쪽이 맞습니다.

댓글 3개

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다