java.io.File에서 java.nio.file로의 이행 (feat. Play, Scala) Scala

Java SE 7에서는 기존의 java.io.File 기반의 File I/O 대신에 java.nio.file.Path 기반의 새로운 API를 사용할 수 있게 되었다. 새 API들은 기존에 비해 좀더 상세한 에러, 강화된 크로스 플랫폼 지원, 심볼릭 링크 지원, 더 나은 메타데이터 지원 등 여러 장점을 가진다. (참고) 하지만 API 구조가 다르기에 이행 작업이 필요하다. 내 경우 플레이 프레임워크 2.6(스칼라 기반)에서 TemporaryFile의 API가 File에서 Path 기반으로 변경되었기에 기존 코드들을 수정할 필요성이 생겨 정리해보았다.

File에서 Path로

기본이 되는 작업은 java.io.File 대신 java.nio.file.Path를 사용하는 것이다.

// 변경 전
import java.io.File

// 변경 후
import java.nio.file.Path

File은 클래스지만, Path는 인터페이스이다. 따라서 직접 생성하는 대신 Paths 등의 도구를 사용해야 한다.

// 변경 전
val file = new File(fullPath)

// 변경 후
val path = Paths.get(fullPath)

파일을 가리킬 뿐 아니라 파일에 대한 다양한 동작을 가지고 있던 File 클래스와 달리, Path 인터페이스는 경로를 가리키는 역할만 수행한다. 기존 File 클래스의 다양한 기능들은 Files 유틸리티 등에 나뉘어져 있다. 이를테면, 경로의 파일 크기를 알아내는 코드가 그렇다.

