mktemp로 안전하게 임시 파일 만들기
쉘 스크립트를 쓰다 보면 잠깐 저장할 파일이 필요할 때가 많습니다. API 응답을 받아서 가공하거나, 여러 명령의 중간 결과를 합치거나, 압축하기 전에 작업 디렉토리를 하나 만들어두는 경우가 그렇죠.
처음에는 대충 /tmp/result.txt 같은 이름을 쓰기 쉽습니다.
조금 더 신경 쓰면 프로세스 ID를 붙여서 /tmp/myapp.$$처럼 만들기도 하고요.
혼자 쓰는 노트북에서는 별문제가 없어 보이지만, 이런 방식은 스크립트가 서버나 CI에서 돌기 시작하면 꽤 위험해집니다.
파일 이름이 예측 가능하고, 이미 같은 이름의 파일이 있을 수 있고, 공격자가 심볼릭 링크를 먼저 만들어둘 수도 있기 때문입니다.
이럴 때 쓰라고 있는 명령어가 mktemp입니다.
mktemp는 안전한 임시 파일이나 임시 디렉토리를 만들고, 그 경로를 출력해줍니다.
이번 글에서는 mktemp의 기본 사용법부터 trap을 이용한 정리 패턴, macOS와 Linux에서 헷갈리기 쉬운 옵션 차이까지 살펴보겠습니다.
임시 파일 이름을 직접 만들면 왜 위험할까?
가장 흔한 안 좋은 예부터 보겠습니다.
tmp="/tmp/myapp.$$"
echo "작업 중..." > "$tmp"
$$는 현재 쉘의 프로세스 ID입니다.
언뜻 보면 매번 다른 숫자가 붙으니 안전해 보이지만, 프로세스 ID는 충분히 예측 가능합니다.
그리고 이 코드는 “이 이름이 비어 있는지 확인하고 파일을 만든다”는 과정을 안전하게 묶어주지 않습니다.
더 나쁜 예는 이런 식입니다.
tmp="/tmp/myapp-output"
some_command > "$tmp"
여러 사용자가 같은 서버에서 이 스크립트를 실행하면 서로의 파일을 덮어쓸 수 있습니다.
공격자가 미리 /tmp/myapp-output을 다른 파일을 가리키는 심볼릭 링크로 만들어두면, 스크립트가 의도하지 않은 파일에 쓰기를 시도할 수도 있고요.
임시 파일의 핵심은 단순히 “이름이 조금 랜덤하다”가 아닙니다.
아직 쓰지 않은 이름을 고르고, 그 이름으로 파일을 실제로 만들고, 다른 프로세스가 중간에 끼어들 틈을 줄이는 게 중요합니다.
mktemp는 이 과정을 대신 처리해줍니다.
mktemp 기본 사용법
mktemp는 템플릿을 받아서 X 부분을 임의 문자열로 바꾸고, 실제 파일을 만든 뒤 그 경로를 출력합니다.
tmpfile=$(mktemp tmp.XXXXXX)
echo "$tmpfile"
tmp.BdoLJF
여기서 tmp.XXXXXX가 템플릿입니다.
X가 있는 자리에 충돌 가능성이 낮은 문자열이 들어가고, mktemp는 그 이름의 빈 파일을 만듭니다.
출력된 경로는 변수에 담아서 다음 명령에서 쓰면 됩니다.
tmpfile=$(mktemp tmp.XXXXXX) || exit 1
echo "hello" > "$tmpfile"
cat "$tmpfile"
rm -f "$tmpfile"
템플릿에는 X가 충분히 들어가야 합니다.
GNU Coreutils 문서에서는 마지막 경로 요소에 최소 3개의 연속된 X가 필요하다고 설명합니다.
macOS의 mktemp도 템플릿 끝에 붙은 X들을 치환하는 방식이라, 이식성을 생각하면 XXXXXX처럼 6개 이상을 파일명 끝에 두는 습관이 좋습니다.
mktemp myapp.XXXXXX # 좋음
mktemp myapp.XXX # 가능하지만 후보가 적음
mktemp myapp.tmp # X가 없어서 실패
mktemp가 만든 파일은 보통 현재 사용자만 읽고 쓸 수 있는 권한으로 생성됩니다.
macOS에서 확인하면 이런 식입니다.
tmpfile=$(mktemp tmp.XXXXXX)
stat -f "%Sp %N" "$tmpfile"
rm -f "$tmpfile"
-rw------- tmp.BdoLJF
권한이 -rw-------이므로 다른 사용자가 읽거나 쓸 수 없습니다.
임시 파일을 직접 touch하거나 리다이렉션으로 만들 때보다 안전한 출발점이죠.
어디에 만들 것인가?
템플릿에 디렉토리를 포함하지 않으면 현재 디렉토리에 파일이 만들어집니다.
위 예제에서 tmp.BdoLJF가 프로젝트 디렉토리에 생긴 이유가 바로 그것입니다.
보통 임시 파일은 시스템의 임시 디렉토리에 두는 편이 자연스럽습니다.
Linux에서는 /tmp를 많이 떠올리지만, macOS에서는 사용자별 임시 디렉토리가 TMPDIR 환경 변수에 들어 있는 경우가 많습니다.
echo "$TMPDIR"
/var/folders/z6/j9z2rfbx3x9975_0fcpxphvc0000gn/T/
그래서 스크립트에서는 다음 패턴을 자주 씁니다.
tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
${TMPDIR:-/tmp}는 TMPDIR이 있으면 그 값을 쓰고, 없으면 /tmp를 쓰겠다는 뜻입니다.
이 문법이 낯설다면 Bash 파라미터 확장에서 더 자세히 다뤘습니다.
/tmp 디렉토리의 역할과 sticky bit가 궁금하다면 Linux 디렉토리 구조의 /tmp 설명도 함께 보면 좋습니다.
여기서 따옴표는 꼭 붙이는 편이 좋습니다. 임시 디렉토리 경로에 공백이 들어갈 가능성은 낮지만, 쉘 스크립트에서는 경로 변수를 습관적으로 감싸는 쪽이 안전합니다.
임시 파일보다 임시 디렉토리가 편할 때
파일 하나만 필요하다면 mktemp 기본 형태로 충분합니다.
하지만 실제 스크립트에서는 중간 파일이 여러 개 생기는 경우가 더 많습니다.
이럴 때는 임시 파일을 여러 개 만드는 대신, 임시 디렉토리 하나를 만들고 그 안에 필요한 파일을 두는 방식이 편합니다.
임시 디렉토리는 -d 옵션으로 만듭니다.
tmpdir=$(mktemp -d)
echo "$tmpdir"
/var/folders/z6/j9z2rfbx3x9975_0fcpxphvc0000gn/T/tmp.0CTbj88EYH
디렉토리 권한도 현재 사용자만 접근할 수 있도록 만들어집니다.
stat -f "%Sp %N" "$tmpdir"
drwx------ /var/folders/z6/j9z2rfbx3x9975_0fcpxphvc0000gn/T/tmp.0CTbj88EYH
실전에서는 이렇게 씁니다.
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
html="$tmpdir/page.html"
json="$tmpdir/result.json"
log="$tmpdir/run.log"
이 방식의 장점은 파일명을 사람이 읽기 쉽게 정할 수 있다는 점입니다. 확장자가 꼭 필요한 도구도 편하게 다룰 수 있고, 마지막에 디렉토리 하나만 지우면 중간 파일을 한꺼번에 정리할 수 있습니다.
rm -rf "$tmpdir"
특히 여러 파일을 생성하는 스크립트라면 “임시 디렉토리 하나 만들고, 그 안에서 마음껏 작업한 뒤, 마지막에 통째로 지운다”는 패턴이 가장 다루기 쉽습니다.
trap으로 자동 정리하기
임시 파일을 만들었다면 정리도 해야 합니다.
스크립트 마지막에 rm을 써두면 될 것 같지만, 중간에 오류가 나거나 사용자가 Ctrl+C로 끊으면 마지막 줄까지 도달하지 못할 수 있습니다.
macOS도 /tmp에 오래 남은 항목을 정리하는 시스템 작업을 가지고 있습니다.
제 환경에서는 /usr/libexec/tmp_cleaner가 매일 실행되도록 등록돼 있고, 내부 기본값으로 /tmp의 3일 지난 항목을 정리하는 설정이 보입니다.
하지만 이건 mktemp가 보장하는 동작이 아니라 운영체제의 청소 정책입니다.
노트북이 꺼져 있거나, 항목의 접근/수정 시간이 갱신됐거나, 사용자별 TMPDIR 아래의 경로를 쓰는 경우에는 기대한 시점에 지워진다고 단정하기 어렵습니다.
그래서 스크립트가 만든 임시 파일은 스크립트가 직접 치운다고 생각하는 편이 안전합니다.
이럴 때는 trap을 사용합니다.
trap은 쉘이 종료될 때나 특정 신호를 받을 때 실행할 명령을 등록해두는 기능입니다.
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
echo "작업 디렉토리: $tmpdir"
curl -fsSL "https://example.com" -o "$tmpdir/page.html"
grep -o "<title>[^<]*</title>" "$tmpdir/page.html" > "$tmpdir/title.txt"
cat "$tmpdir/title.txt"
EXIT에 등록한 cleanup 함수는 스크립트가 정상 종료되든, set -e 때문에 중간에 멈추든 실행됩니다.
덕분에 임시 디렉토리가 남을 가능성이 줄어듭니다.
rm -rf "$tmpdir"에서 따옴표를 빼지 않는 것도 중요합니다.
경로에 공백이 들어가도 한 덩어리로 처리해야 하고, 변수가 비어 있거나 예상과 다를 때 피해를 키우지 않도록 조심해야 합니다.
더 방어적으로 쓰고 싶다면 정리 함수에서 값이 비어 있지 않은지 확인할 수도 있습니다.
cleanup() {
if [ -n "${tmpdir:-}" ] && [ -d "$tmpdir" ]; then
rm -rf "$tmpdir"
fi
}
간단한 스크립트에서는 앞의 짧은 형태로 충분하지만, 배포 스크립트나 CI에서 돌아가는 스크립트라면 이런 확인을 넣어두는 편이 마음이 편합니다.
-u 옵션은 피하자
mktemp에는 -u 옵션이 있습니다.
GNU 문서에서는 --dry-run, macOS 매뉴얼에서는 unsafe mode라고 설명합니다.
이 옵션은 임시 이름만 출력하고 파일이나 디렉토리를 실제로 만들지 않습니다.
mktemp -u tmp.XXXXXX
tmp.xynxmE
이름만 필요해 보일 때는 편해 보입니다.
하지만 -u를 쓰면 mktemp의 가장 중요한 장점이 사라집니다.
이름을 받은 뒤 실제 파일을 만들기 전까지 다른 프로세스가 같은 이름을 선점할 수 있기 때문입니다.
다음 코드는 피하는 편이 좋습니다.
tmpfile=$(mktemp -u "${TMPDIR:-/tmp}/myapp.XXXXXX")
some_command > "$tmpfile"
이 패턴은 결국 “랜덤해 보이는 파일 이름을 직접 만든 뒤 나중에 사용”하는 방식으로 돌아갑니다.
임시 파일이 필요하면 mktemp가 파일을 만들게 두고, 임시 경로 아래에 여러 파일명이 필요하면 mktemp -d로 디렉토리를 먼저 만드는 편이 안전합니다.
실패는 바로 드러나게 하자
mktemp도 실패할 수 있습니다.
템플릿이 잘못됐거나, 대상 디렉토리가 없거나, 권한이 부족하면 임시 파일을 만들 수 없습니다.
이때 실패를 무시한 채 다음 줄로 넘어가면 빈 변수에 리다이렉션을 하거나, 예상하지 못한 위치에 파일을 쓰는 문제가 생길 수 있습니다.
그래서 스크립트에서는 mktemp 결과를 받을 때 바로 실패 처리를 붙이는 편이 좋습니다.
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || {
echo "임시 디렉토리를 만들 수 없습니다." >&2
exit 1
}
set -e를 켜두면 실패 시 스크립트가 멈추는 경우가 많지만, 이렇게 명시적으로 써두면 오류 메시지가 더 분명해집니다.
특히 trap으로 정리 함수를 등록하기 전에 tmpdir을 만들지 못한 상황인지, 이후 작업 중에 실패한 상황인지 구분하기 쉽습니다.
-q 또는 --quiet 옵션은 오류 메시지를 숨깁니다.
사용자에게 직접 더 친절한 메시지를 보여주려는 경우에는 쓸 수 있지만, 습관적으로 붙이는 옵션은 아닙니다.
왜 실패했는지 알아야 디렉토리 권한 문제인지, 템플릿 문제인지, 디스크 공간 문제인지 빨리 찾을 수 있으니까요.
또 하나 주의할 점은 mktemp가 출력한 경로를 항상 변수에 담고, 그 변수를 쓸 때는 따옴표로 감싸는 것입니다.
tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
some_command > "$tmpfile"
another_command "$tmpfile"
쉘 스크립트의 많은 문제는 “대부분의 환경에서는 우연히 잘 됐지만, 경로에 공백이 있거나 값이 비어 있을 때 깨지는” 형태로 나타납니다. 임시 파일은 스크립트의 중간 지점에 놓이는 경우가 많아서, 여기서 경로 처리를 단단히 해두면 뒤쪽 명령도 훨씬 예측 가능해집니다.
확장자가 필요하면 어떻게 할까?
어떤 도구는 파일 확장자를 보고 동작을 바꿉니다.
예를 들어 .json, .png, .sql 같은 확장자가 꼭 필요한 경우가 있죠.
GNU mktemp에는 --suffix 옵션이 있습니다.
mktemp --suffix=.json "${TMPDIR:-/tmp}/myapp.XXXXXX"
하지만 이 옵션까지 믿고 스크립트를 작성하면 macOS나 다른 BSD 계열 환경에서 걸릴 수 있습니다. 공개용 스크립트나 팀에서 같이 쓰는 스크립트라면 임시 디렉토리를 만든 뒤 그 안에 원하는 파일명을 쓰는 방식이 더 이식성이 좋습니다.
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
trap 'rm -rf "$tmpdir"' EXIT
json="$tmpdir/result.json"
sqlite="$tmpdir/cache.sqlite"
curl -fsSL "https://api.example.com/data" -o "$json"
sqlite3 "$sqlite" < schema.sql
이렇게 하면 확장자도 원하는 대로 쓸 수 있고, 파일명 충돌도 걱정하지 않아도 됩니다. 임시 디렉토리 자체가 이미 안전하게 만들어졌기 때문입니다.
macOS와 Linux에서 헷갈리는 부분
mktemp는 macOS와 Linux 모두에서 흔히 쓸 수 있지만, 세부 옵션은 구현마다 조금씩 다릅니다.
Linux에서는 보통 GNU Coreutils의 mktemp를 만나고, macOS에서는 BSD 계열 mktemp를 만납니다.
가장 헷갈리는 옵션은 -t입니다.
macOS에서는 다음처럼 prefix를 넘겨 사용자 임시 디렉토리 아래에 파일을 만들 수 있습니다.
mktemp -t myapp
/var/folders/z6/j9z2rfbx3x9975_0fcpxphvc0000gn/T/myapp.gh3jFgizBF
하지만 GNU와 BSD에서 -t의 세부 동작은 완전히 같다고 기대하지 않는 편이 좋습니다.
또 GNU에는 --suffix 같은 편리한 긴 옵션이 있지만, 모든 환경에서 같은 옵션을 지원한다고 볼 수는 없습니다.
그래서 이식성을 우선하는 스크립트에서는 다음처럼 명시적인 템플릿을 쓰는 것을 권합니다.
tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || exit 1
이 패턴은 읽기 쉽고, macOS와 GNU/Linux 모두에서 의도가 분명합니다.
TMPDIR이 있으면 사용자별 임시 디렉토리를 존중하고, 없으면 /tmp로 돌아갑니다.
실전용 템플릿
마지막으로 복사해서 시작하기 좋은 형태를 하나 정리해보겠습니다. 파일 하나만 필요하다면 이렇게 씁니다.
#!/usr/bin/env bash
set -euo pipefail
tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXX") || {
echo "임시 파일을 만들 수 없습니다." >&2
exit 1
}
trap 'rm -f "$tmpfile"' EXIT
some_command > "$tmpfile"
another_command "$tmpfile"
중간 파일이 둘 이상 필요하다면 임시 디렉토리 버전으로 시작하는 편이 좋습니다.
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX") || {
echo "임시 디렉토리를 만들 수 없습니다." >&2
exit 1
}
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
input="$tmpdir/input.txt"
output="$tmpdir/output.txt"
log="$tmpdir/run.log"
some_command > "$input" 2> "$log"
another_command "$input" > "$output"
cat "$output"
여기서 2> "$log"처럼 표준 오류를 별도 파일로 보내는 문법이 낯설다면 쉘 리다이렉션 사용법을 먼저 읽어보시면 좋습니다.
mktemp는 임시 경로를 안전하게 만드는 도구이고, 리다이렉션은 그 경로로 입력과 출력을 보내는 도구입니다.
둘을 같이 쓰면 쉘 스크립트의 중간 결과를 훨씬 안정적으로 다룰 수 있습니다.
기억할 것들
mktemp를 쓸 때는 몇 가지만 지켜도 실수를 크게 줄일 수 있습니다.
- 임시 파일 이름을 직접 조합하지 말고
mktemp가 실제 파일을 만들게 합니다. - 템플릿은
myapp.XXXXXX처럼 충분한X를 파일명 끝에 둡니다. - 여러 파일이 필요하면 파일 여러 개보다
mktemp -d로 임시 디렉토리를 먼저 만듭니다. - 만든 경로는 변수에 담고, 사용할 때는 항상 따옴표로 감쌉니다.
- 정리는
trap에 맡기고,-u는 특별한 이유가 없으면 쓰지 않습니다.
이 규칙들은 짧은 개인 스크립트에서는 조금 번거롭게 느껴질 수 있습니다. 하지만 스크립트가 CI, 배포 서버, 여러 사람이 함께 쓰는 개발 환경으로 옮겨가면 이런 기본기가 그대로 안정성 차이로 이어집니다.
마치며
mktemp는 작지만 쉘 스크립트의 안전성을 크게 올려주는 명령어입니다.
임시 파일 이름을 직접 만들지 않고, mktemp가 실제 파일이나 디렉토리를 만들게 하는 것이 핵심입니다.
파일 하나면 mktemp, 여러 중간 파일이 필요하면 mktemp -d, 정리는 trap으로 묶는 패턴만 익혀도 대부분의 상황을 깔끔하게 처리할 수 있습니다.
특히 팀에서 쓰는 스크립트라면 -u처럼 이름만 만드는 옵션은 피하고, ${TMPDIR:-/tmp}/name.XXXXXX 형태의 명시적인 템플릿을 쓰는 편이 좋습니다.
macOS와 Linux 차이를 줄이면서도 사용자의 임시 디렉토리 설정을 존중할 수 있기 때문입니다.
더 자세한 옵션은 GNU Coreutils mktemp 문서를 참고하세요.
macOS에서는 man mktemp를 함께 확인하면 BSD 계열 구현의 -t, -p, -u 동작을 바로 비교해볼 수 있습니다.
This work is licensed under
CC BY 4.0