본문 바로가기

개발(합니다)/Java&Spring

[java-기초-19] NIO 기반 입출력 및 네트워킹

반응형

자바 4부터 새로운 입출력이라는 뜻으로 NIO(new Input/Output)인 java.nio 패키지가 포함 되었고 자바 7로 버전업하면서 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가되었습니다.

NIO.2는 java.nio2 패키지로 제공되지 않고 기존 jav.nio의 하위 패키지로 통합되어 제공하고 있습니다.

NIO 패키지 포함되어 있는 내용
java.nio 다양한 버퍼 클래스
java.nio.channels 파일 채널, TCP 채널, UDP 채널 등의 클래스
java.nio.channels.spi java.nio.channels 패키지를 위한 서비스 제공자 클래스
java.nio.charset 문자셋, 인코더, 디코더 API
java.nio.charset.spi java.nio.charset 패키지를 위한 서비스 제공자 클래스
java.nio.file 파일 및 파일 시스템에 접근하기 위한 클래스
java.nio.file.attribute 파일 및 파일 시스템의 속성에 접근하기 위한 클래스
java.nio.file.spi java.nio.file 패키지를 위한 서비스 제공자 클래스

IO와 NIO의 차이점

구분 IO NIO
입출력 방식 스트림 방식 채널 방식
버퍼 방식 넌버퍼 버퍼
비동기 방식 지원 안함 지원
블로킹 / 넌블로킹 방식 블로킹 방식만 지원 블로킹 / 넌블로킹 방식 모두 지원

스트림 vs 채널

  • IO는 스트림 기반으로 입력과 출력이 구분되어 있어 따로 생성해야 합니다.
  • NIO는 채널 기반으로 양방향으로 입출력이 가능합니다.

넌버퍼 vs 버퍼

  • IO는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는 식으로 하나씩 순차적으로 읽는 방식으로 이런 시스템은 대체로 느립니다.
  • NIO는 버퍼를 제공하여 복수 개의 바이트를 한꺼번에 입력받고 출력 하는 것이 가능하여 빠른 성능을 냅니다.

블로킹 vs 넌블로킹

  • IO는 블로킹되어 데이터가 입력되기 전까지 스레드는 블로킹되어 다른일을 할 수 없고 기다립니다.
  • NIO는 블로킹과 넌블로킹의 특성을 가지며 IO와 차이점은 스레드를 인터럽트로 빠져나올수 있습니다.

IO와 NIO의 선택

  • IO는 대용량 처리할 경우에 유리하며 버퍼가 없이 데이터를 흘리면 되고 연결 클라이언트 수가 적고, 전송되는 데이터가 대용량이면 순차적으로 처리될 필요성이 있을 경우에 사용하면 좋습니다.
  • NIO는 불특정 다수의 클라이언트 연결 또는 멀티 파일들을 넌블로킹이나 비동기로 처리하거나 스레드를 효과적으로 재사용할 수 있는 장점으로 하나의 입출력 처리 작업이 오래 걸리지 않는 경우에 사용하면 좋습니다.

NIO의 파일과 디렉토리

경로 정의(Path)

  • Path 구현체를 얻기 위해서는 java.nio.file.Paths 클래스의 정적 메서드인 get()메서드를 호출하면 됩니다.
