[Java] 26. 파일(IO)를 다루는 방법(파일의 작성, 수정, 사용 날짜 변경과 close를 하는 이유, Closable 사용법)


Study/Java  2020. 5. 29. 18:51

안녕하세요. 명월입니다.


이 글은 Java에 파일(IO)를 다루는 방법(파일의 작성, 수정, 사용 날짜 변경과 close를 하는 이유, Closable 사용법)에 대한 글입니다.


I/O의 뜻은 Input/Output으로 입력 장치와 출력 장치를 뜻하는 말입니다. 입력 장치란, 키보드나 마우스등이고 출력 장치란 모니터나 프린트등을 뜻합니다.

그러나 프로그램에서의 I/O는 파일 다루는 것을 뜻합니다.

프로그램에서의 가장 많이 사용되는 리소스라고 하면 파일과 소켓(통신)이 있습니다.


우리가 사용하는 PC나 서버는 메모리의 한계가 있기 때문에 모든 데이터를 전부 변수를 선언해서 메모리에 둘 수 없습니다.

또 이 메모리는 시스템을 재부팅하거나 프로그램을 재기동을 하게 되면 데이터가 사라지기 때문에 데이터를 보존하는 역할로 파일을 다루게 됩니다.

파일은 바이너리로 구성되어 있기 때문에 IO를 사용할 때의 자료형은 대부분 byte(unsigned char)로 처리됩니다.


먼저 String으로 된 데이터를 파일로 저장해보겠습니다.

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // 파일로 저장할 test 변수 선언
      String test = "hello world";
      // String 타입을 byte 형식으로 변환
      byte[] binary = test.getBytes();
      // 파일 인스턴스 생성
      File file = new File("d:\\work\\test.txt");
      // Stream 인스턴스 생성
      OutputStream stream = new FileOutputStream(file);
      // OutputStream에 test의 바이너리를 작성
      stream.write(binary);
      // Stream 리소스 닫기
      stream.close();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 예제를 확인하면 제가 d드라이브의 work 디렉토리의 test.txt 파일에 hello world의 String 문자열을 작성했습니다.

여기서 File 클래스와 OutputStream를 상속 받은 FileOutputStream를 사용했습니다.

File클래스는 파일에 대한 정보가 있는 클래스입니다. 파일의 내용보다는 파일이 작성된 시간, 파일 크기등의 정보가 있습니다. Java에서는 byte 배열로 데이터를 선언을 했지만 그 각자의 데이터들은 힙으로 저장이 되기 때문에 데이터가 일렬로 있지 않습니다.

즉, 그 데이터를 일렬로 나열하는 개념이 필요한데 그것이 Stream입니다. 즉, byte[]의 데이터를 하나의 데이터로 나열하고 파일에 작성하는 것입니다.

IO는 프로그램의 메모리에 상주하는 데이터가 아니라 하드 디스크에서 사용하는 다른 리소스이기 때문에 반드시 close를 해야합니다. 이 내용은 아래에서 자세히 설명하겠습니다.


