[Java] 버퍼 변환과 파일 채널

mmy789·2022년 5월 16일
0

Java2

목록 보기
34/36
post-thumbnail

[ Buffer 변환 ]

채널이 데이터를 읽고 쓰는 버퍼는 모두 ByteBuffer이다.

그렇기 때문에 채널을 통해 읽은 데이터를 복원하려면 ByteBuffer를 문자열 또는 다른 타입 버퍼(CharBuffer, ShortBuffer, IntBuffer, LongBuffer ,FloatBuffer, DoubleBuffer)로 변환해야 한다. 반대로 문자열 또는 다른 타입 버퍼의 내용을 채널을 통해 쓰고 싶다면 ByteBuffer로 변환해야 한다.


ByteBuffer ↔ String

프로그램에서 가장 많이 처리되는 데이터는 String 타입, 즉 문자열이다. 채널을 통해 문자열을 파일이나 네트워크로 전송하려면 특정 문자셋(UTF-8, EUC-KR)으로 인코딩해서 ByteBuffer로 변환해야 한다.

  1. 먼저 문자셋을 표현하는 java.nio.charset.Charset) 객체가 필요하다.
Charset charset = Charset.forName("UTF-8");	//매개값으로 주어진 문자셋
Charset charset = Charset.defaulCharset();	//운영체제가 사용하는 디폴트 문자셋
  1. Charset을 이용해서 문자열을 ByteBuffer로 변환하려면 다음과 같이 encode() 메소드를 호출하면 된다.
String data = ...;
ByteBuffer byteBuffer = charset.encode(data);

반대로 파일이나 네트워크로부터 읽은 ByteBuffer가 특정 문자셋(UTF-8, EUC-KR)으로 인코딩되어 있을 경우, 해당 문자셋으로 디코딩해야만 문자열로 복원할 수 있다. CharsetByteBuffer를 디코딩해서 CharBuffer로 변환시키는 decode() 메소드를 제공하고 있기 때문에 다음과 같이 문자열로 쉽게 복원할 수 있다.

ByteBuffer byteBuffer = ...;
String data = charset.decode(byteBuffer).toString();

ByteBuffer ↔ IntBuffer

int[] 배열을 생성하고 이것을 파일이나 네트워크로 출력하기 위해서는 int[] 배열 또는 IntBuffer로부터 ByteBuffer를 생성해야 한다. int 타입은 4byte 크기를 가지므로 int[] 배열 크기 또는 IntBuffercapacity보다 4배 큰 capacity를 가진 ByteBuffer를 생성하고, ByteBufferputInt() 메소드로 정수값을 하나씩 저장하면 된다.

다음은 int[] 배열을 IntBuffer로 래핑하고, 4배 큰 ByteBuffer를 생성한 후 정수값을 저장한다. putInt() 메소드는 position을 이동시키기 때문에 모두 저장한 후에는 position0으로 되돌려 놓는 flip() 메소드를 호출해야 한다.

int[] data = new int[] { 10, 20 };
IntBuffer intBuffer = IntBuffer.wrap(data);
ByteBuffer byteBuffer = ByteBuffer.allocate(intBuffer.capacity() * 4);
for (int i = 0; i < intBuffer.capacity(); i++) {
    byteBuffer.putInt(intBuffer.get(i));
}
byteBuffer.flip();

반대로 파일이나 네트워크로부터 입력된 ByteBuffer에 4바이트씩 연속된 int 데이터가 저장되어 있을 경우, int[] 배열로 복원이 가능하다. ByteBufferasIntBuffer() 메소드로 IntBuffer를 얻고, IntBuffercapacity와 동일한 크기의 int[] 배열을 생성한다. 그리고 IntBufferget() 메소드로 int 값들을 배열에 저장하면 된다.

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

🚫 주의

array() 메소드는 래핑한 배열만 리턴하기 때문에 int[] 배열을 wrap() 메소드로 래핑한 IntBuffer에서만 사용할 수 있다! 그러므로 ByteBuffer에서 asIntBuffer()로 얻은 IntBuffer에서는 array() 메소드를 사용해서 int[] 배열을 얻을 수 없다.

ByteBufferIntBuffer 간의 변환처럼 ByteBufferShortBuffer, LongBuffer, FloatBuffer, DoubleBuffer 간의 변환도 비슷한 방법으로 하면 된다.

✅ ByteBuffer가 가지고 있는 기본 타입 버퍼로 변환하는 asXXXBuffer() 메소드