리턴 타입 메서드(매개 변수) 설명
int compareTo(Path other) 파일 경로가 동일하면 0을 리턴,
상위 경로면 음수,
하위 경로면 양수를 리턴
음수와 양수 값의 차이나는 문자열의 수
Path getFileName() 부모 경로를 제외한 파일 또는 디렉토리 이름만 가진 Path리턴
FileSystem getFileSystem() FileSystem 객체 리턴
Path getName(int index) C:/Temp/dir/file.txt 일 경우
index가 0이면 "Temp"의 Path 객체 리턴
index가 1이면 "dir"의 Path 객체 리턴
index가 2이면 "file.txt"의 Path 객체 리턴
int getNameCount() 중첩 경로 수, C:/Temp/dir/file.txt일 경우 3을 리턴
Path getParent() 바로 위 부모 폴더의 Path 리턴
Path getRoot() 루트 디렉토리의 Path 리턴
Interator<Path> iterator() 경로에 있는 모든 디렉토리와 파일을 Path 객체로 생성하고 반복자를 리턴
Path normalize() 상대 경로로 표기할 때 불필요한 요소를 제거
C:/Temp/dir1/.../dir2/file.txt -> C:/Temp/dir2/file.txt
WatchKey register(..) WatchService를 등록(와치 서비스에서 설명함)
File toFile() java.io.File 객체로 리턴
String toString() 파일 경로를 문자열로 리턴
URI toUri() 파일 경로를 URI 객체로 리턴

package networks2;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;


public class Nio1 {
    public static void main(String[] args) {
        Path path = Paths.get("src/networks2/Nio1.java");
        System.out.println("파일명 : " + path.getFileName());
        System.out.println("부모 디렉토리 : " + path.getParent().getFileName());
        System.out.println("중첩 경로 수 : " + path.getNameCount());

        System.out.println();
        for (int i = 0; i < path.getNameCount(); i++ ) {
            System.out.println(path.getName(i));
        }

        System.out.println();
        Iterator<Path> iterator = path.iterator();
        while (iterator.hasNext()) {
            Path temp = iterator.next();
            System.out.println(temp.getFileName());
        }

    }
}

파일 시스템 정보(FileSystem)

리턴 타입 메서드(매개 변수) 설명
Iterable<FileStore> getFileStores() 드라이버 정보를 가진 FileStore 객체들을 리턴
Iteralbe<Path> getRootDirectories() 루트 디렉토리 정보를 가진 Path 객체들을 리턴
String getSeparator() 디렉토리 구분자 리턴
long getTotalSpace() 드라이버 전체 공간 크기 리턴
long getUnallocatedSpace() 할당되지 않은 공간 크기 리턴
long getUsableSpace() 사용 가능한 공간 크기, getUnallocatedSpace()와 동일한 값
boolean isReadOnly() 읽기 전용 여부
String  name() 드라이버명 리턴
String type() 파일 시스템 종류

파일 속성 읽기 및 파일, 디렉토리 생성/삭제

리턴 타입 메서드(매개 변수) 설명
long 또는 Path copy(...) 복사
Path createDirectories(...) 모든 부모 디렉토리 생성
Path createDirectory(...) 경로의 마지막 디렉토리만 생성
Path createFile(...) 파일 생성
void delete(...) 삭제
boolean deleteIfExists(...) 존재하면 삭제
boolean exists(...) 존재 여부 
FileStore getFIleStore(...) 파일이 위치한 FileStore(드라이브) 리턴
FileTime getLastModifiedTime(...) 마지막 수정 시간을 리턴
UserPrincipal getOwner(...) 소유자 정보를 리턴
boolean isDirectory(...) 디렉토리인지 여부
boolean isExecutable(...) 실행 가능 여부
boolean isHidden(...) 숨김 여부
boolean isReadable(...) 읽기 가능 여부
boolean isRegularFile(...) 일반 파일인지 여부
boolean isSameFile(...) 같은 파일인지 여부
boolean isWritable(...) 쓰기 가능 여부
Path move(...) 파일 이동
BufferedReader newBufferedReader(...) 텍스트 파일을 읽는 BufferedReader 리턴
BufferedWriter newBufferedWriter(...) 텍스트 파일에 쓰는 BufferedWriter 리턴
SeekableByteChannel newByteChannel(...) 파일에 읽고 쓴느 바이트 채널을 리턴
DirectoryStream<Path> newDirectoryStream(...) 디렉토리의 모든 내용을 스트림으로 리턴
InputStream newInputStream(...) 파일의 InputStream 리턴
OutputStream newOutputStream(...) 파일의 OutputStream 리턴
boolean notExists(...) 존재하지 않는지 여부
String probeContentType(...) 파일의 MIME 타입을 리턴
byte[] readAllBytes(...) 파일의 모든 바이트를 읽고 배열로 리턴
List<String>  readAllLines(...) 텍스트 파일의 모든 라인을 읽고 리턴
long size(...) 파일의 크기 리턴
Path write(...) 파일에 바이트나 문자열을 저장