// 변경 전
val file: File = ...
if (file.length() > maxUploadSize) {

// 변경 후
val path: Path = ...
if (Files.size(path) > maxUploadSize) {

다음 문서에는 위처럼 기존 File 클래스의 특정 기능에 해당되는 java.nio.file 패키지의 요소들이 나열되어 있다.

어떤 경로의 파일을 가리키는 Path 객체로부터 파일명만을 가져오기 위해서는, 오직 파일명만을 가리키는 Path 객체를 생성 후 문자열로 변환하면 된다. 상대경로를 절대경로로 바꿀 때도 비슷하게 새로운 Path 객체를 만들어서 한다. 다음은 기존과의 차이점을 살펴보기 위해 REPL에서 실행한 코드이다.

scala> val file = new File("/home/setzer/myfile")
file: java.io.File = /home/setzer/myfile

scala> file.getName
res0: String = myfile

scala> val path = Paths.get("/home/setzer/myfile")
path: java.nio.file.Path = /home/setzer/myfile

scala> path.toString
res1: String = /home/setzer/myfile

scala> path.getFileName
res2: java.nio.file.Path = myfile

scala> path.getFileName.toString
res3: String = myfile

scala> val file = new File("./myfile")
file: java.io.File = ./myfile

scala> file.getAbsolutePath
res4: String = /home/setzer/./myfile

scala> val path = Paths.get("./myfile")
path: java.nio.file.Path = ./myfile

scala> path.toString
res5: String = ./myfile

scala> path.getFileName.toString
res6: String = myfile

scala> path.toAbsolutePath
res7: java.nio.file.Path = /home/setzer/./myfile

scala> path.toAbsolutePath.toString
res8: String = /home/setzer/./myfile

호환성 등 여러 이유로 java.io.File이 필요해지면 Path.toFile을 활용하면 된다. 역으로 File.toPath 메서드도 존재한다.

// 변경 전
FileUtils.deleteQuietly(file)

// 변경 후
FileUtils.deleteQuietly(path.toFile)

파일의 경로에서 구분자(예컨대 '/home/setzer/myfile' 의 '/')를 가져오고자 한다면 FileSystems 유틸 클래스를 통해 직접 구하면 된다.

// 변경 전. OpenJDK 기준으론 원래도 내부적으로 DefaultFileSystem.getFileSystem.getPathSeparator 이긴 했다.
val separator = File.separator

// 변경 후
val separator = FileSystems.getDefault.getSeparator

임시 파일을 생성하는 메서드는 File에서 Files로 옮겨졌다.

// 변경 전
File.createTempFile(...)

// 변경 후
Files.createTempFiles(...)

위의 기능 매핑 문서에도 나와 있는 내용이긴 하지만 굳이 여기 소개한 이유는, 반환되는 값이 File이 아닌 Path이기 때문에 File.deleteOnExit()로 마킹할 수 없었기 때문이다. 문서에는 이에 대응되는 기능이 파일 생성 때 DELETE_ON_CLOSE 옵션을 주라는 것인데, 위의 방식으로 하면 옵션을 줄 수가 없다.

몇 가지 방법이 있는데 (문서 참고) 아래 방법이 가장 간단하다. 참고로 해당 문서에는 종료시 파일 삭제에 대한 문제점도 다루고 있으니 살펴보길 바란다.

// 변경 전
file.deleteOnExit()

// 변경 후
path.toFile.deleteOnExit()

Commons-IO

편의상 Commons-IO 등 별도 오픈소스 라이브러리에 있던 것들이 구현된 경우도 있다. 그 중 하나는 FileUtils.forceMkdir() 인데, 이 메서드는 경로 문자열로 디렉터리 생성시 상위 디렉터리가 없으면 만드는 것으로, 배시 셸에서의 mkdir -p 명령과 같다. 새로운 API에서는, Files.createDirectories() 로 구현되었다.

// 변경 전
FileUtils.forceMkDir(file)

// 변경 후
Files.createDirectories(path)

FileUtils.deleteDirectory() 는 rm -r 명령처럼 디렉터리의 내용까지 재귀적으로 삭제한다. Files에는 이에 대응하는 메서드가 없지만 별도 라이브러리인 Guava에 같은 기능을 하는 MoreFiles.deleteRecursively() 메서드가 있다. MoreFiles는 Path를 위한 유틸리티 메서드의 모음이다.

// 변경 전
FileUtils.deleteDirectory(file)

// 변경 후
MoreFiles.deleteRecursively(path)

FileUtils.readFileToString() 메서드는 주어진 파일을 특정한 인코딩으로 읽어들여 문자열 변수에 담은 다음, 파일을 닫는 작업까지 수행한다. FileUtils.readLines() 메서드는 파일을 읽어들여 각 행을 담은 컬렉션으로 반환한다. Files를 활용하면 다음처럼 대응이 가능하다.

// 변경 전
val content = FileUtils.readFileToString(file, "UTF-8")
val lines = FileUtils.readLines(file, "UTF-8")

// 변경 후
val content = new String(Files.readAllBytes(path, Charset.forName("UTF-8"))
val lines = Files.readAllBytes(path, Charset.forName("UTF-8"))

보다 스칼라스럽게

여느 자바 API가 그러하듯, NIO API에도 스칼라 래퍼들이 존재한다. 그중 better-files 라이브러리를 활용하면 DSL이나 암묵 변환 등 스칼라의 기능을 활용해 더 편리하게 API를 조작할 수 있다. 이를테면, 위의 FileUtils.readFileToString() 메서드는 다음처럼 이행될 수 있다.

import better.files._

// 암묵 변환으로 Charset.forName("UTF-8")과 같다.
implicit val charset: Charset = "UTF-8" 

// 위에서 선언한 암묵 인자 변수 charset이 사용된다.
val content = File(path).contentAsString

이때 path 변수는 java.nio.file.Path 일 수도 있고 문자열일 수도 있으며, 어느 쪽이든 암묵 변환으로 path.contentAsString 처럼 쓸 수도 있다. 자세한 것은 페이지 참조. 이 메서드는 내부적으로 위에서 java.nio.file.Files를 사용한 예시와 동일하다. 이를 이용하면 쓰기도 간단하다.

// 변경 전
FileUtils.writeStringToFile(file, source, "UTF-8")

// 변경 후
File(file).write(source)(charset = "UTF-8")

파일 혹은 경로 삭제시 IOException을 무시하는 FileUtils.deleteQuietly() 유틸 메서드 또한 인자로 대응해 이행할 수 있다.

// 변경 전
FileUtils.deleteQuietly(file)

// 변경 후
File(file).delete(true) //swallowIOExceptions를 true로 설정한다. 단, deleteQuietly와는 달리 IOException에만 대응한다.

better.files.File 객체는 java.io.File, java.nio.file.Path 및 문자열 뿐 아니라 DSL로도 초기화 가능하며, 메서드 체이닝도 된다. 이를테면 다음과 같은 코드가 가능하다.

import better.files._
val path: Path = ("home" / "setzer" / "myFile").write(source)(charset = "UTF-8").deleteOnExit().path

경로 지정 DSL은 다양한 방식으로 작성 가능한데 자세한 내용은 better-files 홈페이지에서 확인하길 바란다.

Tag :
, ,

Leave Comments