ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] NIO 기반 입출력 및 네트워킹 - 버퍼(Buffer)
    CSE/Java 2015. 9. 1. 14:50
    NIO 기반 입출력 및 네트워킹은 여러 절로 구성되어 있습니다.




    3. 버퍼

     NIO 에서는 데이터를 입출력하기 위해 항상 버퍼를 사용해야 합니다.


     버퍼는 읽고 쓰기가 가능한 메모리 배열입니다. 


     버퍼를 이해하고 잘 사용할 수 있어야 NIO에서 제공하는 API를 올바르게 활용할 수 있습니다.










     3.1 버퍼 종류

      버퍼(Buffer)는 저장되는 데이터 타입에 따라 분류될 수 있고, 어떤 메모리를 사용하느냐에 따라 다이렉트(Direct)와 넌다이렉트(NonDirect)로 분류할 수도 있습니다.



      데이터 타입에 따른 버퍼


     NIO 버퍼는 저장되는 데이터 타입에 따라서 별도의 클래스로 제공됩니다. 이 버퍼 클래스들은 Buffer 추상 클래스를 모두 상속하고 있습니다.







     버퍼 클래스의 이름을 보면 어떤 데이터가 저장되는 버퍼인지 쉽게 알 수 있습니다.


     그 중 MappedByteBuffer는 ByteBuffer의 하위 클래스로 파일의 내용에 랜덤하게 접근하기 위해서 파일의 내용을 메모리와 맵핑시킨 버퍼입니다.




       넌다이렉트와 다이렉트 버퍼


      버퍼가 사용하는 메모리의 위치에 따라서 넌다이렉트 버퍼와 다이렉트 버퍼로 분류됩니다. 


      넌다이렉트 버퍼는 JVM이 관리하는 힙 메모리 공간을 이용하는 버퍼이고, 다이렉트 버퍼는 운영체제가 관리하는 메모리 공간을 이용하는 버퍼입니다.


      두 버퍼의 특징은 아래와 같습니다.



    구분 

     넌다이렉트 버퍼 

     다이렉트 버퍼 

      사용하는 메모리 공간 

      JVM의 힙 메모리 

      운영체제의 메모리 

      버퍼 생성 시간 

      버퍼 생성이 빠르다 

      버퍼 생성이 느리다 

      버퍼의 크기 

      작다 

      크다 

      입출력 성능 

      낮다 

      높다 




     넌다이렉트 버퍼는 JVM 힙 메모리를 사용하므로 버퍼 생성 시간이 빠르지만, 다이렉트 버퍼는 운영체제의 메모리를 할당받기 위해 운영체제의 네이티브 C 함수를 호출해야 하고 여러 가지 잡다한 처리를 해야 하므로 상대적으로 버퍼 생성이 느립니다.


     그렇기 때문에 다이렉트 버퍼는 자주 생성하기 보다는 한 번 생성해 놓고 재사용하는 것이 적합합니다. 넌다이렉트 버퍼는 JVM의 제한된 힙 메모리를 사용하므로 버퍼의 크기를 크게 잡을 수가 없고, 반면 다이렉트 버퍼는 운영체제가 관리하는 메모리를 사용하므로 운영체제가 허용하는 범위 내에서 대용량 버퍼를 생성시킬 수 있습니다.


     넌다이렉트 버퍼는 입출력을 하기 위해 임시 다이렉트 버퍼를 생성하고 넌 다이렉트 버퍼에 있는 내용을 임시 다이렉트 버퍼에 복사합니다. 그리고 나서 임시 다이렉트 버퍼를 사용해서 운영체제의 native I/O 기능을 수행합니다. 그렇기 때문에 직접 다이렉트 버퍼를 사용하는 것보다 입출력 성능이 낮습니다.


     다음 예제는 넌다이렉트 버퍼와 다이렉트 버퍼의 크기 비교 예제입니다. 



     * BufferSizeExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    package pathexam;
     
    import java.nio.ByteBuffer;
     
    public class BufferSizeExample {
        public static void main(String[] args) {
            ByteBuffer directBuffer = ByteBuffer.allocateDirect(800 * 1024 * 1024);
            System.out.println("다이렉트 버퍼가 생성되었습니다.");
            
            ByteBuffer nonDirectBuffer = ByteBuffer.allocate(800 * 1024 * 1024);
            System.out.println("넌다이렉트 버퍼가 생성되었습니다.");
            
        }
     
    }
     
    cs











      다음 예제는 넌다이렉트 버퍼와 다이렉트 버퍼의 성능 테스트 결과를 출력합니다. 이미지 파일을 100번 복사하는데 걸리는 시간을 측정합니다.



     * PerformanceExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
     
    package pathexam;
     
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    import java.util.EnumSet;
     
    public class PerformanceExample {
        public static void main(String[] args) throws Exception {
            Path from = Paths.get("C:/r_temp/IMG_1103.jpg");
            Path to1 = Paths.get("C:/r_temp/IMG_1104.jpg");
            Path to2 = Paths.get("C:/r_temp/IMG_1105.jpg");
     
            long size = Files.size(from);
     
            FileChannel fileChannel_from = FileChannel.open(from);
            FileChannel fileChannel_to1 = FileChannel.open(to1,
                    EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));
            FileChannel fileChannel_to2 = FileChannel.open(to2,
                    EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));
     
            ByteBuffer nonDirectBuffer = ByteBuffer.allocate((int) size);
            ByteBuffer directBuffer = ByteBuffer.allocateDirect((int) size);
     
            long start, end;
     
            start = System.nanoTime();
     
            for (int i = 0; i < 100; i++) {
                fileChannel_from.read(nonDirectBuffer);
                nonDirectBuffer.flip();
                fileChannel_to1.write(nonDirectBuffer);
                nonDirectBuffer.clear();
            }
     
            end = System.nanoTime();
     
            System.out.println("넌 다이렉트: " + (end - start) + " ns");
     
            start = System.nanoTime();
     
            for (int i = 0; i < 100; i++) {
                fileChannel_from.read(directBuffer);
                directBuffer.flip();
                fileChannel_to2.write(directBuffer);
                directBuffer.clear();
            }
     
            end = System.nanoTime();
            System.out.println("다이렉트:\t: " + (end - start) + " ns");
     
            fileChannel_from.close();
            fileChannel_to1.close();
            fileChannel_to2.close();
        }
     
    }
     
    cs








      다이렉트 버퍼는 채널(Channel)을 사용해서 버퍼의 데이터를 읽고 저장할 경우에만 운영체제의 native I/O를 수행합니다. 만약 채널을 사용하지 않고 ByteBuffer 의 get() / put() 메소드를 사용해서 버퍼의 데이터를 읽고 저장한다면 이 작업은 내부적으로 JNI를 호출해서 native I/O를 수행하기 때문에 JNI 호출이라는 오버 헤더가 추가됩니다.


      그렇기 때문에 오히러 넌다이렉트버퍼의 get()/put() 메소드 성능이 더 좋게 나올 수도 있습니다. 참고로 JNI(Java Native Interface)는 자바 코드에서 C 함수를 호출할 수 있도록 해주는 API 입니다.




     


     3.2 버퍼 생성

      각 데이터 타입별로 넌다이렉트 버퍼를 생성하기 위해서는 각 Buffer 클래스의 allocate() 와 wrap() 메소드를 호출하면 되고, 다이렉트 버퍼는 ByteBuffer의 allocateDirect() 메소드를 호출하면 됩니다.



       allocate() 메소드


     allocate() 메소드는 JVM 힙 메모리에 넌다이렉트 버퍼를 생성합니다.


     다음은 데이터 타입별로 Buffer를 생성하는 allocate() 메소드입니다. 파라미터는 해당 데이터 타입의 저장 개수를 말합니다.





      리턴 타입 

      메소드(파라미터) 

      설명 

     ByteBuffer 

     ByteBuffer.allocate(int capacity) 

     capacity개 만큼의 byte 값을 저장 

     CharBuffer

     CharBuffer.allocate(int capacity)

     capacity개 만큼의 char 값을 저장

     DoubleBuffer

     DoubleBuffer.allocate(int capacity)

     capacity개 만큼의 double 값을 저장

     FloatBuffer

     FloatBuffer.allocate(int capacity)

     capacity개 만큼의 float 값을 저장

     IntBuffer

     IntBuffer.allocate(int capacity)

     capacity개 만큼의 int 값을 저장

     LongBuffer

     LongBuffer.allocate(int capacity)

     capacity개 만큼의 long 값을 저장

     ShortBuffer

     ShortBuffer.allocate(int capacity)

     capacity개 만큼의 short 값을 저장



      다음은 최대 100개의 바이트를 저장하는 ByteBuffer를 생성하고, 최대 100개의 문자를 저장하는 CharBuffer를 생성하는 코드입니다.


    1
    2
    3
    4
    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
     
    CharBuffer charBuffer = CharBuffer.allocate(100);
     
    cs




       wrap() 메소드


     각 타입별 Buffer 클래스는 모두 wrap() 메소드를 가지고 있는데, wrap() 메소드는 이미 생성되어 있는 자바 배열을 래핑하여 Buffer 객체를 생성합니다.


     자바 배열은 JVM 힙 메모리에 생성되므로 wrap()은 넌다이렉트 버퍼를 생성합니다. 


     다음은 길이가 100인 byte[]를 이용해서 ByteBuffer를 생성하고, 길이가 100인 char[]를 이용해서 CharBuffer를 생성합니다.


    1
    2
    3
    4
    5
    6
    byte[] byteArray = new byte[100];
    ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
     
    char[] charArray = new char[100];
    CharBuffer charBuffer = CharBuffer.wrap(charArray);
     
    cs



     배열의 모든 데이터가 아니라 일부 데이터만 가지고 Buffer 객체를 생성할 수도 있습니다. 


     이 경우 시작 인덱스와 길이를 추가적으로 지정하면 됩니다. 다음은 0 인덱스부터 50개만 버퍼로 생성합니다.



    1
    2
    3
    byte[] byteArray = new byte[100];
    ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray, 050);
     
    cs




     CharBuffer는 추가적으로 CharSequence 타입의 파라미터를 갖는 wrap() 메소드를 제공합니다.


     String이 CharSequence 인터페이스를 구현했기 때문에 파라미터 값으로 문자열을 제공해서 다음과 같이 CharBuffer를 생성할 수도 있습니다.



    1
    2
    CharBuffer charBuffer = CharBuffer.wrap("NIO I/O");
     
    cs






       allocateDirect() 메소드


     ByteBuffer의 allocateDirect() 메소드는 JVM 힙 메모리 바깥쪽, 즉 운영체제가 관리하는 메모리에 다이렉트 버퍼를 생성합니다. 


     이 메소드는 각 타입별 Buffer 클래스에는 없고, ByteBuffer 에서만 제공됩니다.


     각 타입별로 다이렉트 버퍼를 생성하고 싶다면 우선 ByteBuffer의 allocateDirect() 메소드로 버퍼를 생성한 다음 ByteBuffer()의 asCharBuffer(), asShortBuffer() 등의 as를 접두사에 붙여서 해당 타입별 Buffer를 얻으면 됩니다.



     다음 예제는 각 다이렉트 버퍼 저장 용량 확인을 위한 예제입니다.


     * DirectBufferCapacityExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     
    package pathexam;
     
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.IntBuffer;
     
    public class DirectBufferCapacityExample {
     
        public static void main(String[] args) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);
            System.out.println("byte 저장 용량: " + byteBuffer.capacity());
     
            CharBuffer charBuffer = ByteBuffer.allocateDirect(100).asCharBuffer();
            System.out.println("char 저장 용량: " + charBuffer.capacity());
            
            IntBuffer intBuffer = ByteBuffer.allocateDirect(100).asIntBuffer();
            System.out.println("int 저장 용량: " + intBuffer.capacity());
        }
     
    }
     
    cs











       byte 해석 순서(ByteOrder)


     데이터를 처리할 때 바이트 처리 순서는 운영체제마다 차이가 있습니다. 이러한 차이는 데이터를 다른 운영체제로 보내거나 받을 때 영향을 미치기 때문에 데이터를 다루는 버퍼도 이를 고려해야 합니다.


     앞쪽 바이트부터 먼저 처리하는 것을 Big endian 이라고 하고, 뒤쪽 바이트부터 먼저 처리하는 것을 Little endian 이라고 합니다.






     

     Little endian으로 동작하는 운영체제에서 만든 데이터 파일을 Big endian으로 동작하는 운영체제에서 읽는다면 ByteOrder 클래스로 데이터 순서를 맞춰야 합니다. 


     ByteOrder 클래스의 nativeOrder() 메소드는 현재 동작하고 있는 운영체제가 Big endian 인지 Little endian 인지 알려줍니다.


     JVM도 일종의 독립된 운영체제이기 때문에 이런 문제를 취급하는데, JRE가 설치된 어떤 환경이든 JVM은 무조건 Big endian으로 동작하도록 되어 있습니다.


     다음 예제는 현재 컴퓨터의 운영체제 종류와 바이트 해석하는 순서에 대해 출력해줍니다.



     * ComputerByteOrderExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    package pathexam;
     
    import java.nio.ByteOrder;
     
    public class ComputerByteOrderExample {
     
        public static void main(String[] args) {
            System.out.println("운영체제: " + System.getProperty("os.name"));
            System.out.println("네이티브의 바이트 해석 순서: " + ByteOrder.nativeOrder());
        }
     
    }
     
    cs








     운영체제와 JVM의 바이트 해석 순서가 다를 경우에는 JVM이 운영체제와 데이터를 교환할 때 자동적으로 처리해주기 때문에 문제는 없습니다.


     하지만 다이렉트 버퍼일 경우 운영체제의 native I/O 를 사용하므로 운영체제의 기본 해석 순서로 JVM의 해석 순서를 맞추는 것이 성능에 도움이 됩니다.


     다음과 같이 allocateDirect() 로 버퍼를 생성한 후, order() 메소드를 호출해서 nativeOrder()의 리턴 값으로 세팅해주면 됩니다.



    1
    2
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100).order(ByteOrder.nativeOrder());
     
    cs















     3.3 버퍼의 위치 속성

      버퍼를 생성하는 방법을 알았으니 이제는 사용하는 방법을 알아보도롣 합시다.


      버퍼를 사용하려면 먼저 버퍼의 위치 속성 개념과 위치 속성이 언제 변경되는지 알고 있어야 합니다. 다음은 버퍼의 네 가지 위치 속성을 정리한 것입니다.



      속성

      설명 

     position 

     현재 읽거나 쓰는 위치값이다. 인덱스 값이기 때문에 0부터 시작하며, limit 보다 큰 값을 가질 수 없다. 만약 position과 limit 값이 같아지면 더 이상 데이터를 쓰거나 읽을 수 없다는 뜻이 된다. 

     limit 

     버퍼에서 읽거나 쓸 수 있는 위치의 한계를 나타낸다. 이 값은 capacity 보다 작거나 같은 값을 가진다. 최초에 버퍼를 만들었을 때는 capacity와 같은 값을 가진다. 

     capacity 

     버퍼의 최대 데이터 개수(메모리 크기)를 나타낸다. 인덱스 값이 아니라 수량임을 주의하자. 

     mark 

     reset() 메소드를 실행했을 때 돌아오는 위치를 지정하는 인덱스로서 mark() 메소드로 지정할 수 있다. 주의할 점은 반드시 position 이하의 값으로 지정해주어야 한다. position이나 limit 의 값이 mark 값보다 작은 경우, mark 값은 자동 제거된다. mark가 없는 상태에서 reset() 메소드를 호출하면 InvalidMarkException이 발생한다. 




      position, limit, capacity, mark 속성의 크기 관계는 다음과 같습니다. mark는 position 보다 클 수 없고, position은 limit 보다 클 수 없으며, limit는 capacity 보다 클 수 없습니다.


     



      예를 들어 다음 그림처럼 7 바이트 크기의 버퍼가 있다고 가정해봅시다. 처음에는 limit과 capacity가 모두 7이라는 값을 가지고 있고 position은 0을 가지고 있습니다. 버퍼의 크기가 7이므로 인덱스는 6까지입니다.










      먼저 2바이트를 버퍼에 저장해봅시다. 2바이트는 position이 위치한 0 인덱스에서 시작해서 버퍼에 저장됩니다. 따라서 다음 그림과 같이 처음 2바이트는 채워지고 position은 2번 인덱스가 됩니다.








      계속해서 3바이트를 저장해봅시다. 3 바이트는 position 2 인덱스에서 시작해서 버퍼에 저장됩니다. 따라서 다음 그림과 같이 5 바이트가 채워지고 position은 5번 인덱스가 됩니다.












      이제 버퍼에 저장되어 있는 바이트를 읽어보도록 하겠습니다. 먼저 flip() 메소드를 호출해야 합니다. flip() 메소드를 호출하면 limit을 현재 position 5 인덱스로 설정하고, position 을 0번 인덱스로 설정합니다. 











      버퍼에서 3 바이트를 읽는다고 가정해봅시다. position이 0번 인덱스이므로 처음 3바이트가 읽혀지고 position은 다음 그림처럼 3번 인덱스로 이동합니다.




     


      position이 3번 인덱스를 가르키고 있을 때 mark() 메소드를 호출해서 현재 위치를 기억시켜 놓습니다. 따라서 mark는 3번 인덱스에 위치합니다.








      이어서 2 바이트를 더 읽어봅시다. 다음 그림처럼 position은 5번 인덱스로 이동하게 됩니다.











      이번에는 position을 mark 위치로 다시 이동해야 한다고 가정해봅시다. reset() 메소드를 호출하면 position은 mark가 있는 3번 인덱스로 이동합니다. 주의할 점은 mark가 없는 상태에서 reset() 메소드를 호출하면 예외가 발생하게 됩니다.










      이번에는 버퍼를 되감아 동일한 데이터를 다시 한 번 더 읽고 싶다고 가정해봅시다. rewind() 메소드를 호출하면 limit은 변하지 않지만 position은 0번 인덱스로 다시 설정됩니다. mark는 position이나 limit이 mark 보다 작은 값으로 조정되면 자동적으로 없어집니다.








     


      만약 rewind() 대신 clear() 메소드를 호출하면 버퍼의 세 가지 속성은 초기화 됩니다. limit은 capacity로, position은 0으로 설정되고 mark는 자동적으로 없어집니다. 하지만 데이터는 삭제되지 않습니다.









      버퍼의 위치 속성을 변경하는 또 다른 메소드로 compact()가 있습니다. compact()를 호출하면 현재 position에서 limit 사이의 데이터가 0번 인덱스로 복사되고 현재 position은 복사된 데이터 다음 위치로 이동합니다. 예를 들어 flip() 메소드 호출 후 세 개의 데이터를 읽고 다음과 같이 position이 3번 위치에 있을 때 compact()가 호출되면 3번 인덱스와 4번 인덱스의 데이터는 0번 인덱스와 1번 인덱스로 복사되고 position은 2번 인덱스로 이동합니다. 그리고 limit은 capacity로 이동합니다. 주의할 점은 0번 인덱스와 1번 인덱스를 제외한 나머지 인덱스의 데이터는 삭제되지 않고 남아 있습니다.







      




      compact()를 호출하는 이유는 읽지 않은 데이터 뒤에 새로운 데이터를 저장하기 위해서입니다.









     3.4 버퍼 메소드

      버퍼를 생성한 후 사용할 때는 버퍼가 제공하는 메소드를 잘 활용해야 합니다. 


      버퍼마다 공통적으로 사용하는 메소드들도 있고, 데이터 타입별로 버퍼가 개별적으로 가지고 있는 메소드들도 있습니다.



        공통 메소드


      각 타입별 버퍼 클래스는 Buffer 추상 클래스를 상속하고 있습니다. 


      Buffer 추상 클래스에는 모든 버퍼가 공통적으로 가져야 할 메소드들이 정의되어 있는데, 위치 속성을 변경하는 flip(), rewind(), clear(), mark(), reset() 도 모두 Buffer 추상 클래스에 있습니다.


      다음은 Buffer 추상 클래스가 가지고 있는 메소드들을 정리한 표 입니다.






    [ 출처: Oracle ]





       데이터를 읽고 저장하는 메소드


      버퍼에 데이터를 저장하는 메소드는 put()이고, 데이터를 읽는 메소드는 get() 입니다.


      이 메소드들은 Buffer 추상 클래스에는 없고, 각 타입별 하위 Buffer 클래스가 가지고 있습니다.


      get() 과 put()은 상대적(Relative)와 절대적(Absolute)으로 구분됩니다. 


      버퍼 내의 현재 위치 속성인 position에서 데이터를 읽고, 저장할 경우는 상대적이고, position과 상관없이 주어진 인덱스에서 데이터를 읽고, 저장할 경우는 절대적입니다.


      상대적 get() 과 put()을 호출하면 position 값은 증가하지만, 절대적 get()과 put() 을 호출하면 position의 값은 증가되지 않습니다.


      만약 position 값이 limit 값까지 증가했는데도 상대적 get()을 사용하면 BufferUnderflowException 예외가 발생하고, put() 메소드를 사용하면 BufferOverflowException 예외가 발생합니다.





       버퍼 예외의 종류


      버퍼 클래스에서 발생하는 예외를 살펴보도록 하겠습니다. 주로 버퍼가 다 찼을 때 데이터를 저장하려는 경우와 버퍼에서 더 이상 읽어올 데이터가 없을 때 예외가 발생합니다.


      다음 표는 버퍼와 관련된 예외 클래스 입니다.




      예외

      설명 

     BufferOverflowException 

     position이 limit에 도달했을 때 put() 을 호출하면 발생 

     BufferUnderflowException 

     position이 limit에 도달했을 때 get() 을 호출하면 발생 

     InvalidMarkException 

     mark가 없는 상태에서 reset() 메소드를 호출하면 발생 

     ReadOnlyBufferException 

     읽기 전용 버퍼에서 put() 또는 compact() 메소드를 호출하면 발생 




      다음 예제는 데이터를 버퍼에 쓰고, 읽을 때, 그리고 위치 속성을 변경하는 메소드를 호출할 때 버퍼의 위치 속성값의 변화를 보여줍니다.



     * BufferExample.java


     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    package pathexam;
     
    import java.nio.Buffer;
    import java.nio.ByteBuffer;
     
    public class BufferExample {
     
        public static void main(String[] args) {
            System.out.println("[7바이트 크기로 버퍼 생성]");
     
            ByteBuffer buffer = ByteBuffer.allocateDirect(7);
            printState(buffer);
     
            buffer.put((byte10);
            buffer.put((byte11);
            System.out.println("[2바이트 저장후]");
            printState(buffer);
     
            buffer.put((byte12);
            buffer.put((byte13);
            buffer.put((byte14);
            System.out.println("[3바이트 저장후]");
            printState(buffer);
     
            buffer.flip();
            System.out.println("[filp 실행후]");
            printState(buffer);
     
            buffer.get(new byte[3]);
            System.out.println("[3바이트 읽은후]");
            printState(buffer);
     
            buffer.mark();
            System.out.println("[현재 위치 mark 해놓음");
     
            buffer.get(new byte[2]);
            System.out.println("[2바이트 읽은 후]");
            printState(buffer);
     
            buffer.reset();
            System.out.println("[position 을 마크 위치로 옮김]");
            printState(buffer);
     
            buffer.rewind();
            System.out.println("[rewind 실행 후]");
            printState(buffer);
     
            buffer.clear();
            System.out.println("[clear 실행 후]");
            printState(buffer);
     
        }
     
        public static void printState(Buffer buffer) {
            System.out.print("\tposition: " + buffer.position() + ", ");
            System.out.print("\tlimit: " + buffer.limit() + ", ");
            System.out.println("\tcapacity: " + buffer.capacity());
        }
     
    }
     
     
    cs









      다음 예제는 compact() 메소드 호출 후, 변경된 버퍼의 내용과 position, limit의 위치를 보여줍니다.




     * CompactExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
     
    package pathexam;
     
    import java.nio.ByteBuffer;
     
    public class CompactExample {
     
        public static void main(String[] args) {
            System.out.println("7바이트 크기로 버퍼 생성");
            ByteBuffer buffer = ByteBuffer.allocateDirect(7);
            buffer.put((byte10);
            buffer.put((byte11);
            buffer.put((byte12);
            buffer.put((byte13);
            buffer.put((byte14);
            buffer.flip();
            printState(buffer);
     
            buffer.get(new byte[3]);
            System.out.println("3바이트 읽음");
     
            buffer.compact();
            System.out.println("compact() 실행후");
            printState(buffer);
        }
     
        public static void printState(ByteBuffer buffer) {
            System.out.print(buffer.get(0+ ", ");
            System.out.print(buffer.get(1+ ", ");
            System.out.print(buffer.get(2+ ", ");
            System.out.print(buffer.get(3+ ", ");
            System.out.println(buffer.get(4));
            System.out.print("position: " + buffer.position() + ", ");
            System.out.print("limit: " + buffer.limit() + ", ");
            System.out.println("capacity: " + buffer.capacity());
        }
     
    }
     
    cs










     3.5 버퍼 변환

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


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



       ByteBuffer <-> String


      프로그램에서 가장 많이 처리되는 데이터는 String 타입, 문자열 입니다.


      채널을 통해 문자열을 파일이나 네트워크로 전송하려면 특정 문자셋(UTF-8, EUC-KR)으로 인코딩해서 ByteBuffer로 변환해야 합니다.


      먼저 문자셋을 표현하는 java.nio.charset.Charset 객체가 필요한데, 다음 두 가지 방법으로 얻을 수 있습니다.



    1
    2
    3
    Charset charset = Charset.forName("UTF-8");
    Charset charset = Charset.defaultCharset();
     
    cs



      Charset을 이용해서 문자열을 ByteBuffer로 변환하려면 다음과 같이 encode() 메소드를 호출하면 됩니다.


    1
    2
    3
    String data = ".....";
    ByteBuffer buffer = charset.encode(data);
     
    cs



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


    1
    2
    3
    ByteBuffer buffer = ...;
    String data = charset.decode(byteBuffer).toString();
     
    cs



      다음 예제는 문자열을 UTF-8로 인코딩해서 얻은 ByteBuffer를 다시 UTF-8로 디코딩해서 문자열로 복원하는 예제입니다.



      * ByteBufferToStringExample.java



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     
    package pathexam;
     
    import java.nio.ByteBuffer;
    import java.nio.charset.Charset;
     
    public class ByteBufferToStringExample {
     
        public static void main(String[] args) {
            Charset charset = Charset.forName("UTF-8");
     
            String data = "Hello !!";
            ByteBuffer buffer = charset.encode(data);
     
            data = charset.decode(buffer).toString();
            System.out.println("문자열 복원 : " + data);
        }
     
    }
     
    cs




       ByteBuffer <-> IntBuffer


      int[] 배열을 생성하고 이것을 파일이나 네트워크로 출력하기 위해서 int[] 배열 또는 IntBuffer로부터 ByteBuffer를 생성해야 합니다.


      int 타입은 4 byte 크기를 가지므로 int[] 배열 크기 또는 IntBuffer의 capacity보다 4배 큰 capacity를 가진 ByteBuffer를 생성해야 하고, ByteBuffer의 putInt() 메소드로 정수값을 하나씩 저장하면 됩니다.


      다음 예제는 int[] 배열로부터 얻은 ByteBuffer를 이용해서 다시 int[] 배열로 복원하는 예제입니다.



     * ByteBufferToIntBufferExample.java


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
     
    package pathexam;
     
    import java.nio.ByteBuffer;
    import java.nio.IntBuffer;
    import java.util.Arrays;
     
    public class ByteBufferToIntBufferExample {
     
        public static void main(String[] args) {
            int[] writeData = { 1020305070 };
            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));
        }
     
    }
     
    cs






      ByteBuffer와 IntBuffer 간의 변환을 이해하면 나머지 자료형에 관한 변환도 비슷한 방법으로 하면 됩니다.


      각 자료형에 맞게 asXXXBuffer() 메소드를 끼워서 사용하시면 됩니다.












    * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.

    댓글

Designed by Tistory.