와치 서비스(WatchService)

자바 7에서 처음 소개되었고 디렉토리 내부에서 파일 생성, 삭제, 수정 등의 내용의 변화를 감시하는데 사용합니다.

package networks2;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.io.IOException;
import java.nio.file.*;
import java.util.List;


public class Nio2 extends Application {

    class WatchServiceThread extends Thread {
        @Override
        public void run() {
            try {
                WatchService watchService = FileSystems.getDefault().newWatchService();
                Path directory = Paths.get("C:/_temp");
                directory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                        StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
                while (true) {
                    WatchKey watchKey = watchService.take();
                    List<WatchEvent<?>> list = watchKey.pollEvents();

                    for (WatchEvent watchEvent : list) {
                        WatchEvent.Kind kind = watchEvent.kind();
                        Path path = (Path) watchEvent.context();
                        if(kind == StandardWatchEventKinds.ENTRY_CREATE) {
                            Platform.runLater(() -> textArea.appendText("파일 생성됨 -> " + path.getFileName() + " \n"));
                        }else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                            Platform.runLater(() -> textArea.appendText("파일 삭제됨 -> " + path.getFileName() + " \n"));
                        }else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                            Platform.runLater(() -> textArea.appendText("파일 수정됨 -> " + path.getFileName() + " \n"));
                        }else if (kind == StandardWatchEventKinds.OVERFLOW) {

                        }
                    }

                    boolean valid = watchKey.reset();
                    if(!valid) { break;}
                }

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

        }
    }

    TextArea textArea;

    @Override
    public void start(Stage stage) throws Exception {
        BorderPane root = new BorderPane();
        root.setPrefSize(500, 300);

        textArea = new TextArea();
        textArea.setEditable(false);
        root.setCenter(textArea);

        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("WatchServiceExample");
        stage.show();

        WatchServiceThread watchServiceThread = new WatchServiceThread();
        watchServiceThread.start();

    }

    public static void main(String[] args) {
        launch(args);
    }
}
  • javaFx 버전 문제로 실행은 못시켜봤습니다.

버퍼

넌다이렉트와 다이렉트 버퍼

넌다이렉트 버퍼는 JVM이 관리하는 힙 메모리 공간을 이용하는 버퍼이고, 다이렉트 버퍼는 운영체제가 관리하는 메모리 공간을 이용하는 버퍼입니다.

구분 넌다이렉트 버퍼 다이렉트 버퍼
사용하는 메모리 공간 JVM의 힙 메모리 운영체제의 메모리
버퍼 생성 시간 버퍼 생성이 빠르다. 버퍼 생성이 느리다.
버퍼의 크기 작다. 크다.(큰 데이터를 처리할 때 유리)
입출력 성능 낮다. 높다.(입출력이 빈번할 때 유리)
package networks2;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;


