JAVA/이것이 자바다

Chapter .19-1 NIO 기반 입출력 및 네트워킹

개념원리 2021. 8. 5. 10:35

19.1 NIO 소개

자바4부터 새로운 입출력(NIO : New Input/Output)이라는 뜻에서 java.nio 패키지가 포함되었는데, 자바 7로 버전업 하면서 자바 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고, 비동기채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가 되었다.

 

NIO.2는 java.nio2패키지로 제공되지 않고 기존 java.nio의 하위 패키지(java.nio.channels, java.nio.charset, java.nio.file)에 통합되어 있다.

 

이 책에서는 NIO와 NIO.2를 구별하지 않고 그냥 NIO로 부르기로 하겠다.

NIO에서 제공하는 패키지

 

19.1.1 IO와 NIO의 차이점

IO와 NIO는 데이터를 입출력한다는 목적은 동일하지만, 방식에 있어서 크게 차이가 난다.

IO와 NIO의 차이점

 

스트림 vs. 채널

- IO 스트림:  입력 스트림과 출력 스트림으로 구분되어 별도 생성

- NIO  채널: 양방향으로 입출력이 가능하므로 하나만 생성

 

IO는 스트림(Stream) 기반이다.

스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야 하고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야 한다.

 

예를 들어 하나의 파일에서 데이터를 읽고 저장하는 작업을 모두 해야 한다면 FileInputStream과 FileOutputStream을 별도로 생성해야 한다.

 

NIO는 채널(Channel) 기반이다.

채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다.

그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.

 

예를 들어 하나의 파일에서 데이터를 읽고 저장하는 작업을 모두 해야 한다면 FileChannel 하나만 생성하면 된다.

 

 

넌버퍼 vs. 버퍼

• IO 스트림 - 넌버퍼(non-buffer)

  – IO에서는 1바이트씩 읽고 출력 – 느림

  – 보조 스트림인 BufferedInputStream, BufferedOutputStream를 사용해 버퍼 제공 가능

  – 스트림으로부터 입력된 전체 데이터를 별도로 저장해야

    » 저장 후 입력 데이터의 위치 이동해가면서 자유롭게 이용 가능

 

• NIO 채널 - 버퍼(buffer)

  – 기본적으로 버퍼 사용해 입출력 - 성능 좋음

  – 읽은 데이터를 무조건 버퍼(Butter: 메모리 저장소)에 저장

    » 버퍼 내에서 데이터 위치 이동해 가며 필요한 부분만 읽고 쓸 수 있음

 

IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다.

이런 시스템은 대체로 느리다.

이것보다는 버퍼(Buffer : 메모리 저장소)를 사용해서 복수 개의 바이트를 한꺼번에 입력받고 출력하는 것이 빠른 성능을 낸다.

 

그래서 IO는 버퍼를 제공해주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해서 사용하기도 한다.

 

NIO는 기본적으로 버퍼를 사용해서 입출력을 하기 때문에 IO보다는 입출력 성능이 좋다.

 

채널은 버퍼에 저장된 데이터를 출력하고, 입력된 데이터를 버퍼에 저장한다.

 

IO는 스트림에서 읽은 데이터를 즉시 처리한다.

그렇기 때문에 스트림으로 부터 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 이동해 가면서 자유롭게 이용할 수 없다.

 

NIO는 읽은 데이터를 무조건 버퍼에 저장하기 때문에 버퍼 내에서 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓸 수 있다.

 

블로킹 vs. 넌블로킹

 

• IO 스트림 - 블로킹

  – 입력 스트림의 read() 메소드 호출

      » 데이터 입력 전까지 스레드는 블로킹(대기상태)

  – 출력 스트림의 write() 메소드 호출

      » 데이터 출력 전까지 스레드는 블로킹

  – 스레드 블로킹 -다른 일을 할 수가 없고 interrupt 해 블로킹 빠져나올 수도 없음

  –  블로킹을 빠져 나오는 유일한 방법 - 스트림을 닫는 것

 

• NIO 채널 – 블로킹, 넌블로킹

  – NIO 블로킹은 스레드를 interrupt 함으로써 빠져나올 수 있음

  – NIO는 넌블로킹 지원

    » 입출력 작업 시 스레드가 블로킹되지 않음

  – 준비 완료된 채널을 선택하는 기능!!!

 

