프로그래밍/Java

[Java] Buffer의 구조와 주요 메소드 다루기

Lou Park 2024. 3. 9. 13:52

Java NIO Buffer는 Channel과 상호작용할때 사용된다. Channel에서 Buffer로 데이터를 읽어들일 수 있고, 다시 Channel로 쓰여질 수 있다. Channel을 통하지 않고 Buffer를 직접 다룰 수도 있는데, Buffer의 구조와 주요 메소드들을 살펴보면서 알아보도록 하겠다.

Buffer의 구조

https://jenkov.com/tutorials/java-nio/buffers.html

 

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()) 

 

참고자료
https://jenkov.com/tutorials/java-nio/buffers.html