public class Nio3 {
    public static void main(String[] args) {
        Path from = Paths.get("C:/_temp/test.txt");
        Path to1 = Paths.get("C:/_temp/test1.txt");
        Path to2 = Paths.get("C:/_temp/test2.txt");

        try {
            long size = Files.size(from);
            FileChannel fileChannelFrom = FileChannel.open(from);
            FileChannel fileChannelTo1 = FileChannel.open(to1, EnumSet.of(StandardOpenOption.CREATE,
                    StandardOpenOption.WRITE));
            FileChannel fileChannelTo2 = FileChannel.open(to2, EnumSet.of(StandardOpenOption.WRITE));

            ByteBuffer noneDirectBuffer = ByteBuffer.allocate((int) size); // allocate의 인자 수만큼 버퍼 생성
            ByteBuffer directBuffer = ByteBuffer.allocateDirect((int) size); // allocate의 인자 수만큼 버퍼 생성

            long start, end;

            start = System.nanoTime();

            for (int i =0; i < 100; i++) {
                fileChannelFrom.read(noneDirectBuffer);
                noneDirectBuffer.flip();
                fileChannelTo1.write(noneDirectBuffer);
                noneDirectBuffer.clear();
            }
            end = System.nanoTime();
            System.out.println("넌다이렉트 \t " + (end - start) + " ns");

            fileChannelFrom.position(0);


            start = System.nanoTime();
            for (int i =0; i < 100; i++) {
                fileChannelFrom.read(directBuffer);
                directBuffer.flip();
                fileChannelTo2.write(directBuffer);
                directBuffer.clear();
            }

            end = System.nanoTime();
            System.out.println("다이렉트 \t " + (end - start) + " ns");

            fileChannelFrom.close();
            fileChannelTo1.close();
            fileChannelTo2.close();


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


    }
}

Buffer 생성

넌다이렉트 버퍼를 생성하기 위해서는 Buffer 클래스의 allocate()와 wrap() 메서드를 호출하면 되고,
다이렉트 버퍼를 생성하기 위해서는 ByteBuffer의 allocateDirect() 메서드를 호출하면 됩니다.

