JAVA/Java

[Java] NIO 와 NIO.2

민트맛녹차 2022. 10. 2. 01:53

Path 클래스

Path pt1 = Paths.get("C:\\JavaStudy\\PathDemo.java");
Path pt2 = pt1.getRoot();
Path pt3 = pt1.getParent();
Path pt4 = pt1.getFileName();

Path 인터페이스는 경로를 표현하기 위한 인터페이스다. java.io.File 클래스를 대체하기 위해 정의된 인터페이스로 java.nio.file 패키지에 정의되어 있다. 인스턴스의 생성은 해당 파일/디렉토리의 존재 유무와 상관이 없고 실행한다고 하여 파일/디렉토리가 생성되는 것도 아니다.

Path 인스턴스를 사용하여 파일이나 디렉토리의 정보를 담은 인스턴스를 생성할 수 있다. 

 

파일 및 디렉토리 생성

public static Path createFile(Path path, FileAttribute<?>...attrs) throws IOExeption { ... }
public static Path createDiretory(Path path, FileAttribute<?>...attrs) throws IOExeption { ... }
public static Path createDiretories(Path path, FileAttribute<?>...attrs) throws IOExeption { ... }

생성된 인스턴스를 사용해 파일 또는 디렉토리의 생성을 명령할 수 있다. createFile 과 createDirector 메서드는 경로가 유효하지 않거나 파일이 존재한다면 예외가 생긴다. createDirectories 메서드는 전달된 경로의 디렉토리를 모두 생성하므로 경로가 유효하지 않아도 예외가 발생하지 않는다. 생성 시 파일 또는 디렉토리에 속성과 권한을 부여할 수 있다.

 

파일 대상 입출력

java.nio.file.Fiels 에 정의된 입출력 메서드들을 사용하여 파일 대상 입출력을 할 수 있다.

public static byte[] readAllBytes(Path path) throws IOException { ... }
public static Path write(Path path, byte[] bytes, OpenOption...options) throws IOException { ... }

byte 배열을 입출력하는 메서드들이다. readAllBytes 메서드를 사용하면 byte 배열을 생성해 파일의 모든 내용을 저장하고 반환한다. write 메서드를 사용하면 path 가 지시하는 파일에 배열 bytes의 데이터가 전부 저장된다. options 에는 파일이 어떻게 열리는지 나타내며 대표적인 옵션은 다음과 같다.

  • APPEND : 파일의 끝에 데이터 추가
  • CREATE : 파일이 존재하지 않으면 생성
  • CREATE_NEW : 새파일 생성. 파일 존재하면 예외 발생
  • TRUNCATE_EXISTING : 파일 존재 시 내용 덮어씀

옵션을 전달하지 않으면 CREATE, TRUNCATE_EXISTING, WRITE 가 기본으로 지정된다.

public static List<String> readAllLines(Path path) throws IOException { ... }
public static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption...options) throws IOException { ... }

 문자 데이터를 입출력하는 메서드들이다. readAllLines 메서드를 사용하면 path가 지시하는 파일의 모든 내용을 저장하고 반환한다. write 메서드를 사용하면 path 가 지시하는 파일에 lines가 저장된다. 옵션을 전달하지 않으면 CREATE, TRUNCATE_EXISTING, WRITE 가 기본으로 지정된다.

 

파일 및 디렉토리 복사와 이동

public static Path copy(Path source, Path target, CopyOption...options) throws IOException { ... }
public static Path move(Path source, Path target, CopyOption...options) throws IOException { ... }

copy 메서드를 사용하면 src 가 지시하는 파일/디렉토리를 dst가 지시하는 위치와 이름으로 복사한다.

move 메서드를 사용하면 src 가 지시하는 파일/디렉토리를 dst가 지시하는 디렉토리로 이동한다.

 

NIO.2 기반 I/O 스트림 생성

/** 기존 바이트 스트림 **/
InputStream in = new FileInputStream("data.dat");

/** NIO.2 바이트 스트림 **/
InputStream in = new FileInputStream(Paths.get("data.dat"));

NIO.2 에서는 Path를 사용한 스트림 생성을 제안한다.

아래의 코드는 NIO.2에서 제안하는 방법으로 구현한 스트림들이다.

Path fp = Paths.get("data.dat");

DataInputStream in = new DataInputStream(Files.newInputStream(fp));
DataOutputStream out = new DataOutputStream(Files.newOutputStream(fp));

BufferedWriter bw = new BufferedWriter(Files.newBufferedWriter(fp));
BufferedReader br = new BufferedReader(Files.newBufferedReader(fp));

 

NIO 기반 입출력

NIO에서는 스트림을 대신해서 채널을 생성한다. 채널과 스트림은 데이터의 입출력을 위한 통로가 된다는 공통점을 가진다. 차이점은 다음과 같다. 스트림은 한 방향으로만 데이터가 이동하지만 채널은 양방향으로 데이터 이동이 가능하다. 또 채널은 직접 데이터 입출력을 허용하지 않으므로 반드시 버퍼에 연결해서 사용해야 한다.

Path src = Paths.get("C:\\Users\\alsrn\\Desktop\\test1.txt");
Path dst = Paths.get("C:\\Users\\alsrn\\Desktop\\test2.txt");

ByteBuffer buf = ByteBuffer.allocate(1024);

