[Java] java.nio.file.Files 사용법 익히기

56238 단어 JavaJava

개요

일을 하다보니 java.nio.file.Files 를 알게 되었다.
꽤나 유용한 거 같아서 이 기회에 간단한 사용법을 익혀보려고 이 글을 썼다.
지금부터 이 패키지에 어떤 기능이 있는지 간단하게 알아보자.

그리고 java.nio.file.Files 로만 하기 힘들거나, 더 심플하게 코딩할 수 있는
다른 API 들도 중간중간 소개하겠다.


참고: 주로 사용하는 패키지 목록. import 시 헷갈리면 여기를 참조하세요!

  • import java.util.stream.Collectors;
  • import java.io.BufferedInputStream;
  • import java.io.BufferedOutputStream;
  • import java.io.BufferedReader;
  • import java.io.BufferedWriter;
  • import java.io.InputStream;
  • import java.io.OutputStream;
  • import java.nio.file.Files;
  • import java.nio.file.Paths;
  • import java.nio.file.attribute.BasicFileAttributes;
  • import java.time.Instant;
  • import java.time.LocalDateTime;
  • import java.time.ZoneId;
  • 시간 관련된 건 다 java.time 패키지 하위에 있는 걸 사용한다.


파일 속성 조회

@Test
void FileFindFilteringTest() throws IOException {
    String filePath = "D:/upload/wow.txt";
    BasicFileAttributes basicFileAttributes 
    	= Files.readAttributes(Paths.get(filePath), BasicFileAttributes.class);
    System.out.println("isRegularFile = " + basicFileAttributes.isRegularFile());
    System.out.println("creationTime = " + basicFileAttributes.creationTime());
    System.out.println("lastModifiedTime = " + basicFileAttributes.lastModifiedTime());
}

출력 결과:

isRegularFile() = true
creationTime() = 2021-12-21T11:03:45.21Z
lastModifiedTime() = 2021-12-21T10:04:22Z




Stream 을 통한 파일 복제

  • Files.newInputStream(Paths.get(realPath))
  • Files.newOutputStream(Paths.get(writePath))
@Test
void testMe() {
    // 그냥 경로를 지정하는 코드다 신경쓰지 말자
    String realPath = "D:/[Java] Read And Write File/123.txt";
    String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;

    // 여기서부터 집중
    try(InputStream is = Files.newInputStream(Paths.get(realPath));
        OutputStream os = Files.newOutputStream(Paths.get(writePath))
    ) {

        // 어떤 java 버전에서도 가능한 정석 코드
        byte[] buffer = new byte[8192];
        int read;
        while ((read = is.read(buffer)) >= 0) {
            os.write(buffer, 0, read);
        }

        // java 9 이상 사용시 Inputstream 의 transferTo(OutputStream out); 사용
        // is.transferTo(os);

        // Spring 을 사용 중이라면 아래 2가지 방식 모두 고려해보자.
        // StreamUtils.copy(is, os); // is, os 를 close 하지 않음, 내부 버퍼 4096 사용
        // FileCopyUtils.copy(is, os); // is, os 를 자동으로 close 함, 내부 버퍼 8192 사용

    } catch (Exception e) {
        e.printStackTrace();
    }
}

위에서 사용한 것은 InputStream, OutputStream 이였다면
이번에는 Reader, Writer로 파일을 제어할 수 있는 방법도 알아보자.

  • Files.newBufferedReader(Paths.get("파일경로"), StandardCharsets.UTF_8);
  • Files.newBufferedWriter(Paths.get("파일경로"), StandardCharsets.UTF_8);

이 방법으로 다시 위의 동일한 파일 복제 기능을 구현하려면 아래처럼 코딩한다.

String realPath = "D:/[Java] Read And Write File/123.txt";
String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;

try(BufferedReader reader = Files.newBufferedReader(Paths.get(realPath), StandardCharsets.UTF_8);
    BufferedWriter writer = Files.newBufferedWriter(Paths.get(writePath), StandardCharsets.UTF_8)
) {

    // 어떤 java 버전에서도 가능한 정석 코드
    char[] buffer = new char[8192];
    int read;
    while ((read = reader.read(buffer)) >= 0) {
        writer.write(buffer, 0, read);
    }

} catch (Exception e) {
    e.printStackTrace();
}

지금까지 Stream 을 사용한 파일 복제 방법을 알아봤다.

그런데 단순 파일 복제는 아래에서 볼 경로를 통한 파일 복제 방법이 더 편하다.
Stream 방식은 HttpServletRequest, HttpServletResponse 와 작업을 할 때 더 유용하다.





경로를 통한 파일 복제

정말 간단하다.

public static void main(String[] args) throws IOException {
    String realPath = "D:/[Java] Read And Write File/123.txt";
    String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;
    Files.copy(Paths.get(realPath), Paths.get(writePath));
}

Files.copy(); 는 오버라이드로 3가지 종류가 있다

  • Files.copy(Path path, OutputStream out)
  • Files.copy(InputStream in, Path path, CopyOption... option)
  • Files.copy(Path path, Path target, CopyOption... option)

