[Java] Buffer의 구조와 주요 메소드 다루기
Java NIO Buffer는 Channel과 상호작용할때 사용된다. Channel에서 Buffer로 데이터를 읽어들일 수 있고, 다시 Channel로 쓰여질 수 있다. Channel을 통하지 않고 Buffer를 직접 다룰 수도 있는데, Buffer의 구조와 주요 메소드들을 살펴보면서 알아보도록 하겠다.
Buffer의 구조
Buffer는 데이터를 읽고, 쓸 수 있는 메모리 블록이다. 그리고 커서 역할을 하는 다음 3개의 속성 값들을 가지고 있다.
- capacity: 버퍼의 사이즈
- position
- limit
Buffer는 읽기, 쓰기 모드로 나뉘어지는데 각 모드에 따라 커서가 하는 일을 살펴보자.
쓰기 모드 (Write Mode)
[ ][ ][ ][ ][ ].
⭡ ⭡
position limit, capacity
position
초기 position
은 0이다. Buffer에 데이터를 쓰면, position
은 데이터를 쓰고난 다음 셀의 Index를 가리킨다. 그러니까 position
은 Buffer에 데이터를 쓰기 시작할 위치인 것이다.
아래 도식처럼 capacity=5
의 버퍼에 hi라는 두 글자를 넣었다면 position=2
이된다.
[ h ][ i ][ ][ ][ ]
⭡
pos=2
limit
쓰기 모드에서 limit
은 Buffer에 얼마나 쓸 수 있는지를 가리킨다.
그래서 그냥 capacity
와 동일한 값을 가진다.
읽기 모드 (Read Mode)
[ h ][ i ][ ][ ][ ].
⭡ ⭡ ⭡
pos limit capacity
position
읽기 모드에서 position
은 다음 읽을 위치를 가리킨다. (0 부터 시작)
나중에 설명할테지만, flip()
메소드로 Buffer를 읽기모드로 바꾸게되면 position=0
으로 리셋되면서 Buffer의 처음부터 읽을 수 있도록 해준다.
limit
읽기 모드에서 limit은 어디까지 읽을 수 있는지를 가리킨다.
따라서 position == limit
일 경우 전부 읽은 상태다.
쓰기 모드에서 읽기 모드로 바꿨을때, 쓰기 모드의 position
이 읽기 모드의 limit
이 된다.
Buffer의 주요 메소드
allocate()
버퍼를 만들떄는 얼마나 사이즈를 할당할지 결정해야한다.allocate()
로 새로운 Buffer를 만들 수 있다.
val bufer = ByteBuffer.allocate(10)
flip()
잠깐 이야기 했지만, flip()
메소드는 Buffer를 쓰기 모드에서 읽기 모드로 바꾸어 준다.
읽기 모드가 되면 position
은 0으로 돌아가고, limit
이 원래 position자리를 대체한다.
val buffer = ByteBuffer.allocate(10)
buffer.put("hello".toByteArray()) // position = 5, limit = 10
buffer.flip() // 읽기 모드로 변경됨
println(buffer.position()) // position = 0
println(buffer.limit()) // limit = 5
rewind()
테이프로 음악들었을 시절에는 참 많이 들어본 단어인데...ㅋㅋㅋ
테이프를 생각하면 rewind()
가 왜 쓰이는지 알기 쉽다.
테이프로 음악을 듣다가 처음부터 다시 듣고싶으면 역으로 되감아야한다... 그것 밖에 방법이 없다. Buffer도 읽기를 통해서 position
이 증가하게되면, 다시 처음부터 읽어들이기 위해서는 어떻게든 position
을 0으로 만들어야 한다. 억지로 현재 position
이전 위치를 읽어들이려고하면 BufferUnderflowException이 터지게 된다.
그렇다. rewind()
는 position을 다시 0으로 만들어서 다시 Buffer의 데이터를 읽을 수 있도록 한다. (*limit은 변하지 않는다.)
val buffer = ByteBuffer.allocate(10)
buffer.put("hello".toByteArray()) // position = 5, limit = 10
buffer.flip() // 읽기 모드로 변경됨, position = 0, limit = 5
val read = ByteArray(5)
buffer.get(read, 0, 5) // position = 5, limit = 5
buffer.rewind() // position = 0, limit = 5
clear(), compact()
Buffer에서 데이터를 다 읽고나면, Buffer가 다시 쓸 수 있도록 만들어야한다.
clear()
또는 compact()
를 호출해서 그렇게 할 수 있다.
clear()
는 positoin=0
, limit=capacity
로 만든다. 즉, Buffer에 처음부터 데이터를 쓸 수 있는 상태로 만들어준다는 것이다. 하지만 Buffer안에있는 실제 데이터는 그대로 있다. 단지 커서들만 그렇게 변경될 뿐이다.
예로, "hi"라는 데이터가 들어가있는 Buffer에 clear()
를 호출할 경우 이렇게 된다.
[ h ][ i ][ ][ ][ ].
⭡ ⭡
position limit, capacity
compact()
는 아직 읽지않은 데이터를 Buffer의 처음으로 복사하는 메소드다. 그리고 position
을 아직 읽지않은 데이터 다음으로 위치시킨다. 읽지 않은 데이터를 덮어쓰지 않으면서 마저 Buffer에 쓰고 싶을때 사용할 수 있다.
"hello"를 "olleh"로 바꿔보면서 예시로 살펴보자.
Buffer안의 hello를 전부 읽진않았고 "hell" 까지만 읽어놓은 Buffer의 상태다.
[ h ][ e ][ l ][ l ][ o ]
⭡
position = 4
이 상태에서 compact()를 하게되면 Buffer의 상태는 이렇게 바뀐다.
[ o ][ ][ ][ ][ ]
⭡
position = 1
여기에다가 put()
을 사용해서 Buffer에 "lleh"라는 글자를 넣어주게되면 "olleh"가 완성된다.
[ o ][ l ][ l ][ e ][ h ].
⭡
position = 5
val buffer = ByteBuffer.allocate(10)
buffer.put("hello".toByteArray()) // pos = 5, limit = 10
buffer.flip() // 읽기 모드로 변경됨, pos = 0, limit = 5
val read = ByteArray(5)
buffer.get(read, 0, 4) // pos = 4, limit = 5
buffer.compact() // 쓰기 모드로 변경됨, pos = 1, limit = 10
buffer.put("lleh".toByteArray()) // pos = 5, limit = 10
mark(), reset()
mark()
와 reset()
은 단짝이다.mark()
메소드로 Buffer의 position
을 마킹 할 수있고 이후에 reset()
을 호출하면 Buffer의 position
이 그때로 돌아간다. 이를테면 테이프의 구간반복같은걸 구현할 수 있는 것이다.
buffer.mark()
buffer.get(array, 0, 5)
assert(array.decodeToString(), "olleh")
// position이 0으로 돌아가기때문에
// 다시 읽어도 BufferUnderflow 오류가 나지 않는다.
buffer.reset()
assert(buffer.position(), 0)
buffer.get(array, 0, 5)
assert(array.decodeToString(), "olleh")
버퍼 언더플로우, 오버플로우
Buffer를 직접 다룰때 실수로인해 볼 수 있는 오류들이 언더플로우, 오버플로우 오류다. 둘다 position
이 고삐풀려서 limit
을 벗어날때 일어난다.
BufferUnderflowException
BufferUnderflowExeption
은 Buffer 읽기 도중 발생하는 오류로, Buffer의 position
> limit
일때 발생한다.
val buffer = ByteBuffer.allocate(10)
buffer.put("hello".toByteArray())
buffer.flip()
val array = ByteArray(5)
buffer.get(array, 0, 3)
buffer.get(array, 0, 2)
// [!] BufferUnderflowException
buffer.get(array, 0, 1)
BufferOverflowException
BufferOverflowException
은 쓰기 도중 발생하는 오류로, Buffer의 position
> limit(capacity)
일때 발생한다.
val buffer = ByteBuffer.allocate(10)
// [!] BufferOverflowException
buffer.put("helloworld 앗살람말라이쿰".toByteArray())