try (FileChannel ifc = FileChannel.open(src, StandardOpenOption.READ);
     FileChannel ofc = FileChannel.open(dst, StandardOpenOption.WRITE, StandardOpenOption.CREATE);) {

    int num;
    while (true) {
        num = ifc.read(buf);
        if (num==-1)
            break;

        buf.flip();
        ofc.write(buf);
        buf.clear();
    }

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

ByteBuffer 클래스의 allocate 메서드를 사용하여 버퍼를 생성한다. 인자로 전달된 크기의 버퍼가 생성된다. flip 메서드를 사용하면 버퍼에 저장된 데이터를 읽을 수 있는 상태로 변경한다. clear 메서드와 compact 메서드를 사용하면 버퍼를 비울 수 있다. clear 메서드는 버퍼를 완전히 비우고, compact 메서드는 버퍼에 저장된 내용 중에서 읽은 데이터만 지운다. 버퍼를 비우는 과정을 생략해도 컴파일과 실행은 되지만 버퍼에 데이터가 누적이 되어 정상적인 결과를 얻을 수 없게 된다.

FileChannel 클래스의 open 메서드를 사용하면 채널을 생성한다. 매개변수로 Path 인스턴스와 option을 받는다. 스트림과 달리 채널은 버퍼와 채널의 연결 과정을 거치지 않고 독립적으로 존재한다. 데이터를 읽을 때는 read, 쓸 때는 write 메서드를 사용한다. 이때 인자로 버퍼를 받아 버퍼로 읽거나 버퍼에 저장된 데이터를 쓴다.

기존 IO 모델은 입출력 스트림 각각에 버퍼 스트림은 연결해야만 해 두 개의 버퍼가 필요했다. 그리고 입력 버퍼에 저장된 데이터를 출력 버퍼로 이동하는 버퍼 사이의 데이터 이동 과정을 반드시 거쳐야 했다.

 

ByteBuffer buf1 = ByteBuffer.allocate(1024);	    // Non-direct 버퍼
ByteBuffer buf2 = ByteBuffer.allocateDirect(1024);	// Direct 버퍼

NIO 모델에서는 이러한 작업을 생략할 수 있다. NIO 모델은 Non-direct 버퍼를 생성하지 않고 Direct 버퍼를 생성하여 성능의 향상을 얻을 수 있다. Non-direct 버퍼는 이전에 소개한 버퍼로, 가상머신이 생성하고 유지하는 버퍼이다. 파일에 저장된 데이터를 읽어들일 때 운영체제 버퍼와 가상머신 버퍼를 거쳐 실행중인 프로그램으로 데이터가 전달된다. 반면 Direct 버퍼는 파일에 저장된 데이터를 읽어들일 때  운영체제 버퍼만 거쳐 실행중인 프로그램으로 데이터가 전달된다. 중간 과정이 하나 생략되어 성능 향상을 기대할 수 있으나, Direct 버퍼의 할당과 해제에 드는 시간적 비용이 Non-direct 버퍼에 비해 높기 때문에 입출력할 파일의 크기가 크지 않거나 버퍼를 빈번히 할당하고 해제해야하는 상황이라면 Non-direct 버퍼를 사용하는 것이 더 빠를 수 있다. Non-direct 버퍼는 ByteBuffer 이외에 CharBuffer, IntBuffer, DoubleBuffer 와 같은 버퍼를 사용해 입출력을 할 수 있지만 Direct 버퍼는 ByteBuffer 와 그 하위 클래스 대상으로만 생성이 된다.

 

NIO 모델은 파일 랜덤 접근을 지원한다. 파일 랜덤 접근은 파일에 데이터를 쓰거나 읽을 때 원하는 위치에 쓰거나 읽는 것을 의미한다. 

Path fp = Paths.get("C:\\Users\\alsrn\\Desktop\\test1.txt");

ByteBuffer wb = ByteBuffer.allocate(1024);

wb.putInt(120);
wb.putInt(240);
wb.putDouble(0.94);
wb.putDouble(0.75);

try (FileChannel fc = FileChannel.open(fp, StandardOpenOption.CREATE,
        StandardOpenOption.READ,
        StandardOpenOption.WRITE)) {

    wb.flip();
    fc.write(wb);

    ByteBuffer rb = ByteBuffer.allocate(1024);
    fc.position(0);
    fc.read(rb);

    rb.flip();
    System.out.println(rb.getInt());
    rb.position(Integer.BYTES * 2);

    System.out.println(rb.getDouble());
    System.out.println(rb.getDouble());

    rb.position(Integer.BYTES);
    System.out.println(rb.getInt());
} catch (IOException e) {
    e.printStackTrace();
}

일반적으로는 입력한 순서에 따라 120, 240, 0.94, 0.75 의 순으로 읽지만 위의 코드는 파일 랜덤 접근을 사용하여 120, 0.94, 0.75, 240 의 순으로 읽는다. 이는 버퍼의 포지션을 사용했기 때문이다. 포지션은 어느 위치까지 데이터를 썼고 읽었는지 표시하기 위한 위치 정보이다. 

버퍼를 생성하는 순간 포지션은 0이다. flip 메서드를 호출해도 포지션이 0이 된다. 데이터를 읽거나 쓰면 읽고 쓴 바이트 수만큼 포지션이 증가한다. wb.putInt(120) 나 rb.getInt() 를 사용했다면 포지션이 4 증가하고 wb.putDouble(0.94) 나 rb.getDouble() 을 사용했다면 포지션이 8 증가한다. position 메서드를 사용하면 인자로 받은 정수에 해당하는 위치로 포지션이 이동한다. 위의 코드에서 rb.position(Integer.BYTES) 를 호출하면 포지션 4로 이동하게 된다.

 

 

 

참조
윤성우의 열혈 JAVA 프로그래밍
https://docs.oracle.com/javase/9/docs/api/index.html?overview-summary.html