elevne's Study Note
Java - NIO (2) 본문
NIO 에서는 데이터를 입출력하기 위해 항상 Buffer 을 사용하게된다. Buffer 은 읽기, 쓰기가 가능한 메모리 배열로, 저장되는 데이터 타입에 따라서 분류될 수도, 어떤 메모리를 사용하느냐에 따라 다이렉트와 논다이렉트 버퍼로 분류될 수도 있다. 우선 아래와 같이 저장되는 데이터 타입에 따라 별도의 Buffer 클래스가 제공된다. (모두 Buffer 추상클래스를 상속한다)
버퍼는 메모리의 위치에 따라서 논다이렉트와 다이렉트 버퍼로 분류되기도 한다. 논다이렉트 버퍼는 JVM 이 관리하는 힙 메모리 공간을 이용하는 버퍼이고, 다이렉트 버퍼는 운영체제가 관리하는 메모리 공간을 이용하는 버퍼이다. 두 버퍼는 아래와 같은 특징을 지닌다.
구분 | 논다이렉트 버퍼 | 다이렉트 버퍼 |
사용하는 메모리 공간 | JVM 힙 메모리 | 운영체제 메모리 |
버퍼 생성 시간 | 생성이 빠름 | 생성이 느림 |
버퍼의 크기 | 작음 | 큼 |
입출력 성능 | 낮다 | 높다 |
논다이렉트 버퍼는 JVM 힙 메모리를 사용하기 때문에 생성은 빠르지만, 다이렉트 버퍼는 운영체제의 메모리를 할당받기 위해 운영체제의 Native C 함수를 호출해야 하고, 여러가지 잡다한 처리를 해야해서 상대적으로 버퍼 생성이 느리다고 한다. 이러한 이유로 다이렉트 버퍼는 자주 생성하기 보다는 한 번 생성해두고 재사용하는 것이 적합하다고 한다.
논다이렉트 버퍼는 입출력을 하기 위해 임시 다이렉트 버퍼를 생성하고 논다이렉트 버퍼에 있는 내용을 임시 다이렉트 버퍼에 복사한다. 그 후 임시 다이렉트 버퍼를 이용하여 운영체제의 Native I/O 기능을 수행하기에 직접 다이렉트 버퍼를 사용하는 것보다는 입출력 성능이 낮다고 한다.
또, 다이렉트 버퍼는 채널을 사용해서 버퍼의 데이터를 읽고 저장하는 경우에만 운영체제의 native I/O 를 수행한다. 만약 채널을 사용하지 않고 ByteBuffer 의 get()/put() 메소드를 사용해서 버퍼의 데이터를 읽고 저장한다면 해당 작업은 내부적으로 JNI (Java Native Interface: 자바 코드에서 C 함수를 호출할 수 있도록 하는 API) 를 호출해서 native I/O 를 수행하기 때문에 JNI 호출이라는 오버 헤더가 추가되어 오히려 논다이렉트 버퍼의 get()/put() 메소드 성능이 더 좋게 나올 수 있다고 한다.
각 데이터 타입별로 논다이렉트 버퍼를 생성하기 위해서는 각 Buffer 클래스의 allocate(), wrap() 메소드를, 다이렉트 버퍼를 생성하기 위해서는 ByteBuffer 의 allocateDirect() 메소드를 호출하면 된다.
데이터를 처리할 때 바이트 처리 순서는 운영체제마다 차이가 있다. 이러한 차이는 데이터를 다른 운영체제로 보내거나 받을 때 영향을 미치기 때문에 데이터를 다루는 버퍼도 이를 고려해야 한다고 한다. 앞쪽 바이트부터 처리하는 것을 Big Endian, 뒤쪽 바이트부터 처리하는 것을 Little Endian 이라 한다. Little Endian 으로 동작하는 운영체제에서 만든 데이터 파일을 Big Endian 으로 동작하는 운영체제에서 읽는다면 ByteOrder 클래스로 데이터 순서를 맞춰줘야 한다. ByteOrder 클래스의 nativeOrder() 메소드는 현재 동작하고 있는 운영체제가 Big Endian 인지 Little Endian 인지 알려준다. JVM 도 일종의 독립된 운영체제로 이러한 문제를 취급하는데, JRE 가 설치된 어떤 환경이든 JVM 은 무조건 Big Endian 으로 동작하도록 되어있다고 한다.
Buffer 의 위치 속성 개념과 위치 속성이 언제 변경되는지 알아야 한다.
속성 | 설명 |
position | 현재 읽거나 쓰는 위치값. 인덱스 값으로 0 부터 시작하며, limit 보다 큰 값을 가질 수 없다. 만약 position 과 limit 의 값이 같아진다면 더 이상 데이터를 쓰거나 읽을 수 없다는 뜻. |
limit | 버퍼에서 읽거나 쓸 수 있는 위치의 한계. 이 값은 capacity 보다 작거나 같은 값을 가진다. 최초에 버퍼를 만들었을 때는 capacity 와 같은 값을 가진다. |
capacity | 버퍼의 최대 데이터 개수(메모리 크기). 인덱스 값이 아니라 수량이다. |
mark | reset() 메소드 실행 시 돌아오는 위치를 지정하는 인덱스로 mark() 메소드로 지정할 수 있다. position 이하의 값으로 지정해야한다. position 이나 limit 의 값이 mark 값보다 작으면 mark 는 자동 제거됨. mark 가 없는 상태에서 reset() 메소드 호출 시 InvalidMarkException 이 발생함. |
이러한 버퍼에 데이터를 저장할 때는 put(), 읽을 때는 get() 메소드를 사용한다. 이 메소드들은 Buffer 추상클래스에는 없고 각 타입별 하위 Buffer 클래스가 가지고있다. 아래와 같이 예제 코드를 작성해볼 수 있다.
package nio;
import java.nio.Buffer;
import java.nio.ByteBuffer;
public class BufferExample {
public static void main(String[] args) throws Exception {
System.out.println("[7 바이트 크기로 버퍼 생성하기]");
ByteBuffer buffer = ByteBuffer.allocateDirect(7);
printState(buffer);
buffer.put((byte) 10);
buffer.put((byte) 11);
System.out.println("[2 바이트 저장 후]");
printState(buffer);
buffer.put((byte) 12);
buffer.put((byte) 13);
buffer.put((byte) 14);
System.out.println("[3 바이트 저장 후]");
printState(buffer);
// limit 을 position 으로, position 을 0 인덱스로 이동
buffer.flip();
System.out.println("[flip 실행 후]");
printState(buffer);
buffer.get(new byte[3]);
System.out.println("[3 바이트 읽은 후]");
printState(buffer);
buffer.mark();
System.out.println("[현재 위치 마크]");
buffer.get(new byte[2]);
System.out.println("[2 바이트 읽은 후]");
printState(buffer);
buffer.reset();
System.out.println("---------[position 을 마크 위치로 옮김]");
printState(buffer);
// position 을 0 으로 이동
buffer.rewind();
System.out.println("[rewind() 실행 후]");
printState(buffer);
// 모든 위치 속성값 초기화
buffer.clear();
System.out.println("[clear() 실행 후]");
printState(buffer);
}
public static void printState(Buffer buffer){
System.out.println("\tPOSITION: "+buffer.position()+",");
System.out.println("\tLIMIT: "+buffer.limit()+",");
System.out.println("\tCAPACITY: "+buffer.capacity());
}
}
채널이 데이터를 읽고 쓰는 버퍼는 모두 ByteBuffer 이다. 채널을 통해 읽은 데이터를 복원하기 위해서는 ByteBuffer 을 문자열 또는 다른 버퍼 타입으로 변환해야 한다. 반대로 문자열 또는 다른 타입 버퍼의 내용을 채널을 통해 쓰고 싶다면 ByteBufer 로 변환해야 한다.
String 타입, 문자열을 채널을 통해 파일이나 네트워크로 전송하려면 특정 문자셋 (UTF-8, EUC-KR 등) 으로 인코딩해서 ByteBuffer 로 변환해야 한다. 먼저 문자셋을 표현하는 java.nio.charset.Charset 객체가 필요하다. 해당 객체의 encode() 메소드를 호출하여 문자열을 인코딩해준다. 이와 반대로 파일이나 네트워크로부터 읽은 ByteBuffer 이 특정 문자셋으로 인코딩되어 있을 경우, 해당 문자셋으로 디코딩해야만 문자열로 복원할 수 있다. 이 때는 Charset 의 decode() 메소드가 사용된다.
public static void main(String[] args) throws Exception {
Charset charset = Charset.forName("EUC-KR");
String data = "이터널 선샤인~";
ByteBuffer buffer = charset.encode(data);
data = charset.decode(buffer).toString();
System.out.println("복원된 문자열: "+data);
}
int[] 배열을 생성하고 이를 파일이나 네트워크로 출력하기 위해서는 int[] 배열 또는 IntBuffer 로부터 ByteBuffer 을 생성해야 한다. 아래와 같이 작성해볼 수 있다.
public static void main(String[] args) throws Exception {
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 readByteBuffer = writeByteBuffer;
IntBuffer readIntBuffer = readByteBuffer.asIntBuffer();
int[] readData = new int[readIntBuffer.capacity()];
readIntBuffer.get(readData);
System.out.println("배열 복원: "+ Arrays.toString(readData));
}
Reference:
이것이 자바다
'Backend > Java' 카테고리의 다른 글
Java - CompletableFuture (0) | 2023.06.15 |
---|---|
Java - NIO (3) (0) | 2023.05.30 |
Java - NIO (1) (0) | 2023.05.28 |
Java - Reflection (1) | 2023.05.27 |
Java - Socket(UDP) (0) | 2023.05.26 |