IO는 블로킹(Blocking) 된다.

입력 스트림의 read() 메소드를 호출하면 데이터가 입력되기 전까지 스레드는 블로킹(대기 상태)된다.

 

마찬가지로 출력 스트림의 write() 메소드를 호출하면 데이터가 출력되기 전까지 스레드는 블로킹 된다.

 

IO 스레드가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트(interrupt)도 할 수 없다.

블로킹을 빠져나오는 유일한 방법은 스트림을 닫는 것이다.

 

NIO는 블로킹과 넌블로킹(non-blocking) 특징을 모두 가지고 있다.

IO 블로킹과의 차이점은 NIO 블로킹은 스레드를 인터럽트함으로써 빠져나올 수가 있다는 것이다.

 

블로킹의 반대 개념이 넌블로킹인데, 입출력 작업 시 스레드가 블로킹되지 않는 것을 말한다.

NIO의 넌블로킹은 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하기 때문에 작업 스레드가 블로킹되지 않는다.

 

여기서 작업 준비가 완료되었다는 뜻은 지금 바로 읽고 쓸 수 있는 상태를 말한다.

 

NIO 넌블로킹의 핵심 객체는 멀티플렉서(multiplexor)인 셀렉터(Selector)이다.

 

셀렉터는 복수 개의 채널 중에서 준비 완료된 채널을 선택하는 방법을 제공해준다.

 

 

19.1.2 IO와 NIO의 선택

- IO 방식 선택하는 경우

  • 연결 클라이언트의 수가 적고,

  • 전송되는 데이터가 대용량이면서

  • 순차적으로 처리될 필요성 있을 경우

 

- NIO 방식 선택하는 경우

  • 연결 클라이언트의 수가 많고

  • 전송되는 데이터 용량이 적으면서, 

  • 입출력 작업 처리가 빨리 끝나는 경우

 

NIO는 불특정 다수의 클라이언트 연결 또는 멀티 파일들을 넌블로킹이나 비동기로 처리할 수 있기 때문에 과도한 스레드생성을 피하고 스레드를 효과적으로 재사용한다는 점에서 큰 장점이 있다.

또한 운영체제의 버퍼(다이렉트 버퍼)를 이용한 입출력이 가능하기 때문에 입출력 성능이 향상된다.

 

NIO는 연결 클라이언트 수가 많고, 하나의 입출력 처리 작업이 오래 걸리지 않는 경우에 사용하는 것이 좋다.

스레드에서 입출력 처리가 오래 걸린다면 대기하는 작업의 수가 늘어나기 때문에 제한된 스레드로 처리하는 것이 오히려 불리할 수 있다.

 

대용량 데이터를 처리할 경우엔느 IO가 더 유리한데, NIO는 버퍼의 할당 크기도 문제가 되고, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 받은 즉시 처리하는 IO보다는 좀 더 복잡하다.

 

연결 클라이언트 수가 적고, 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있을 경우에는 IO로 서버를 구현하는 것이 좋다.

 

19.2 파일과 디렉토리

IO는 파일의 속성 정보를 읽기 위해 File 클래스만 제공하지만, NIO는 좀 더 다양한 파일의 속성정보를 제공해주는 클래스와 인터페이스를 java.nio.file, java.nio.file.attribute 패키지에서 제공하고 있다.

 

19.2.1 경로 정의(Path)

NIO에서 제일 먼저 살펴봐야 할 API는 java.nio.file.Path 인터페이스이다.

Path는 IO의 java.io.File 클래스에 대응되는 NIO 인터페이스이다.

 

NIO의 API에서 파일의 경로를 지정하기 위해 Path를 사용하기 떄문에 Path 사용방법을 잘 익혀두어야 한다.

Path 객체를 얻기 위해서는 java.nio.file.Path 클래스의 정적 메소드인 get() 메소드를 호출하면 된다.

Path path = Paths.get(String first, String... more)
Path path = Path.get(URI uri);

get() 메소드의 매개값은 파일의 경로인데, 문자열로 지정할 수도 있고, URI 객체로 지정할 수도 있다.

문자열로 지정할 경우 전체 경로를 한꺼번에 지정해도 좋고, 상위 디렉토리와 하위 디렉토리를 나열해서 지정해도 좋다.