  1. allocate() 메서드 : 인자만큼의 버퍼를 생성합니다.
  2. wrap() 메서드 : 이미 생성되어 있는 자바 배열을 래핑해서 Buffer 객체를 생성합니다.
  3. allocateDirect() 메서드 : 운영체제가 관리하는 메모리에 다이렉트 버퍼를 생성합니다. 단, ByteBuffer에서만 제공

byte 해석 순서

데이터를 처리할 때 바이트 처리 순서는 운영체제마다 차이가 있는데, 앞쪽 바이트부터 먼저 처리하는 것을 Big endian이라고 하고, 뒤쪽 바이트부터 먼저 처리하는 것을 Little endian이라고 합니다.

Buffer의 위치 속성

속성 설명
position 현재 읽거나 쓰는 위치값입니다. 인덱스 값이기 때문에 0부터 시작하며, limit보다 큰 값을 가질수 없고 만약 position과 limit의 값이 같아진다면 더 이상 데이터를 쓰거나 읽을 수 없다는 의미입니다.
limit 버퍼에서 읽거나 쓸 수 있는 위치의 한계를 나타내며, 이 값은 capacity보다 작거나 같은 값을 가집니다. 최초에 버퍼를 만들때는 capacity와 같은 값을 가집니다.
capacity 버퍼의 최대 데이터 개수(메모리 크기)를 나타내며 인덱스 값이 아니라 수량임을 주의합니다.
mark reset() 메서드를 실행했을 때에 돌아오는 위치를 지정하는 인덱스로서 mark() 메서드로 지정할 수 있습니다. 주의할 점은 반드시 position이하의 값으로 지정해주어야 하고 position이나 limit의 값이 mark값보다 작은 경우, mark는 자동 제거 됩니다. mark가 없는 상태에서 reset()메서드를 호출하면 InvalidMarkException이 발생합니다.

Buffer 공통 메서드

리턴 타입 메서드(매개변수) 설명
Object array() 버퍼가 래핑한 배열을 리턴
int arrayOffset() 버퍼의 첫 번째 요소가 있는 내부 배열의 인덱스를 리턴
int capacity() 버퍼의 전체 크기를 리턴
Buffer clear() 버퍼의 위치 속성을 초기화(position=0, limit=capacity)
Buffer flip() limit를 position으로, position을 0인덱스로 인동
boolean hasArray() 버퍼가 래핑한 배열을 가지고 있는지 여부
boolean hasRemaining() position과 limit사이에 요소가 있는지 여부(position < limit)
boolean hasDirect() 운영체제의 버퍼를 사용하는지 여부
boolean isReadOnly() 버퍼가 읽기 전용인지 여부
int limit() limit 위치를 리턴
Buffer limit(int newLimit) newLimit으로 limit위치를 설정
Buffer mark() 현재 위치를 mark로 표시
int position() position 위치를 리턴
Buffer position(int newPosition) newPosition으로 position 위치를 설정
int remaining() position과 limit사이의 요소의 개수
Buffer reset() position을 mark위치로 이동
Buffer rewind() position을 0인덱스로 이동

파일 채널

fileChannel은 정적 메서드은 open()을 호출해서 사용할 수 있고 IO의 FileInputStream, FileOutputStream의 getChannel()메서드를 호출해서 얻을 수도 있습니다.

열거 상수 설명
READ 읽기용으로 파일을 연다
WRITE 쓰기용으로 파일을 연다
CREATE 파일이 없다면 새 파일을 생성한다
CREATE_NEW 새 파일을 만든다. 이미 파일이 있으면 예외와 함께 실패한다
APPEND 파일 끝에 데이터를 추가한다(WRITE나 CREATE와 함께 사용)
DELETE_ON_CLOSE 채널을 닫을 때 파일을 삭제한다(임시 파일을 삭제할 때 사용)
TRUNCATE_EXISTING 파일을 0바이트로 잘라낸다(WRITE 옵션과 함께 사용)

기본 사용법

package networks2;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;


public class Nio4 {
    public static void main(String[] args) {
        try {
            FileChannel fileChannel = FileChannel.open(Paths.get("C:/_temp/test.txt"), StandardOpenOption.CREATE_NEW,
                    StandardOpenOption.WRITE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

파일 생성 및 쓰기

public class Nio5 {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("C:/_temp/fileChannel.txt");
        Files.createDirectories(path.getParent());

        FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE);

        String data = "안녕하세요";
        Charset charset = Charset.defaultCharset();
        ByteBuffer byteBuffer = charset.encode(data);

        int byteCount = fileChannel.write(byteBuffer);
        System.out.println("file.txt : " + byteCount + " bytes written");

        fileChannel.close();
    }
}

파일 읽기

package networks2;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;


public class Nio6 {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("C:/_temp/fileChannel.txt");

        FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
        ByteBuffer byteBuffer = ByteBuffer.allocate(100);

        Charset charset = Charset.defaultCharset();
        String data ="";
        int byteCount;

        while(true) {
            byteCount = fileChannel.read(byteBuffer);
            if (byteCount == - 1) break;
            byteBuffer.flip();
            data += charset.decode(byteBuffer).toString();
            byteBuffer.clear();
        }

        fileChannel.close();
        System.out.println("file.txt : " + data);
    }
}

파일 복사

package networks2;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;


public class Nio7 {
    public static void main(String[] args) throws IOException {
        Path from = Paths.get("C:/_temp/test.txt");
        Path to = Paths.get("C:/_temp/fileChannelCopy.txt");

        FileChannel fileChannelFrom = FileChannel.open(from, StandardOpenOption.READ);
        FileChannel fileChannelTo = FileChannel.open(to, StandardOpenOption.CREATE, StandardOpenOption.WRITE);

        ByteBuffer buffer = ByteBuffer.allocateDirect(100);
        int byteCount;

        while(true) {
            buffer.clear();
            byteCount = fileChannelFrom.read(buffer);
            if (byteCount == -1) break;
            buffer.flip();
            fileChannelTo.write(buffer);
        }

        fileChannelFrom.close();
        fileChannelTo.close();
        System.out.println("파일 복사 성공");


    }
}

파일 비동기 채널

비동기 처리는 AsynchronousFileChannel로 처리할 수 있습니다.

반응형