대충보면 어떻게 쓰는지 감이 잡히리라 생각한다. 더 이상의 설명은 안하겠다.

참고로 java.nio.file.CopyOption 인자 값으로 몇가지 추가적인 옵션을 줄 수 있다.

다른 블로그에서는 StandardCopyOption.REPLACE_EXISTING 를 흔히 쓰는 걸 봤다. 필요하다면 사용하자.





Directory 복사하기

Files 에는 재귀적으로 Directory 내의 모든 것을 복사하는 기능은 없다.

Directory의 내부 모든 파일을 재귀적으로 복사하고 싶다면
org.springframework.util.FileSystemUtils.copyRecursively(~) 를 사용하자.





파일의 문자열 모두 읽어오기


방법1: Files.newBufferedReader

public static void main(String[] args) {
    String temp = "D:/[Java] Read And Write File/123.txt";

    try(BufferedReader br = Files.newBufferedReader(Paths.get(temp), StandardCharsets.UTF_8)) {
        
        StringBuilder builder = new StringBuilder();
        String str = null;
        while((str = br.readLine()) != null) {
            builder.append(str).append("\n");
        }
        System.out.print(builder.toString());
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}

방법2: Files.lines

public static void main(String[] args) {
    String temp = "D:/[Java] Read And Write File/123.txt";

    try(Stream<String> lines = Files.lines(Paths.get(temp), StandardCharsets.UTF_8)) {
        
        StringBuilder builder = new StringBuilder();
        lines.forEach(s -> builder.append(s).append("\n"));
        System.out.println(builder.toString());
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}





텍스트 파일에 글 쓰기

public static void main(String[] args) throws IOException {
    String temp = "D:/[Java] Read And Write File/123.txt";
    ArrayList<String> messageList = new ArrayList<>(Arrays.asList("hello", "world"));
    
    
    // 방법1: Files.write
    // 두번째 파라미터로 Iterable<? extends CharSequence> iterable 를 받는다.
    Files.write(Paths.get(temp), messageList, StandardCharsets.UTF_8);
    
    // 방법2: Files.writeString
    Files.writeString(Paths.get("C:/upload/jackson/ex1.txt"), 
    				  "안녕하세요", StandardCharsets.UTF_8);
}

위 2가지 방법 모두 기존 텍스트 파일의 내용은 다 지워지고, 새로이 작성되는 점 주의하기 바란다.





directory 내의 파일 조회하기

테스트를 위하여 C:\upload\directory1 경로에 아래와 같은 디렉토리 및 파일 구조를 잡았다.

\---directory1
    +---directory1_depth1
    |   |   directory1_dept1_file1.txt
    |   |   directory1_dept1_file2.txt
    |   |
    |   \---directory1_dept2
    |           directory1_dept2_file1.txt
    |           directory1_dept2_file2.txt
    |
    \---directory2_depth1
            directory2_dept1_file1.txt
            directory2_dept1_file2.txt

이제 directory 모든 파일들을 조회해보자.

@Test
void FilesWalkTest() {
    String directoryPath = "C:/upload/directory1";
    List <Path> list = Collections.emptyList();
    
    try(Stream<Path> walk = Files.walk(Paths.get(directoryPath))) {
    
        list = walk.filter(Files::isRegularFile)
                .collect(Collectors.toList());
                
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    list.forEach(System.out::println);
}

출력은 아래와 같이 나온다.


폴더의 depth 를 지정하고 싶다면 Files.walk(Paths.get(directoryPath), 2);
처럼 maxDepth를 지정할 수도 있다.

그리고 반드시 사용할 때는 try-with-resource를 사용해서 close 가 되도록 해주자.

API Note:
This method must be used within a try-with-resources statement or similar control structure to ensure that the stream's open directories are closed promptly after the stream's operations have completed.

참고:https://stackoverflow.com/questions/54096143/how-to-close-implicit-stream-in-java





특정 확장자의 파일들만 찾아내기

테스트를 위하여 C:\upload\directory1 경로에 아래와 같은 디렉토리 및 파일 구조를 잡았다.

\---directory1
    |   dIZwF9gV0Z.jpg
    |   Hpdf_ghlRA7mJEb.jpg
    |
    +---directory1_depth1
    |   |   directory1_dept1_file1.txt
    |   |   directory1_dept1_file2.txt
    |   |
    |   \---directory1_dept2
    |           directory1_dept2_file1.txt
    |           directory1_dept2_file2.txt
    |
    \---directory2_depth1
            directory2_dept1_file1.txt
            directory2_dept1_file2.txt
            idea64_w4zW6Yf1T3.jpg
            idea64_W8HLV3vgyV.jpg
            idea64_ZbmjlckX4b.jpg

Java 코드는 아래와 같다.

@Test
void FileFindTest() {
    String[] fileExtensions = {"png", "jpg", "gif"};
    String directoryPath = "C:/upload/directory1";
    Path path = Paths.get(directoryPath);
    List<Path> collect = Collections.emptyList();

    if (Files.isDirectory(path)) {

        try(Stream<Path> walk = Files.walk(path)) {
            collect = walk.filter(p -> !Files.isDirectory(p))
                    .filter(f -> Arrays.stream(fileExtensions)
                    		.anyMatch(s -> f.toString().endsWith(s))
                    ).collect(Collectors.toList());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    collect.forEach(System.out::println);
}

출력 결과





특정 기간 동안 수정된 파일들만 찾아내기

위에서는 Files.walk를 사용했지만, 이번에는 Files.find API를 사용해보자.

아래 코드는 어제 자정(= 00:00)부터 현재까지 변경 이력이 있는 파일 목록을 조회하는 코드다.

/*
디렉토리 구조
C:\UPLOAD\DIRECTORY1
|   23day.png  ===> 2021-12-23 에 수정됨
|   dodo.png
|   lala.png
|
+---directory1_depth1
|   |   directory1_dept1_file1.txt ===> 2021-12-22 에 수정됨
|   |   directory1_dept1_file2.txt
|   |
|   \---directory1_dept2
|           directory1_dept2_file1.txt
|           directory1_dept2_file2.txt ===> 2021-12-22 에 수정됨
|
+---directory2_depth1
|       directory2_dept1_file1.txt
|       directory2_dept1_file2.txt
|
\---img
        at_12_17(2).png
        at_12_17.png
        image1.png
*/

public static void main(String[] args) {


    // 필터링할 파일을 갖고 있는 디렉토리
    Path directoryPath = Paths.get("C:/upload/directory1");

    // 앞으로 자주 쓸 DateTimeFormatter 를 선언 및 할당
    DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    ZonedDateTime yesterdayTruncate
        = ZonedDateTime.now(ZoneId.of("Asia/Seoul"))	// 내가 사는 한국 시간 기준으로
        .minusDays(1)					// 하루를 빼고
        .truncatedTo(ChronoUnit.DAYS); 			// 거기에 자정으로 시간을 맞춘다.

    // 참고) 현재 코드를 작성한 시간은 2021-12-23 오후 12 시 29분 
    System.out.println("today : " 
    	+ ZonedDateTime.now(ZoneId.of("Asia/Seoul")).format(dateFormat));
        
    // 2021-12-22 00:00:00 이 출력된다.
    System.out.println("yesterdayTruncate : " 
    	+ yesterdayTruncate.format(dateFormat));

    // Instant를 쓰면 TimeZone을 신경 쓰지 않고 시간 연산을 할 수 있기 때문에 편리하다.
    Instant yesterdayInstant = yesterdayTruncate.toInstant();

    // 필터링 한 파일 목록을 저장할 리스트
    List<Path> result = Collections.emptyList();

    // Files.find의 인자값은 아래와 같다.
    // 첫번째 인자값은 디렉토리명
    // 두번째 파라미터는 디렉토리 내부에서 검색할 때 몇 레벨까지 검색할지 결정
    // 세번째 인자값은 필터링 콜백
    try(Stream<Path> pathStream = Files.find(directoryPath, 
    					     Integer.MAX_VALUE, 
                             		     (path, basicFileAttributes) -> {

        // 폴더는 무시 + 읽을 수 없는 파일도 무시
        if(Files.isDirectory(path) || !Files.isReadable(path)) {
            return false;
        }

        // 최종 수정된 날짜를 조회
        FileTime fileModifedTime = basicFileAttributes.lastModifiedTime();

        // 조회된 날짜에서 Instant 를 뽑아냄
        Instant fileInstant = fileModifedTime.toInstant();

        // 최종적으로 위에서 작성한 yesterdayInstant 와 비교한다.
        // 만약 0 이상이면  2021-12-22 00:00:00 시간을 포함한 그 이후의 시간에
        // 파일이 수정되었다는 것을 의미한다.
        return fileInstant.compareTo(yesterdayInstant) >= 0;

    })) {

        // 결과를 수집한다.
        result = pathStream.collect(Collectors.toList());
    } catch (IOException e) {
        e.printStackTrace();
    }


    // 결과를 조회한다.
    result.forEach(p -> {
        try {

            // 속성 조회
            BasicFileAttributes readAttributes = 
            	Files.readAttributes(p, BasicFileAttributes.class);

            // 파일 수정시간 조회
            FileTime lastModifiedTime = readAttributes.lastModifiedTime();

            // 수정 시간에서 ZonedDateTime을 뽑아냄
            ZonedDateTime zonedDateTime = 
            	lastModifiedTime.toInstant().atZone(ZoneId.of("Asia/Seoul"));

            // 출력
            System.out.println(String.format("%-100s [%s]", 
            			p.toAbsolutePath().toString(),
                        	zonedDateTime.format(dateFormat)));

        } catch (IOException e) {
            e.printStackTrace();
        }

    });
}

Files.findFiles.walk 와 마찬가지로 사용할 때는 try-with-resource를 사용해야 한다.

Files.walkFiles.find API는 굉장히 유사하다.
그러니 자신이 더 편리하다 생각하는 API를 사용하면 될 듯하다.

참고로 위 코드에서 ZoneDateTime 과 Instant 에 대해서 잘 모르겠다면
이 블로그를 참고하면 좋다.

좋은 웹페이지 즐겨찾기