다음은 "C:\Temp\dir\file.txt" 경로를 이용해서 Path 객체를 얻는 방법을 보여준다.

Path path = Paths.get("C:\Temp\dir\file.txt");
Path path = Paths.get("C:\Temp\dir", "file.txt");
Path path = Paths.get("C:","Temp","dir","file.txt");

파일의 경로는 절대 경로와 상대 경로를 모두 사용할 수 있다.

만약 현재 디렉토리 위치가 "C:\Temp"일 경우 "C:\Temp\dir\file.txt"는 다음과 같이 지정이 가능하다.

Path path = Paths.get("dir\file.txt");
Path path = Paths.get(".\dir\file.txt");

 

현재 위치가 "C:\Tempdir1"이라면 "C:\Temp\dir2\file.txt:는 다음과 같이 지정이 가능하다.

Path path = Paths.get("..\dir2\file.txt");

Path 인터페이스에는 다음과 같이 파일 경로에서 얻을 수 있는 여러가지 정보를 제공해주는 메소드가 있다.

 

다음 예제는 상대 경로를 이용해서 소스파일에 대한 Path 객체를 얻고, 파일명, 부모 디렉토리명, 중첩 경로 수, 경로 상에 있는 모든 디렉토리를 출력한다.

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

public class PathExample {
	public static void main(String[] args) throws Exception {
		Path path = Paths.get("src/sec02/exam01_path/PathExample.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());
		}
	}
}

 

 

19.2.2 파일 시스템 정보(File System)

운영체제의 파일 시스템은 FileSystem 인터페이스를 통해서 접근할 수 있다.

FileSystem 구현 객체는 FileSystems의 정적 메소드인 getDefault()로 얻을 수 있다.

FileSystem fileSystem = FileSystems.getDefault();

FileSystem에서 제공하는 메소드

파일 시스템 정보 얻기 예제

import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;

public class FileSystemExample {
	public static void main(String[] args) throws Exception {
		FileSystem fileSystem = FileSystems.getDefault();
		for(FileStore store : fileSystem.getFileStores()) {
			System.out.println("드라이버명: " + store.name());
			System.out.println("파일시스템: " + store.type());
			System.out.println("전체 공간: \t\t" + store.getTotalSpace() + " 바이트");
			System.out.println("사용 중인 공간: \t" + (store.getTotalSpace() - store.getUnallocatedSpace()) + " 바이트");
			System.out.println("사용 가능한 공간: \t" + store.getUsableSpace() + " 바이트");
			System.out.println();
		}
		
		System.out.println("파일 구분자: " + fileSystem.getSeparator());
		System.out.println();
		
		for(Path path : fileSystem.getRootDirectories()) {
			System.out.println(path.toString());
		}
	}
}

 

 

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

 

- java.nio.file.Files 클래스

  • 파일과 디렉토리의 생성 및 삭제, 이들의 속성을 읽는 메소드 제공

    – 속성: 숨김, 디렉토리 여부, 크기, 소유자…

 

  • 제공 메소드 참조해 파일 속성 읽고 출력 (p.1107~1109) 

제공 메소드

 

다음 예제는 파일의 속성을 읽고 출력한다.

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileExample {
	public static void main(String[] args) throws Exception {
		Path path = Paths.get("src/sec02/exam03_file_directory/FileExample.java");
		System.out.println("디렉토리 여부: " + Files.isDirectory(path));
		System.out.println("파일 여부: " + Files.isRegularFile(path));
		System.out.println("마지막 수정 시간: " + Files.getLastModifiedTime(path));
		System.out.println("파일 크기: " + Files.size(path));
		System.out.println("소유자: " + Files.getOwner(path).getName());
		System.out.println("숨김 파일 여부: " + Files.isHidden(path));
		System.out.println("읽기 가능 여부: " + Files.isReadable(path));
		System.out.println("쓰기 가능 여부: " + Files.isWritable(path));
	}
}

출력 결과

 

