임시 파일(temporary file)을 생성하면 운영체제(OS)의 임시 폴더 안에 무작위 이름을 가진 빈 파일이 하나 만들어집니다.
그리고 이 파일은 자동으로 삭제됩니다.
그렇다면 Ruby에서는 어떻게 할 수 있을까요?
다음과 같이 하면 됩니다:
require 'tempfile'
Tempfile.create { |f| f << "abc\n" }
여기서 f는 생성된 파일 객체이고, << 연산자를 통해 파일에 데이터를 작성합니다.
이 기능은 Ruby에 기본 내장되어 있기 때문에 별도의 젬(gem)을 설치할 필요가 없습니다.
아주 간단하죠?
그럼에도 불구하고 몇 가지 궁금증이 남을 수 있습니다.
예를 들면:
- 파일은 정확히 언제 삭제될까요?
- 임시 파일에서 다시 내용을 읽을 수 없는 이유는 무엇일까요?
- 임시 파일의 이름은 항상 고유하다고 보장할 수 있을까요?
이 글에서는 위 질문들과 더불어 추가적인 내용까지 다루면서, 임시 파일에 대한 더 깊은 이해를 도와드리겠습니다!
new와 create의 차이점은 무엇일까요?
잘 살펴보면 Tempfile에는 파일을 생성하는 두 가지 메서드가 있다는 것을 알 수 있습니다.
하나는 new이고, 다른 하나는 create입니다.
무슨 차이가 있을까요?
공식 문서에 따르면 new는 예상대로 Tempfile 객체를 생성하고, create는 File 객체를 반환합니다.
사실 이 부분은 크게 중요하지 않습니다. Tempfile이 내부적으로 File에 위임(delegate)하기 때문입니다.
진짜 차이점은 create는 블록을 인자로 받을 수 있지만, new는 그렇지 않다는 것입니다.
직접 확인해 보세요:
Tempfile.new {}
그러면 다음과 같은 유용한 경고 메시지가 출력됩니다:
# warning: Tempfile.new doesn't call the given block.
create와 블록을 함께 사용하면 블록 실행이 끝난 후 임시 파일이 반드시 삭제되도록 보장할 수 있습니다.
그렇다면 이런 의문이 생깁니다…
임시 파일은 언제 삭제될까요?
파일이 삭제되는 시점은 직접 제어할 수 있습니다. 일반 파일처럼 delete 메서드를 호출하거나, create에 블록을 사용하면 됩니다.
자동 삭제가 이루어지는 경우는 다음과 같습니다:
- 프로그램이 종료될 때
- 파일이 "가비지 컬렉션(garbage collection)"될 때, 즉 공간 확보를 위해 알고리즘에 의해 파일이 메모리에서 제거될 때
후자의 경우는 해당 파일에 대한 참조가 전혀 없을 때만 발생합니다.
예를 들어 변수처럼요.
다음 예시를 살펴보세요:
t = Tempfile.new
변수 t가 범위(scope)를 벗어나면 임시 파일이 제거될 수 있습니다.
Linux를 사용하고 있다면, 실시간으로 어떤 파일들이 생성·삭제되는지 관찰할 수 있는 흥미로운 도구인 inotify-tools를 활용해 보세요.
다음 명령어를 실행해 보세요:
inotifywait /tmp -m --format "%w %e %f %T" --timefmt "%H:%m:%S"
그다음 파일을 생성하는 Ruby 코드를 실행합니다.
예시:
ruby -rtempfile -e "def c; t = Tempfile.new('testing'); end; c; sleep 10"
그러면 다음과 같은 결과를 볼 수 있습니다:
/tmp/ CREATE testing20190506-11391-1wqcng0 14:51:48 /tmp/ OPEN testing20190506-11391-1wqcng0 14:51:48 /tmp/ CLOSE_WRITE,CLOSE testing20190506-11391-1wqcng0 14:51:58 /tmp/ DELETE testing20190506-11391-1wqcng0 14:51:58
임시 파일에서 다시 읽을 수 없는 이유는?
임시 파일에 데이터를 쓴 후 다시 읽으려고 하면 빈 문자열이 반환됩니다.
예시:
Tempfile.create { |f| f << "abc\n"; f.read }
# ""
왜 그럴까요?
사실 File은 IO 객체입니다.
IO 객체에는 위치 포인터(position pointer)가 있으며, 파일에 쓰기를 할 때마다 이 포인터가 앞으로 이동합니다.
따라서 내용을 읽고 싶다면 먼저 이 포인터를 되감아야(rewind) 합니다.
다음과 같이 하면 됩니다:
Tempfile.create { |f| f << "abc\n"; f.rewind; f.read }
# "abc\n"
또한 파일 내용을 flush해야 할 필요가 있을 수도 있습니다.
예시:
temp = Tempfile.new temp << "1" temp << "2" temp.flush
임시 파일 이름은 정말 고유할까요?
Tempfile은 고유한 파일명을 생성하고 권한 모드를 600으로 설정합니다. 즉, 파일을 생성한 사용자만 해당 파일을 읽을 수 있습니다.
그런데 파일명이 중복될 가능성은 과연 없을까요?
공식 문서에는 다음과 같이 나와 있습니다:
“Tempfile의 파일명 선택 방식은 스레드 세이프(thread-safe)이자 프로세스 간 세이프(inter-process-safe)입니다. 다른 스레드나 프로세스가 동일한 파일명을 선택하는 일이 없음을 보장합니다.”
또한 new의 설명에는 고유한 파일명을 찾지 못할 경우 에러가 발생할 수 있다고 명시되어 있습니다.
파일에 접두사(prefix)를 설정할 수 있다는 점도 알아두면 유용합니다.
다음과 같습니다:
Tempfile.new("banana-").path
# "/tmp/banana-20190426-25403-1cm7sjt"
이렇게 하면 파일명 충돌 가능성을 더욱 줄일 수 있습니다.
바이너리 모드로 임시 파일 열기
이미지, 음악 등 일반 텍스트가 아닌 데이터를 다룰 때는 파일 모드를 바이너리(binary)로 설정하는 것이 좋습니다.
바이너리 모드는 줄바꿈 문자(line-ending) 변환을 방지해 줍니다.
결과적으로 데이터가 더 "원본 그대로(raw)" 처리되며, 바이너리 파일의 일부가 손상되는 것을 피할 수 있습니다.
바이너리 모드는 기본적으로 활성화되어 있지 않습니다:
temp = Tempfile.new temp.binmode? # false
다음과 같이 활성화할 수 있습니다:
temp = Tempfile.new temp.binmode temp.binmode? # true
create 메서드에서도 마찬가지로 적용됩니다:
Tempfile.create do |f| f.binmode f << "bacon\n" end
마무리
지금까지 Tempfile 클래스를 활용한 Ruby의 임시 파일 처리 방법을 배웠습니다!
이제 여러분이 직접 실습해 볼 차례입니다.
읽어주셔서 감사합니다 🙂