이제 파일을 작성했으니 이번에는 작성한 파일을 읽어서 콘솔에 출력하겠습니다.

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class Example {
  // 날짜 포멧
  private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
  public static void main(String[] args) {
    try {
      // 파일 인스턴스 생성
      File file = new File("d:\\work\\test.txt");
      // 파일이 마지막으로 작성된 날짜 출력
      System.out.println("LastModified - " + format.format(new Date(file.lastModified())));
      // Calendar 인스턴스 가져오기
      Calendar c = Calendar.getInstance();
      // 2000년 1월 1일 12시로 설정
      c.set(2000, 01, 01, 12, 00);
      // 파일의 작성 시간 변경
      Files.setAttribute(Paths.get(file.getAbsolutePath()), "basic:creationTime", FileTime.fromMillis(c.getTimeInMillis()), java.nio.file.LinkOption.NOFOLLOW_LINKS);
      // 파일의 마지막 작성 시간 변경
      file.setLastModified(c.getTimeInMillis());
      // 파일의 마지막 접근 시간 변경
      Files.setAttribute(Paths.get(file.getAbsolutePath()), "basic:lastAccessTime", FileTime.fromMillis(c.getTimeInMillis()), java.nio.file.LinkOption.NOFOLLOW_LINKS);
      // 파일 크기로 binary를 선언
      byte[] binary = new byte[(int)file.length()];
      // Stream 인스턴스 생성
      InputStream stream = new FileInputStream(file);
      // 파일을 읽어오기
      stream.read(binary);
      // Stream 리소스 닫기
      stream.close();
      // 바이너리를 String으로 변환하고 콘솔 출력
      System.out.println(new String(binary));
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

이번에는 Inputstream으로 파일을 읽어와서 콘솔에 출력했습니다.

그리고 File 인스턴스를 이용해서 파일의 정보도 수정했습니다.

작성 일자, 마지막 수정 날짜, 마지막 접근 날짜가 2000년으로 바뀌었습니다.

File 인스턴스에서는 기본적으로 마지막 작성시간은 수정이 가능합니다만 작성 시간과 마지막 접근 시간은 Files의 클래스를 이용해서 속성을 변경해야 합니다.

(참고, TV에서 여러 사건들의 증거 수집할 때, 이 파일 작성 시간과 수정 날짜, 접근 시간등을 증거로 채택을 많이 합니다. 단순히 EXIF 정보를 수정한다고 증거 채택이 되지 않습니다. 포랜식 검사로 파일 조작 증명 여부를 판단할 수 있습니다.)


파일IO는 프로그램에서 메모리 자원을 사용하는 것이 아니고 하드 디스크의 자원을 사용하는 것입니다.

하드 디스크의 자원은 하나의 프로그램이 독점해서 사용하는 것이 아니고 여러 프로그램이 공용으로 사용하는 자원입니다. 즉, 프로그램에서 자원을 사용한다고 연결(connection)을 하면 반드시 리소스 반환(close)이 되어야 합니다.

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // 파일로 저장할 test 변수 선언
      String test = "hello world\r\n";
      // String 타입을 byte 형식으로 변환
      byte[] binary = test.getBytes();
      // 파일 인스턴스 생성
      File file = new File("d:\\work\\test.txt");
      // Stream 인스턴스 생성
      OutputStream stream = new FileOutputStream(file);
      // OutputStream에 test의 바이너리를 작성
      stream.write(binary);
      // 시스템을 멈춘다.
      synchronized (stream) {
        stream.wait();
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 소스대로 작성하면 OutputStream에서 파일의 연결(connection)을 잡고 시스템을 멈추게 했습니다.

즉, IO connection이 잡혀있는 상태에서 반환(close)이 되지 않은 상태입니다.


이 상태에서 메모장에서 수정을 하려고 하면 다음과 같은 에러가 발생합니다.

다른 프로세스에서 사용 중이므로 파일에 접근할 수 없다라는 에러가 발생하는 것입니다.

저는 콘솔로서 단발성 프로그램이라 close를 안해도 프로그램이 종료될 때 자동으로 리소스가 반환됩니다. 그러나 서버 프로그램에서 close를 안하면 어떻게 될까요?

서버 프로그램이 종료되기 전까지 다른 프로세스에서는 접근을 할 수 없는 상태가 되어 버립니다.


이렇게 close가 필요한 클래스는 보통 Closable 인터페이스를 상속받아 받드시 close 함수를 만들어야 합니다.

Closable 인터페이스를 상속받은 클래스들은 close를 자동으로 호출하는 방법이 있습니다.

import java.io.Closeable;
// Closeable 인터페이스 상속
public class Example implements Closeable {
  // 재정의 함수
  @Override
  public void close() {
    // 콘솔 촐력
    System.out.println("call close");
  }
  // 실행 함수
  public static void main(String[] args) {
    // 예외 처리 try에 if의 조건절 처럼 Closable를 상속받은 클래스를 넣으면 try의 스택영역이 끝날 때 자동으로 close를 호출한다.
    try (Example e = new Example()) {
      // 콘솔 출력
      System.out.println("hello world");
    }
  }
}

위 소스처럼 try에서 if의 조건절처럼 Closable 인터페이스를 상속 받은 클래스를 넣으면 스택 영역이 끝날 때 자동으로 close를 호출합니다.

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class Example {
  // 실행 함수
  public static void main(String[] args) {
    // 파일로 저장할 test 변수 선언
    String test = "hello world\r\n";
    // String 타입을 byte 형식으로 변환
    byte[] binary = test.getBytes();
    // 파일 인스턴스 생성
    File file = new File("d:\\work\\test.txt");
    // Stream 인스턴스 생성
    // 예외 처리 try에 if의 조건절 처럼 Closable를 상속받은 클래스를 넣으면 try의 스택영역이 끝날 때 자동으로 close를 호출한다.
    try (OutputStream stream = new FileOutputStream(file)) {
      stream.write(binary);
    // 기본적으로 Stream 클래스는 throws IOException이 있기 때문에 catch도 같이 붙어간다.
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

여기까지 Java에 파일(IO)를 다루는 방법(파일의 작성, 수정, 사용 날짜 변경과 close를 하는 이유, Closable 사용법)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.