다음 예제는 디렉토리와 파일을 생성하고, 디렉토리의 내용을 출력한다.

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class DirectoryExample {
	public static void main(String[] args) throws Exception {
		Path path1 = Paths.get("C:/Temp/dir/subdir");
		Path path2 = Paths.get("C:/Temp/file.txt");
		
		if(Files.notExists(path1)) {
			Files.createDirectories(path1);
		}
		
		if(Files.notExists(path2)) {
			Files.createFile(path2);
		}
		
		Path path3 = Paths.get("C:/Temp");
		DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path3);
		for(Path path : directoryStream) {
			if(Files.isDirectory(path)) {
				System.out.println("[디렉토리] " + path.getFileName());
			} else {
				System.out.println("[파일] " + path.getFileName() + " (크기:" + Files.size(path) + ")");
			}
		}
	}
}

 

 

19.2.4 와치 서비스(WatchService)

와치 서비스(WatchSercie)는 자바 7에서 처음 소개된 것으로 디렉토리 내부에서 파일 생성, 삭제, 수정 등의 내용 변화를 감시하는데 사용된다.

 

흔하게 볼 수 있는 와치 서비스의 적용 예는 에디터에서 파일을 편집하고 있을 때, 에디터 바깥에서 파일 내용을 수정하게 되면 파일 내용이 변경 됬으니 파일을 다시 불러올 것인지 묻는 대화상자를 띄우는 것이다.

 

와치 서비스는 일반적으로 파일 변경 통지 메커니즘으로 알려져 있다.

WatchService를 생성하려면 다음과 같이 FileSystem의 newWatchService() 메소드를 호출하면 된다.

WatchService watchService = FileSystems.getDefault().newWatchService();

WatchService를 생성했다면 감시가 필요한 디렉토리의 Path객체에서 register() 메소드로 WatchService를 등록하면 된다.

이때 어떤 변화(생성, 삭제, 수정)를 감지할것인지를 standard WatchEventKinds 상수로 지정할 수 있다.

 

다음은 생성, 수정, 삭제를 감시하도록 WatchService를 등록한다.

directory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
						         StandardWatchEventKinds.ENTRY_DELETE,
						         StandardWatchEventKinds.ENTRY_MODIFY);

디렉토리(Path)에 WatchService를 등록한 순간부터 디렉토리 내부에서 변경이 발생되면 와치 이벤트(WatchEvent)가 발생하고, WatchService는 해당 이벤트 정보를 가진 와치키(WatchKey)를 생성하여 큐(Queue)에 넣어준다.

 

프로그램은 무한 루프를 돌면서 WatchService의 take()메소드를 호출하여 WatchKey가 큐에 들어올 때까지 대기하고 있다가 WatchKey가 큐에 들어오면 WatchKey를 얻어 처리하면 된다.

while(true) {
	WatchKey watchKey = watchService.take();  //큐에 WatchKey가 들어올 때까지 대기
}

 

WatchKey를 얻고 나서 해야 할 일은 pollEvents() 메소드를 호출해서 WatchEvent 리스트를 얻어내는 것이다.

한 개의 WatchEvent가 아니라 List<WatchEvent<?>>로 리턴하는 이유는 여러 개의 파일이 동시에 삭제, 수정, 생성될 수 있기 때문이다.

참고로 WatchEvent는 파일당 하나씩 발생한다.

List<WatchEvent<?>> list = watchKey.pollEvents();

프로그램은 WatchEvent 리스트에서 WatchEvent를 하나씩 꺼내어 이벤트의 종류와 path 객체를 얻어낸 다음 적절히 처리하면 된다.

for(WatchEvent watchEvent : list) {
    //이벤트 종류 얻기
    Kind kind = watchEvent.kind();
    //감지된 Path 얻기
    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) {
    }
}

OVERFLOW 이벤트는 운영체제에서 이벤트가 소실됬거나 버려진 경우에 발생하므로 별도의 처리 코드가 필요 없다.

따라서 Create, Delete, Modify 이벤트만 처리하면 된다.

 

한번 사용된 WatchKey는 reset() 메소드로 초기화해야 하는데, 새로운 WatchEvent가 발생하면 큐에 다시 들어가기 때문이다.

 

초기화에 성공하면 reset() 메소드는 true를 리턴하지만, 감시하는 디렉토리가 삭제되었거나 키가 더이상 유효하지 않을 경우에는 false를 리턴한다.

 

WatchKey가 더 이상 유효하지 않게되면 무한 루프를 빠져나와 WatchService의 close() 메소드를 호출하고 종료하면 된다.

 