리턴 타입변환 메소드설명
ShortBufferasShortBuffer()2바이트씩 연속된 short 데이터를 가진 ByteBuffer일 경우
IntBufferasIntBuffer()4바이트씩 연속된 int 데이터를 가진 ByteBuffer일 경우
LongBufferasLongBuffer()8바이트씩 연속된 long 데이터를 가진 ByteBuffer일 경우
FloatBufferasFloatBuffer()4바이트씩 연속된 float 데이터를 가진 ByteBuffer일 경우
DoubleBufferasDoubleBuffer()8바이트씩 연속된 double 데이터를 가진 ByteBuffer일 경우

[ 파일 채널 ]

java.nio.channels.FileChannel을 이용하면 파일 읽기/쓰기를 할 수 있다.

FileChannel은 동기화 처리가 되어있기 때문에 멀티 스레드 환경에서 사용해도 안전하다.


1. FileChannel 생성과 닫기

FileChannel은 정적 메소드인 open()을 호출해서 얻을 수도 있지만, IO의 FileInputStream, FileOutputStreamgetChannel() 메소드를 호출해서 얻을 수도 있다.

다음은 open() 메소드로 FileChannel을 얻는 방법을 보여준다.

FileChannel fileChannel = FileChannel.open(Path path, OpenOption... options);
  • 첫 번째 path 매개값은 열거나, 생성하고자 하는 파일의 경로를 Path 객체로 생성해서 지정하면 된다.
  • 두 번째 options 매개값은 열기 옵션 값으로, StandardOpenOption의 다음 열거 상수를 나열해주면 된다.
열거 상수설명
READ읽기용으로 파일을 연다.
WRITE쓰기용으로 파일을 연다.
CREATE파일이 없다면 새 파일을 생성한다.
CREATE_NEW새 파일을 만든다. 이미 파일이 있으면 예외와 함께 실패한다.
APPEND파일 끝에 데이터를 추가한다.(WRITE나 CREATE와 함께 사용됨)
DELETE_ON_CLOSE채널을 닫을 때 파일을 삭제한다.(임시 파일을 삭제할 때 사용)
TRUNCATE_EXISTING파일을 0바이트로 잘라낸다.(WRITE 옵션과 함께 사용됨)

예를 들어 "C:\Temp\file.txt" 파일을 생성하고, 어떤 내용을 쓰고 싶다면 다음과 같이 매개값을 지정하면 된다.

FileChannel fileChannel = FileChannel.open(
	Paths.get("C:/Temp/file.txt"),
    StandardOpenOption.CREATE_NEW,
    StandardOpenOption.WRITE
);  

다음은 "C:\Temp\file.txt" 파일을 읽고, 쓸 수 있도록 FileChannel을 생성한다.

FileChannel fileChannel = FileChannel.open(
	Paths.get("C:/Temp/file.txt"),
    StandardOpenOption.READ,
    StandardOpenOption.WRITE
);

FileChannel을 더 이상 이용하지 않을 경우에는 다음과 같이 close() 메소드를 호출해서 닫아주어야 한다.

fileChannel.close();

2. 파일 쓰기와 읽기

파일에 바이트를 쓰려면 FileChannelwrite() 메소드를 호출하면 된다.

매개값으로 ByteBuffer 객체를 주면 되는데, 파일에 쓰여지는 바이트는 ByteBufferposition부터 limit까지다. position0이고 limitcapacity와 동일하다면 ByteBuffer의 모든 바이트가 파일에 쓰여진다. write() 메소드의 리턴값은 ByteBuffer에서 파일로 쓰여진 바이트 수이다.

int bytesCount = fileChannel.write(ByteBuffer src);

파일로부터 바이트를 읽기 위해서는 FileChannelread() 메소드를 호출하면 된다. 매개값으로 ByteBuffer 객체를 주면 되는데, 파일에 읽혀지는 바이트는 ByteBufferposition부터 저장된다. position0이면 ByteBuffer의 첫 바이트로부터 저장된다. read() 메소드의 리턴값은 파일에서 ByteBuffer로 읽혀진 바이트 수이다. 한 번 읽을 수 있는 최대 바이트수는 ByteBuffercapacity까지이므로 리턴되는 최댓값은 capacity가 된다. 더 이상 읽을 바이트가 없다면 read() 메소드는 -1을 리턴한다.

int bytesCount = fileChannel.read(ByteBuffer dst);

버퍼에 한 바이트를 저장할 때마다 position1씩 증가하게 되는데, read() 메소드가 -1을 리턴할 때까지 버퍼에 저장한 마지막 바이트의 위치는 position - 1 인덱스이다.


[ 참고자료 ]

이것이 자바다 책

profile
Backend Developer

0개의 댓글