기존 IO의 2가지 비 효율적인 부분
1. 커널 영역 버퍼에서 프로세스 영역 안의 버퍼로 데이터를 복사
[디스크]에서 [커널 영역 버퍼]로 데이터를 저장하는 것은 디스크 컨트롤러가 DMA 기술을 사용하기 때문에 CPU를 사용 하지 않는다. 하지만 [커널 영역]에서 [프로세스 영역 버퍼]으로 데이터를 전달하는 것은 CPU를 사용한다. 만약 커널 영역의 버퍼에 저장된 데이터를 직접 사용한다면 복사하는 시간을 단축 할 수 있고, 복사 대상인 데이터의 가비지 컬렉션도 필요 없다. 그리고 CPU자원도 최소화 할 수 있다.
2. 디스크 컨트롤러에서 커널 영역의 버퍼로 데이터를 복사하는 동안 프로세스 영역의 블록킹
운영체제는 효율을 높이기 위해 최대한 많은 양의 데이터를 커널 영역의 버퍼에 저장한 후 프로세스 영역의 버퍼로 전달한다. 따라서 디스크의 파일 데이터를 커널 영역 안의 버퍼로 모두 복사할 때 까지 자바 프로세스가 블록킹 된다. 좀 더 정확하게는 파일 읽기를 요청한 자바 스레드가 블록킹 된다.
요약하면 OS에서 관리하는 커널 버퍼에 직접 접근할 수 없고, 기본의 IO는 Blocking I/O여서 매우 비효율적이었다는 것이다. 커널에서 JVM내부 메모리(사용자 프로세스 영역)로 복사한는 의미는 디스크에서 커널로 데이터를 읽어들이는 것은 CPU의 개입없이 DMA가 처리해주지만 커널에서 사용자 프로세스 내부로 데이터를 복사하는 것은 CPU를 포함한 많은 Resource의 사용 댓가가 필요하다. 또 복사용으로 사용된 Buffer 활용 후 GC 대상이 되므로 이것 또한 부담으로 남는다.
NIO에서 도입된 3가지 기술
버퍼(Buffer)
NIO에서 Buffer클래스를 도입했다. 1.4이전 버전에서 모든 메모리는 JVM 힙 영역을 통해서 관리했다. 결국, IO 과정에서 항상 비효율적인 보사 과장을 거치고, 블록킹이 된다. 하지만 1.4 부터는 시스템 메모리를 직접 사용할 수 있는 Buffer 클래스가 도입 되었다(물론, DirectByteBuffer에 한정된 것이지만).
채널(Channel)
NIO에서는 스트림의 향상된 버전인 Channel을 도입했다. 채널은 스트림처럼 읽거나 쓰는 단방향에서부터, 읽고 쓰는 양방향 통신이 가능한 세가지 형식이 존재한다. ㄸㅎ한 운영체제에서 제공해주는 다양한 네이티브IO 서비스들을 이용할 수 있게 해준다.
셀럭터(Selector)
셀럭터는 네트워크 프로그래밍의 효율을 높이기 위한 것이다. 클라이언트 하나당 스레드 하나를 생성해서 처리해야 했는데, 사용자가 늘어나면 스레드가 많이 생성됨으로 인해 급격한 성능 저하를 가져왔다. 따라서 셀렉터는 하나의 스레드에서 다수의 동시 사용자를 처리할 수 있는 기술이다.
NIO에서는 커널 버퍼에 직접 접근할 수 있는 클래스를 제공해주는데. Buffer클래스들이 그것인데, 내부적으로 커널버퍼를 직접 참조하도록 하고있다. 운영체제가 제공해주는 효율적인 I/O 핸들링 서비스를 이용 할 수 있게 해주며 위에서 발생한 복사문제로 인해 CPU자원의 비효율성, I/O 요청 Thread가 Blocking 되는 문제점 등이 해결될 수 있다.
위 그림을 살펴보면 Buffer에는 여러가지 자료형을 지원하는데Direct Buffer는 ByteBuffer만 지원한다. 따라서 커널 버퍼를 직접 사용하고 싶다면 불편하더라도 ByteBuffer만 사용해야 한다. Buffer를 만드는 방법은 다음과 같다.
1. ByteBuffer buf = ByteBuffer.allocate(SIZE);
2. ByteBuffer directBuf = ByteBuffer.allocateDirect(SIZE);
아랫줄의 코드와 같이 사용하여야 커널 버퍼를 직접 이용하는 것이며. 윗 줄은 기존 방식과 같은 것으로 JVM내부에 메모리가 할당 된다.
ByteBuffer
ByteBuffer의 중요한 속성
1. pos
pos는 현재 버퍼 포인터의 위치이고, lim은 버퍼에 저장할수 있는 최대 량, 끝 이라고 생각하면 된다. cap은 원래 수용할 수 있는 최대 공간이다. pos는 put을 통해 데이터를 입력할때마다 데이터 개수만큼 뒤로 가서 새로은 pos를 가르키게 된다.
예를 들어 put(4바이트) 를 하게되면 포스는 4를 가르키게 된다.
2. lim
lim은 현재 입력받을 수 있는 최대 buffer 양이다. buffer.limit() 을통해 현재 limit값을 리턴받을수도있고 buffer.limit(int num) 을 통해 원하는 값으로 lim을 수정할 수도 있다.
이 lim을 초과하는 put이 들어오면 에러를 뿜어낸다. 현재의 버퍼 크기라고 생각하면 된다.
3. cap
버퍼의 절대적인 수치이다. 처음 선언시 만들어 주는 크기이며 변화 시킬 수 없는 값이다. lim역시 당연히 cap의 크기를 넘어 설수는 없다.
ByteBuffer의 중요한 Method
1. clear()
해당 ByteBuffer instance 가 생성되었을 때 상태로 만들어 준다.
2. limit()
쓰기 가능 영역 크기를 조정할 수 있다. 파라미터로 넘기는 값이 allocate 로 지정한 capacity 를 초과할 수 없다.
3. flip()
filp 메서드를 사용하게 되면 lim = pos 가 되고 pos= 0 이 된다. 이것이 어떤 의미를 가지게 되냐면 버퍼 내용을 프린터할때 lim 까지 프린터 할수도 있고, 그리고 get을 통해 byteArray로 옮기게 될때 pos부터 lim까지의 양을 (유효한 양) 을 옮길 수 있다.
즉 filp은 보통 쓰기와, 읽기를 오갈때 써준다고 생각하면 된다. 다 쓰고 나면 flip -> 읽기 하는 방식으로 사용한다.
4. put*()
put* 메소드들을 이용해서 데이터를 ByteBuffer 에 넣어준다.
5. get*()
원하는 데이터 크기만큼 데이터를 읽을 수 있다. get* 메소드를 이용해서 데이터를 읽으면 다음 get* 함수 이용시 이전 get* 메소드를 이용해 읽은 다음 부분부터 값을 읽는다.
6. put*()
값을 쓰거나 SocketChannel 의 read 함수로 ByteBuffer instance 에 값을 쓰고 나서 데이터를 읽기 전에 ByteBuffer 클래스의 flip 함수를 한번 호출해야 한다.
7. compact()
compact메소드 socket 통신의 경우 buffer 에 읽을 수 있는 양이 찰 때까지 read 함수로 buffer 를 채우고 buffer 가 충분히 차면 의미있는 범위까지 데이터를 읽고 buffer 의 남은 부분을 유지하고 남은 부분 다음부터 데이터를 채우는 작업을 반복하는 경우가 있다.
이때 데이터의 남은 부분을 buffer 의 앞으로 이동시키고 남은 부분 다음부터 쓸 수 있게 하는 메소드가가 compact 메소드이다
'Java > Input/Output Facilities' 카테고리의 다른 글
[NIO] 디렉토리 모니터링 API(WatchService) (0) | 2013.11.14 |
---|---|
[NIO] Stream 파일복사 vs Buffer/Channel 파일복사 (0) | 2013.11.14 |
Filter Stream과 Decorator 패턴 (0) | 2013.11.11 |
바이트 Stream과 문자 스트림 (0) | 2013.11.11 |
[바이트 스트림] 데이터 압축 및 해제 - GZIPOutputStream & GZIPInputStream 사용 (0) | 2013.02.11 |