while(true){
    WatchKey watchKey = watchService.take();
    List<WatchEvent<?>> list = watchKey.pollEvents();
    
    for(WatchEvent watchEvent : list){
        .......
    }
    
    boolean valid = watchKey.reset();
    if(!valid){ break; }
}

watchService.close();

 

 

다음 예제는 C:\Temp 디렉토리를 감시 디렉토리로 설정했다.

 

실행 후 C:\Temp\dir 디렉토리와 C:\Temp\file.txt 파일을 생성하고, file.txt 파일 내용을 수정 한 다음 저장하였다.

그리고 dir, file.txt를 동시에 삭제했다.

 

이 모든 행위들이 TextArea에 기록되는 것을 볼 수 있다.

import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;

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;

public class WatchServiceExample 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) {
						//이벤트 종류 얻기
						Kind kind = watchEvent.kind();
						//감지된 Path 얻기
						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 (Exception e) {}
		}
	}	
	
	TextArea textArea;
	
	@Override
	public void start(Stage primaryStage) throws Exception {
		BorderPane root = new BorderPane();
		root.setPrefSize(500, 300);
		
		textArea = new TextArea();
		textArea.setEditable(false);
		root.setCenter(textArea);
		
		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("WatchServiceExample");
		primaryStage.show();
		
		WatchServiceThread wst = new WatchServiceThread();
		wst.start();
	}
	
	public static void main(String[] args) {
		launch(args);
	}
}

 

 

19.3 버퍼

NIO에서는 데이터를 입출력하기 위해 항상 버퍼를 사용해야 한다.

버퍼는 읽고 쓰기가 가능한 메모리 배열이다.

 

버퍼를 이해하고 잘 사용할 수 있어야 NIO에서 제공하는 API를 올바르게 활용할 수 있다.

 

 

19.3.1 Buffer 종류

- 분류 기준

  • 저장되는 데이터 타입에 따라 분류

    – ByteBuffer, CharBuffer, IntBuffer, DoubleBuffer

  • NIO 버퍼는 저장되는 데이터 타입에 따라 별도의 클래스로 제공

  • 이들 버퍼 클래스들은 Buffer 추상 클래스 모두 상속

  • MappedByteBuffer는 파일의 내용에 랜덤하게 접근하기 위해서 파일의 내용을 메모리와 맵핑시킨 버퍼

 

Buffer의 종류

 

 • 어떤 메모리를 사용하느냐에 따른 분류

   – 다이렉트(Direct)와, 넌다이렉트(NonDirect) 버퍼

   – P.1115~1118 예제에서 버퍼 속도 차이만 확실히 이해

 

버퍼가 사용하는 메모리의 위치에 따라서 넌다이렉트(non-direct) 버퍼와 다이렉트(direct) 버퍼로 분류된다.

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

 

 

19.3.2 Buffer 생성

각 데이터 타입별로 넌다이렉트 버퍼를 생성하기 위해서는 각 Buffer 클래스의 allocate()와 wrap() 메소드를 호출하면 되고, 다이렉트 버퍼는 ByteBuffer의 allocateDirect() 메소드를 호출하면 된다.

 

allocate() 메소드(넌다이렉트 버퍼 생성)

  • 각 데이터 타입 별 넌다이렉트 버퍼 생성

  • 매개값 - 해당 데이터 타입의 저장 개수

 

wrap() 메소드(넌다이렉트 버퍼 생성)

  • 이미 생성되어 있는 타입 별 배열을 래핑해 버퍼 생성

  • 일부 데이터만 가지고도 버퍼 생성 가능!

 

allocateDirect() 메소드(다이렉트 버퍼 생성)

  • JVM 힙 메모리 바깥쪽 (운영체제가 관리하는 메모리)에 다이렉트 버퍼   생성

  • 각 타입 별 Buffer 클래스에는 없고 ByteBuffer 에서만 제공

 

  • asXXXBuffer() 메소드(각 타입 별 다이렉트 버퍼 생성)

    – asCharBuffer(), asShortBuffer(), asIntBuffer(), asLongBuffer(), asFloatBuffer(), asDoubleBuffer()

    – 우선 다이렉트 ByteBuffer를 생성하고 호출

    – 초기  다이렉트 ByteBuffer 생성 크기에 따라 저장 용량 결정

 

 

