Tempfile
Tempfile (임시 파일)
Tempfile은 임시 파일을 관리하는 유틸리티 클래스예요. 임시 파일을 만드는 방법은 두 가지가 있어요.
Tempfile.create(권장)Tempfile.new와Tempfile.open(주로 하위 호환용, 권장하지 않음)
Tempfile.create는 일반 File 객체를 만들어요. 파일 삭제 시점을 예측할 수 있고, 생성 직후 임시 파일을 지우는 "open-and-unlink" 기법도 지원해요. 반면 Tempfile.new와 Tempfile.open은 Tempfile 객체를 만들어요. 만들어진 파일은 가비지 컬렉터(GC, finalizer)에 의해 지워지는데, 삭제 시점을 예측할 수 없어요.
출처: Ruby 4.0 API
본문
Synopsis
require 'tempfile'
# Tempfile.create with a block
# 파일명은 자동으로 정해져요.
# (선택 인자로 파일명의 prefix와 suffix를 지정할 수 있어요.)
Tempfile.create {|f|
f.puts "foo"
f.rewind
f.read # => "foo\n"
} # 블록 종료 시 파일이 삭제돼요.
# 블록 없이 Tempfile.create
# 블록 없는 형태에서는 파일을 직접 unlink해야 해요.
f = Tempfile.create
f.puts "foo"
f.close
File.unlink(f.path) # 직접 unlink해야 해요.
# 블록 없이 Tempfile.create(anonymous: true)
f = Tempfile.create(anonymous: true)
# anonymous라서 파일이 이미 삭제돼 있어요.
f.path # => "/tmp/" (파일이 없어 파일명도 없어요)
f.puts "foo"
f.rewind
f.read # => "foo\n"
f.close
# 블록과 함께 Tempfile.create(anonymous: true)
Tempfile.create(anonymous: true) {|f|
# anonymous라서 파일이 이미 삭제돼 있어요.
f.path # => "/tmp/" (파일이 없어 파일명도 없어요)
f.puts "foo"
f.rewind
f.read # => "foo\n"
}
# 권장하지 않음: 블록 없이 Tempfile.new
file = Tempfile.new('foo')
file.path # => OS 임시 디렉터리의 고유 파일명,
# 예: "/tmp/foo.24722.0"
# basename 안에 'foo'가 들어 있어요.
file.write("hello world")
file.rewind
file.read # => "hello world"
file.close
file.unlink # 임시 파일을 삭제해요
Tempfile.new와 Tempfile.open에 대해
이 절은 Tempfile 객체를 돌려주지 않는 Tempfile.create에는 적용되지 않아요.
Tempfile 객체를 만들면 고유한 파일명을 가진 임시 파일이 생성돼요. Tempfile 객체는 File 객체처럼 동작하므로, 데이터 읽기·쓰기·권한 변경 같은 일반적인 파일 연산을 모두 할 수 있어요. 그래서 이 클래스가 File의 모든 인스턴스 메서드를 명시적으로 문서화하지 않아도, 실제로는 Tempfile 객체에 File 인스턴스 메서드를 얼마든지 호출할 수 있어요.
Tempfile 객체는 임시 파일을 지우는 finalizer를 가져요. 즉 임시 파일은 GC를 통해 삭제돼요. 이 때문에 몇 가지 문제가 생길 수 있어요.
- 긴 GC 간격과 보수적인(conservative) GC에서는 삭제되지 않고 쌓이는 임시 파일이 생길 수 있어요.
- Ruby가 비정상 종료(SIGKILL, SEGV 등)하면 임시 파일이 삭제되지 않아요.
Tempfile.new와 Tempfile.open에는 레거시 관행이 있어요.
명시적 close
Tempfile 객체가 가비지 컬렉트되거나 Ruby 인터프리터가 종료되면 연결된 임시 파일이 자동으로 삭제돼요. 그래서 사용 후 Tempfile을 명시적으로 지울 필요는 없지만, 지우는 게 좋은 습관이에요. 사용하지 않는 Tempfile을 지우지 않으면 가비지 컬렉트되기 전까지 파일시스템에 임시 파일이 잔뜩 남을 수 있어요. 이 임시 파일들이 있으면 새 Tempfile 파일명을 정하기도 어려워져요.
따라서 항상 ensure 블록 안에서 unlink 또는 close를 호출해야 해요.
file = Tempfile.new('foo')
begin
# ...file로 무언가 하기...
ensure
file.close
file.unlink # 임시 파일을 삭제해요
end
이 용도로 Tempfile.create { … }이 존재하고 더 편리해요. Tempfile.create는 Tempfile 대신 File 인스턴스를 돌려주는데, 이러면 위임(delegation)의 오버헤드와 복잡함도 피할 수 있어요.
Tempfile.create('foo') do |file|
# ...file로 무언가 하기...
end
생성 후 unlink (Unlink after creation)
POSIX 시스템에서는 파일을 만든 직후, 닫기 전에 unlink할 수 있어요. 이렇게 하면 파일 핸들은 닫지 않고 파일시스템 엔트리만 지우므로, 이미 파일 핸들을 열어 둔 프로세스만 파일 내용에 접근할 수 있어요. Tempfile을 다른 프로세스가 읽거나 쓰지 못하게 하고 싶고, 파일명도 알 필요 없을 때는 이 방법을 강력히 권장해요.
또 이 방법은 Ruby가 비정상 종료해도 임시 파일이 삭제되는 것을 보장해요. OS가 파일이 닫히거나 Ruby 프로세스가(정상·비정상 모두) 종료될 때 임시 파일의 저장 공간을 회수하거든요.
예를 들어 RAM에 넣기엔 너무 큰 바이트 버퍼가 필요할 때(웹 서버에서 클라이언트 파일 업로드 데이터를 버퍼링하는 경우처럼) 생성 후 unlink가 실용적으로 쓰여요.
Tempfile.create(anonymous: true)가 이 동작을 지원해요. Windows에서도 동작해요.
참고 사항 (Minor notes)
Tempfile의 파일명 선택 방식은 스레드 안전하고 프로세스 간에도 안전해요. 다른 스레드나 프로세스가 같은 파일명을 고를 일이 없다는 걸 보장해요. 다만 Tempfile 자체는 완전히 스레드 안전하지 않을 수 있어요. 여러 스레드에서 같은 Tempfile 객체에 접근한다면 mutex로 보호해야 해요.
상수 (Constants)
VERSION- 버전.
클래스 메서드 (Public Class Methods)
create(basename="", tmpdir=nil, mode: 0, anonymous: false, **options, &block)
기본 파일시스템에 파일을 만들고, 그 파일을 기반으로 한 새 File 객체를 돌려줘요.
블록도 인자도 없으면 다음 조건의 파일을 만들어 돌려줘요.
- 클래스는
File(Tempfile이 아님). - 디렉터리는 시스템 임시 디렉터리(시스템 의존적).
- 생성 파일명은 그 디렉터리에서 고유.
- 권한은
0600. - 모드는
'w+'(읽기/쓰기 모드, 끝 위치).
임시 파일 삭제 여부는 키워드 인자 anonymous와 블록 유무에 달려 있어요.
f = Tempfile.create # => #<File:/tmp/20220505-9795-17ky6f6>
f.class # => File
f.path # => "/tmp/20220505-9795-17ky6f6"
f.stat.mode.to_s(8) # => "100600"
f.close
File.exist?(f.path) # => true
File.unlink(f.path)
File.exist?(f.path) # => false
Tempfile.create {|f|
f.puts "foo"
f.rewind
f.read # => "foo\n"
f.path # => "/tmp/20240524-380207-oma0ny"
File.exist?(f.path) # => true
} # 블록 종료 시 파일이 삭제돼요.
f = Tempfile.create(anonymous: true)
# anonymous라서 파일이 이미 삭제돼 있어요
f.path # => "/tmp/" (파일이 없어 파일명도 없어요)
f.puts "foo"
f.rewind
f.read # => "foo\n"
f.close
Tempfile.create(anonymous: true) {|f|
# anonymous라서 파일이 이미 삭제돼 있어요
f.path # => "/tmp/" (파일이 없어 파일명도 없어요)
f.puts "foo"
f.rewind
f.read # => "foo\n"
}
인자 basename을 주면 그 값에 따라 달라져요.
- 문자열: 생성 파일명이
basename으로 시작.Tempfile.create('foo') # => #<File:/tmp/foo20220505-9795-1gok8l9> - 두 문자열의 배열
[prefix, suffix]: 생성 파일명이prefix으로 시작하고suffix로 끝나요.Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg>
인자 basename과 tmpdir을 주면 tmpdir 디렉터리에 파일을 만들어요.
Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8>
키워드 인자 mode와 options는 File.open 메서드로 그대로 전달돼요.
mode값은 정수여야 하고,File::Constants에 정의된 상수들의 논리합(OR)으로 표현할 수 있어요.options는 Open Options를 참고하세요.
키워드 인자 anonymous는 파일이 언제 삭제되는지 지정해요.
anonymous=false(기본값) + 블록 없음: 파일이 삭제되지 않아요.anonymous=false(기본값) + 블록 있음: 블록이 끝난 후 파일이 삭제돼요.anonymous=true+ 블록 없음: 반환하기 전에 파일이 삭제돼요.anonymous=true+ 블록 있음: 블록이 호출되기 전에 파일이 삭제돼요.
첫 번째 경우(anonymous=false + 블록 없음)에는 파일이 자동 삭제되지 않아요. 명시적으로 close해야 하고, 원하는 파일명으로 rename하는 데 쓸 수 있어요. 파일이 필요 없으면 명시적으로 지워야 해요.
anonymous가 true일 때 생성된 파일 객체의 File#path 메서드는 끝에 슬래시가 붙은 임시 디렉터리를 돌려줘요.
블록을 주면 위에서 설명한 대로 파일을 만들어 그 파일을 블록에 넘기고, 블록의 값을 돌려줘요. 반환하기 전에 파일 객체는 닫히고 기본 파일은 삭제돼요.
Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists"
구현 참고: anonymous=true는 Windows에서는 FILE_SHARE_DELETE로, Linux에서는 O_TMPFILE로 구현해요.
관련: Tempfile.new.
new(basename="", tmpdir=nil, mode: 0, **options)
기본 파일시스템에 파일을 만들고, 그 파일을 기반으로 한 새 Tempfile 객체를 돌려줘요.
가능하면 Tempfile.create를 쓰는 걸 고려하세요. Tempfile.create는,
Tempfile.new가 수퍼클래스DelegateClass(File)을 호출할 때 발생하는 위임의 성능 비용을 피해요.- 닫고 unlink하기 위해 finalizer에 의존하지 않아요(그건 불안정할 수 있어요).
Tempfile.new는 다음 조건의 파일을 만들어 돌려줘요.
- 클래스는 Tempfile (
Tempfile.create에서처럼 File이 아님). - 디렉터리는 시스템 임시 디렉터리(시스템 의존적).
- 생성 파일명은 그 디렉터리에서 고유.
- 권한은
0600. - 모드는
'w+'.
기본 파일은 Tempfile 객체가 죽어 가비지 컬렉터에 회수될 때 삭제돼요.
f = Tempfile.new # => #<Tempfile:/tmp/20220505-17839-1s0kt30>
f.class # => Tempfile
f.path # => "/tmp/20220505-17839-1s0kt30"
f.stat.mode.to_s(8) # => "100600"
File.exist?(f.path) # => true
File.unlink(f.path) #
File.exist?(f.path) # => false
basename 인자와 tmpdir, mode, options의 동작은 Tempfile.create와 같아요.
Tempfile.new('foo') # => #<Tempfile:/tmp/foo20220505-17839-1whk2f>
Tempfile.new(%w/foo .jpg/) # => #<Tempfile:/tmp/foo20220505-17839-58xtfi.jpg>
Tempfile.new('foo', '.') # => #<Tempfile:./foo20220505-17839-xfstr8>
관련: Tempfile.create.
open(*args, **kw) { |tempfile| ... }
새 Tempfile을 만들어요. 이 메서드는 권장하지 않으며 주로 하위 호환용이에요. Tempfile.create를 쓰세요. Tempfile.create는 위임 비용을 피하고, finalizer에 의존하지 않으며, 블록을 주면 파일도 unlink해요.
Tempfile.open은 finalizer에 의해 unlink되기를 원하고 프로그램에서 어느 지점에 Tempfile을 안전하게 unlink할 수 있는지 확실히 알 수 없을 때 여전히 적합할 수 있어요.
블록이 없으면 Tempfile.new와 동의어예요.
블록을 주면 Tempfile 객체를 만들고 그 객체를 인자로 블록을 실행해요. 블록이 끝난 후 Tempfile 객체는 자동으로 닫혀요. 하지만 파일은 unlink되지 않으며 Tempfile#close! 또는 Tempfile#unlink로 직접 unlink해야 해요. finalizer가 unlink를 시도하긴 하지만, 의도한 것보다 훨씬 오래 파일을 디스크에 남길 수 있으므로 의존하면 안 돼요. 예를 들어 CRuby에서는 보수적 스택 스캔과 사용하지 않는 메모리에 남은 참조 때문에 finalizer가 지연될 수 있어요.
호출은 블록의 값을 돌려줘요.
어느 경우든 모든 인자(*args)는 Tempfile.new에 전달돼요.
Tempfile.open('foo', '/home/temp') do |f|
# ... f로 무언가 하기 ...
end
# 동일:
f = Tempfile.open('foo', '/home/temp')
begin
# ... f로 무언가 하기 ...
ensure
f.close
end
인스턴스 메서드 (Public Instance Methods)
close(unlink_now=false)
파일을 닫아요. unlink_now가 true면 파일을 닫은 뒤 unlink(삭제)해요. 지금 unlink하지 않았다면 나중에 unlink를 호출할 수도 있어요. 명시적으로 unlink하지 않으면 삭제가 finalizer로 지연돼요.
close!()
파일을 닫고 unlink(삭제)해요. close(true)를 호출한 것과 같은 효과예요.
delete()
unlink의 별칭이에요.
length()
size의 별칭이에요.
open()
파일을 모드 "r+"로 열거나 다시 열어요.
path()
임시 파일의 전체 경로명을 돌려줘요. unlink가 호출됐다면 nil이에요.
size()
임시 파일의 크기를 돌려줘요. 부수 효과로, 크기를 정하기 전에 IO 버퍼를 flush해요. length로도 별칭돼 있어요.
unlink()
파일을 파일시스템에서 unlink(삭제)해요. 사용 후에는 항상 unlink해야 하는데, 이는 Tempfile 개요의 "Explicit close" 습관 절에서 설명했어요.
file = Tempfile.new('foo')
begin
# ...file로 무언가 하기...
ensure
file.close
file.unlink # 임시 파일을 삭제해요
end
Unlink-before-close: POSIX 시스템에서는 파일을 닫기 전에 unlink할 수 있어요. 이 관행은 Tempfile 개요의 "Unlink after creation" 절에 자세히 설명돼 있어요. 다만 unlink-before-close는 비-POSIX 운영체제에서 지원되지 않을 수 있어요. 가장 대표적인 게 Microsoft Windows인데, 닫히지 않은 파일을 unlink하면 오류가 나고 이 메서드는 그 오류를 조용히 무시해요. 가능할 때마다 unlink-before-close를 쓰고 싶다면 이렇게 코드를 작성해야 해요.
file = Tempfile.new('foo')
file.unlink # Windows에서는 조용히 실패해요.
begin
# ... file로 무언가 하기 ...
ensure
file.close! # 파일 핸들을 닫아요. #unlink가 실패해서
# 파일이 unlink되지 않았다면 이 메서드가 다시 시도해요.
end
delete로도 별칭돼 있어요.