byte 해석 순서(ByteOrder)

  • 운영체제는 두 바이트 이상을 처리할 때 처리 효율이나 CPU 디자인 상의 문제로 바이트 해석 순서를 정함

 

  • 데이터를 외부로 보내거나 외부에서 받을 때도 영향 미치기 때문에 바이트 데이터를 다루는 버퍼도 이를 고려해야 !

  • Big endian: 앞 바이트부터 먼저  처리

    Little endian: 뒤 바이트부터 먼저 처리

 

다음 예제는 현재 컴퓨터의 운영체제 종류와 바이트를 해석하는 순서에 대해서 출력한다.

import java.nio.ByteOrder;

public class ComputerByteOrderExample {
	public static void main(String[] args) {
		System.out.println("운영체제 종류 : "+System.getProperty("os.name"));
		System.out.println("네이티브의 바이트 해석 순서 :"+ByteOrder.nativeOrder());
	}
}

 

운영체제와 JVM의 바이트 해석 순서가 다를 경우에는 JVM이 운영체제와 데이터를 교환할 떄 자동적으로 처리해주기 때문에 문제는 없다.

 

하지만 다이렉트 버퍼일 경우 운영체제의 native I/O를 사용하므로 운영체제의 기본 해석 순서로 JVM의 해석 순서를 맞춰 주는 것이 성능에 도움이 된다.

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100).order(byteOrder.nativeOrder());

 

 

19.3.3 Buffer의 위치 속성

Buffer의 위치 속성 개념과 위치 속성이 언제 변경되는지 알고 있어야 한다.

Buffer의 네가지 위치 속성
position, limit, capacity, mark  속성의 크기 관계

 

 

19.3.4 Buffer 메소드

Buffer를 생성한 후 사용할때에는 Buffer가 제공하는 메소드를 잘 활용해야 한다.

Buffer마다 공통적으로 사용하는 메소드들도 있고, 데이터 타입별로 Buffer가 개별적으로 가지고 있는 메소드들도 있다.

공통 메소드: Buffer 추상 클래스에 정의된 메소드

 

데이터를 읽고 저장하는 메소드

- 데이터 읽기: get(…)

- 데이터 저장: put(…)

  • Buffer 추상 클래스에는 없고, 각 타입 별 하위 Buffer 클래스가 가짐

 

- 상대적(Relative) 메소드

  • 현재 위치 속성인 position에서 데이터를 읽고 저장

  • 상대적 get()과 put() 메소드 호출하면 position 값 증가

 

  • position 값이 limit 값까지 증가한 상태

    – 상대적 get() 사용  - BufferUnderflowException 예외 발생

    – 상대적 put() 사용 -  BufferOverflowException 예외 발생

 

- 절대적(Absolute) 메소드

  • position과 상관없이 주어진 인덱스에서 데이터 읽고 저장

  • 절대적 get()과 put() 메소드를 호출하면 position의 값은 증가되지 않음

 

- 상대적과 절대적 메소드 구분 방법

  • 상대적 메소드: index 매개값이 없는 메소드

  • 절대적 메소드: index 매개값이 있는 메소드

 

■ 버퍼 예외의 종류

- 버퍼 예외 발생 주요 원인

  • 버퍼가 다 찼을 때, 데이터를 저장하려는 경우

  • 버퍼에서 더 이상 읽어올 데이터가 없을 때 데이터를 읽으려는 경우

 

- 버퍼와 관련된 예외 클래스

 

19.3.5 Buffer 변환

채널이 데이털르 읽고 쓰는 버퍼는 모두 ByteBuffer이다.

그렇기 때문에 채널을 통해 읽은 데이털르 복원하려면 ByteBuffer를 문자열 또는 다른 타입 버퍼(CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, Doublebuffer)로 변환해야 한다.

 

반대로 문자열 또는 다른 타입 버퍼의 내용을 채널을 통해 쓰고 싶다면 ByteBuffer로 변환해야 한다.

 

ByteBuffer <--> String

프로그램에서 가장 많이 처리 되는 데이터는 String 타입, 즉 문자열이다.

채너를 통해 문자열을 파일이나 네트워크로 전ㄴ송하려면 특정 문자셋(UTF-8, EUC-KR)으로 인코딩해서 ByteBuffer로 변환해야 한다.

Charset charset = Charset.forName("UTF-8");  //매개값으로 주어진 문자셋
Charset charset = Charset.defaultCharset();  //운영체제가 사용하는 디폴트 문자셋

 

Charset을 이용해서 문자열을 ByteBuffer로 변환하려면 다음과 같이 encode() 메소드를 호출하면 된다.

String data = "";
ByteBuffer byteBuffer = charset.encode(data);

 

반대로 파일이나 네트워크로 부터 읽은 ByteBuffer가 특정 문자셋(UTF-8, EUC-KR)으로 인코딩이 되었을 경우, 해당 문자셋으로 디코딩 해야만 문자열로 복원할 수 있다.

Charset은 ByteBuffer를 디코딩해서 CharBuffer로 변환시키는 decode() 메소드를 제공하고 있기 때문에 다음과 같이 문자열로 쉽게 복원 할 수 있다.

 

다음 에제는 문자열을 UTF-8로 인코딩해서 얻은 ByteBuffer를 다시 UTF-8로 디코딩해서 문자열로 복원한다.

import java.nio.ByteBuffer;
import java.nio.charset.Charset;

public class ByteBufferToStringExample {
	public static void main(String[] args) {
		Charset charset = Charset.forName("UTF-8");
		
		//문자열 -> 인코딩 -> ByteBuffer
		String data = "안녕하세요";
		ByteBuffer byteBuffer = charset.encode(data);
		
		//ByteBuffer -> 디코딩 -> CharBuffer -> 문자열
		data = charset.decode(byteBuffer).toString();
		System.out.println("문자열 복원: " + data);
	}
}

 

ByteBuffer <--> IntBuffer

int[] 배열을 생성하고 이것을 파일이나 네트워크로 출력하기 위해서는 int[]배열 또는 IntBuffer로부터 ByteBuffer를 생성해야 한다.

 

int 타입은 4byte크기를 가지므로 int[] 배열의 크기 또는 IntBuffer의 capacity보다 4배 큰 capacity를 가진 ByteBuffer를 생성하고, ByteBuffer의 putInt() 메소드로 정수값을 하나씩 저장하면 된다.

int[] writeData = { 10, 20 };
IntBuffer writeIntBuffer = IntBuffer.wrap(writeData);
ByteBuffer writeByteBuffer= ByteBuffer.allocate(writeIntBuffer.capacity()*4);
for(int i=0; i<writeIntBuffer.capacity(); i++) {
	writeByteBuffer.putInt(writeIntBuffer.get(i));
}
writeByteBuffer.flip();   //position을 0으로 되돌림.

 

반대로 파일이나 네트워크로 부터 입력된 ByteBuffer에 4바이트씩 연속된 int 데이터가 저장되어 있을 경우, int[] 배열로 복원이 가능하다.

 

ByteBUffer의 asIntBuffer() 메소드로 IntButter를 얻고, IntBuffer의 capacity와 동일한 크기의 int[] 배열을 생성한다.

그리고 IntBuffer의 get() 메소드로 int값들을 배열에 저장하면 된다.

ByteBuffer byteBuffer = ...;
IntBuffer intBuffer = byteBuffer.asIntBuffer();
int[] data = new int[intBuffer.capacity()];
intBuffer.get(data);

 

다음 예제는 int[]배열로 부터 얻은 ByteBuffer를 이용해서 다시 int[]배열로 복원한다.

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Arrays;

public class ByteBufferToIntBufferExample {
	public static void main(String[] args) throws Exception {
		//int[] -> IntBuffer -> ByteBuffer
		int[] writeData = { 10, 20 };
		IntBuffer writeIntBuffer = IntBuffer.wrap(writeData);
		ByteBuffer writeByteBuffer= ByteBuffer.allocate(writeIntBuffer.capacity()*4);
		for(int i=0; i<writeIntBuffer.capacity(); i++) {
			writeByteBuffer.putInt(writeIntBuffer.get(i));
		}
		writeByteBuffer.flip();
		
		//ByteBuffer -> IntBuffer -> int[]
		ByteBuffer readByteBuffer = writeByteBuffer;	
		IntBuffer readIntBuffer = readByteBuffer.asIntBuffer();
		int[] readData = new int[readIntBuffer.capacity()];
		readIntBuffer.get(readData);
		System.out.println("배열 복원: " + Arrays.toString(readData));
	}
}