Tistory View

 

 

buffer.zip
0.01MB

소스코드

 

 

 

 

ByteBuffer, IntBuffer, FloatBuffer, DoubleBuffer, ShortBuffer, LongBuffer, CharBuffer.. 등에는 flip, compact, clear등에 함수가 있다. 근데, 이 flip, compact, clear는 도대체가 뭐 하는 녀석인지.. 알기가 어렵다.

 

답은 개발자 식으로 말하면 Producer-Consumer방식을 사용하기 위해 있는 것이다.

 

 

이 Buffer들은 읽기모드와 쓰기모드가 있다. 

현재상태에 따라 해야하는 작업이 정해진다. 읽기모드일 경우는 말 그대로 읽기명령(get함수등)등을 써야 하며, 쓰기모드에서는 쓰기명령(put함수 등)으로 Buffer에 작업을 하면 된다.

 

 

예로 이해하기

아주 간단한 예를 들어보겠다. Buffer에 데이터를 쓰고 Buffer에서 데이터를 읽는 정말 단순한 코드이다.

읽어내려갈 때, 쓰기모드인지 읽기모드인지 잘 확인하고 버퍼의 상태를 잘 보길 바란다.

P : Position, L : Limit C : Capacity

 

 

CharBuffer buf = CharBuffer.allocate(8); // capacity : 8

8개의 글자가 들어갈 수 있는 버퍼를 만든다. allocate하면 버퍼는 현재 쓰기모드이다.

P               L
|0|1|2|3|4|5|6|7| 
| | | | | | | | |

 

버퍼상에 아무런 데이터가 없으니 일단 데이터를 써보자

buf.put('a'); buf.put('b'); buf.put('c'); buf.put('d');

P가 하나씩이동하면서 'abcd'가 버퍼에 들어가게 된다.

P------>P       L
|0|1|2|3|4|5|6|7|
|a|b|c|d| | | | |

 

이제 이 입력된 데이터를 읽기위해 read-mode로 바꾼다.

buf.flip();

버퍼의 시작이자 데이터의 시작인 0으로 P가 이동하고 데이터의 끝 부분에 L이 위치하게 된다.

flip()은 L을 P위치로 바꾸고, P를 0으로 바꿀뿐이다.

flip이라는 뜻 자체가 "뒤집다"라서 쓰기모드를 읽기모드로 바꾸는 것을 의미한다. 하지만 한번 더 호출한다고 해도 읽기모드에서 쓰기모드로 바꾸는 것은 아니다.

        L<------L
P<------P
|0|1|2|3|4|5|6|7|
|a|b|c|d| | | | |

L = P
P = 0

 

이제 데이터를 읽어보자

char a, b, c;

a = buf.get(); b = buf.get(); c = buf.get();

데이터를 3개 읽어왔다.

3개를 읽어서 P가 3번 이동을 했다.

P---->P L
|0|1|2|3|4|5|6|7|
|a|b|c|d| | | | |

 

이제 다시 쓰기모드로 바꿔보자. 아직 'd'데이터는 읽지않은 상태다.

buf.compact();

아직 읽지않은 'd'가 복사되고 'd'의 바로 뒤로 쓸 수 있도록 P가 변한 것을 알 수 있다.

  P<--P   
        L------>L
|0|1|2|3|4|5|6|7|
|d| | | | | | | |


남아있는 데이터를 처음으로 복사
P = 남아있는 데이터 수
L = C

 

이 코드들이 다시 루프를 돌면서 쓰기-읽기를 반복하는 것이다.

 

compact() vs clear()

compact는 남아 있는 데이터를 첫 부분으로 복사를 하고 clear는 말 그대로 싹 지워버리는 것이다. 둘다 현재모드와 상관없이 write-mode로 바꾸는 것이다. compact시 데이터가 없다면 결과는 clear와 같게 된다.

사실상 필자의 경우 재사용을 위해 초기화 할 때를 제외하고는 clear를 쓸 일은 별로 없었다.

 

참고로 clear 후의 버퍼 모습은 다음과 같다.

P               L
|0|1|2|3|4|5|6|7|
| | | | | | | | |

P = 0
L = C

 

남아있는 공간 확인

쓰기모드일 때 얼마나 데이터를 더 쓸 수 있는 지, 아니면 읽기모드일 때 얼마나 데이터를 읽을 수 있는 지를 알아내는 함수는 다음의 함수가 있다.

int remaining()

CharBuffer의 경우 length()라는 함수가 추가되어 있는 데, 이 것의 결과는 remaining()과 동일하다.

실제 손으로 계산을 해보면 늘 같은 값이 나온다. 이 것은 산수로 L-P일 뿐이다.

 

 

필자가 맨 위에서 Buffer의 현재상태가 읽기인지 쓰기모드인지 구분하면서 시작을 했다. 솔직히 이 것은 그냥 개념적인 것이다. allocate의 경우 Buffer내부의 데이터가 없는 상태(쓰레기값?)이기에 쓰기모드라고 명명했다

하지만 wrap함수(이미 존재하는 배열을 buffer로 쓰기)를 쓰면, 배열에 읽을 데이터를 미리 넣어두었다면 읽기모드로 시작되지만, 메모리 할당을 줄이기 위해 사용한다면 쓰기모드로 시작하게 될 것이다.

 

기타

mark()와 reset()

이 글의 주제와는 맞지 않지만 워낙에 작은 주제라 그냥 이 곳에 쓰기로 한다.

mark()는 현재 위치(P)를 임시 저장한다.

reset()은 임시저장된 위치로 다시 P를 옮겨 놓는다.

주로, 데이터를 미리 살짝 읽어 볼 때(Peek) 주로 쓴다.

여기서 reset을 clear와 헤깔리면 안된다. clear를 데이터를 비워서 쓰기모드로 바꾸는 것이다.

 

 

근데 이걸 왜 써?

생각해보면 그냥 데이터 보내고 받으면 되지 왜 굳이 이런 것을 만들어 놨을 까?

데이터가 스트림으로 지속적으로 들어온다고 가정을 해보자, 그리고, 한번 처리를 하는 데, 어떤 경우는 1개의 입력이 필요하고 어떤 경우는 2개의 입력이 필요하고 또 어떤 경우는 3개의 입력이 필요한 상황일 경우, 게다가 입력이 얼마나 필요한지는 데이터를 읽어봐야 몇개가 필요한지 알 수 있는 경우가 있다.

대표적인 것이 byte스트림을 String으로 변환할 때가 대표적인 경우다. UTF-8로 된 byte스트림이 있다면, 이게 영문자는 1개가 들어오면 바로 처리가능하지만 한글의 경우 3개가 들어와야 하는 데, 한글인지 아닌지는 데이터가 들어오기 전에는 알 수가 없다. 그래서 일단 들어온 스트림을 지속적으로 입력버퍼에 쓰고, UTF-8을 한글로 변환하는 디코더가 필요한 만큼만 읽어서 처리하기 위해서다.(설명이 좀 어렵네..)

 

 

예제

다음의 예는 int로 들어오는 값을 3개씩 읽어서 한글로 바꾸는 예다.

예를 들어 353이 입력이면 "삼오삼"으로 바꾸는 코드이다. 가변적이진 않지만 실제 사용법은 이런 식이고, 주석에 다 적어 놨으니 꼼꼼히 읽기를 바란다.

import android.util.Log;
import java.nio.CharBuffer;
import java.nio.IntBuffer;

public class SimpleCalc {

    private static final String TAG = "SimpleCalc";

    // allocate시 write-mode로 시작한다.
    private IntBuffer   mBufInt = IntBuffer.allocate(4);
    private CharBuffer  mBufChr = CharBuffer.allocate(4);

    enum CalcResult {
        RESULT_UNKNOWN,
        RESULT_UNDERFLOW,
        RESULT_OVERFLOW
    }

    public void compute(int[] in, int inLen, StringBuilder out ) {

        // 현재 bufInt는 read-mode이며 bufFlt는 write-mode이다.


        final String hangul = "영일이삼사오육칠팔구";

        int inOffset = 0;
        int inRemain = inLen;

        int[] calc = new int[3];

        while( inRemain > 0 ) {
            //버퍼에 남아있는 크기 만큼 집어넣는다. 따라서 Overflow는 발생하지 않는다.
            int copyLen = mBufInt.remaining() < inRemain ? mBufInt.remaining() : inRemain;
            mBufInt.put(in, inOffset, copyLen); // 현재 write-mode
            inOffset += copyLen;
            inRemain -= copyLen;

            // read-mode로 변경하여 계산기가 읽을 수 있게 한다.
            mBufInt.flip();

            while( true ) {

                // 계산기 : 입력된 값을 최대한 처리하도록 만든다.
                // 계산기에 들어가기전에 bufInt는 read-mode이며 bufFlt는 write-mode이다.
                // 따라서 내부에는 clear, compat, flip 등은 존재하지 않는다.
                // 계산기가 끝났을 경우도 이 상태을 그대로 유지하는 것이 편하다.
                CalcResult result = CalcResult.RESULT_UNKNOWN;
                while (true) {
    
                    if (mBufInt.remaining() < 3 ) {
                        result = CalcResult.RESULT_UNDERFLOW;
                        break;
                    }

                    if (mBufChr.remaining() < 3 ) {
                        result = CalcResult.RESULT_OVERFLOW;
                        break;
                    }

                    mBufInt.get(calc); // 현재 read-mode

                    mBufChr.put( hangul.charAt( calc[0] ) );
                    mBufChr.put( hangul.charAt( calc[1] ) );
                    mBufChr.put( hangul.charAt( calc[2] ) );
                }


                //
                if (result == CalcResult.RESULT_OVERFLOW) {

                    // bufFlt가 꽉 찼으니 결과를 저장한다. 보통 OutputStream계열을 쓰게 될 것이다.
                    // 이 예제는 StringBuilder로 처리한다.

                    // 현재 mBufChr는 write-mode이기에 읽을 수 있게 read-mode로 바꾼다.
                    mBufChr.flip();
                    while (mBufChr.remaining() > 0) {
                        out.append(mBufChr.get());
                    }

                    // 계산 결과를 저장했으니, 이제 계산기가 쓸 수 있도록 write-mode로 바꾼다.
                    mBufChr.compact();
                    // 이 예제는 결과가 늘 한개씩이라 mBufChr.clear()를 사용하는 것이 더 효율적이지만
                    // 다른 곳에서 사용한다면 결과를 다 저장하지 못하는 경우가 있으니,
                    // compact를 사용한다.

                    // Overflow로 발생으로 입력값을 다 계산하지 못했으니, 다시 계산을 하도록 한다.
                    continue;
                }

                if (result == CalcResult.RESULT_UNDERFLOW) {
                    Log.d( TAG, "Underflow");
                    // 들어온 값으로 일단 계산 할 수 있는 것은 다 했다.
                    // 하지만 아직 처리 못한 입력값은 그대로 이 mBufInt에 있다.

                    // data를 앞쪽으로 밀어 다음번 호출시 data를 입력할 공간을 마련한다.
                    mBufInt.compact(); // write-mode로 변경한다.
                    break;
                }
            }
        }
    }

    // 마지막 짜투리 데이터 처리
    public void flush( StringBuilder out ) {
		// 현재 mBufChr는 write-mode다. 읽기위해 read-mode로 바꾼다.
        mBufChr.flip();
        while (mBufChr.remaining() > 0) {
            out.append(mBufChr.get());
        }
    }

}

실제 실행하는 코드다.

public void RunBufferTest() {

   // 총 60개 3개씩 * 20번
   final int   total = 20 * 3;

   int[] inputData = new int[total];
   int[] dataBuf = new int[32];

   int offset = 0; // inputData의 입력위치 

   SimpleCalc calc = new SimpleCalc();
   
   StringBuilder outBuilder = new StringBuilder();

   // 60개의 데이터를 미리 만든다. stream방식을 흉내내려는 사용목적과는 다르지만 코드가
   // 복잡해지는 것을 막으려고 했다.
   for( int i = 0 ; i < total ; i++ ) {
       inputData[i] = (int)( Math.random() * 10.0 );
   }


   int remain = total;
   while( remain > 0 ) {

       // 스트림을 흉내낸다. 데이터를 일정한 크기로 넣는 것이 아니고
       // 1개~4개를 입력으로 사용한다.
       int dataLen = (int)( Math.random() * ( 4 - 1 ) ) + 1;

       int count = remain < dataLen ? remain : dataLen;
       for( int j = 0 ; j < count ; j++ ) {
           dataBuf[j]= inputData[offset];
           //inBuilder.append( dataBuf[j] );
           offset++;
           remain--;
       }

       calc.compute(dataBuf, dataLen, outBuilder );
   }

   // 아직 결과값이 calc내부에 있기 때문에 나머지 결과를 다 가지고 온다.
   calc.flush( outBuilder );

   Log.d( TAG, outBuilder.toString() );
}
​

 

 

 

QnA

Q. 현재 읽기모드인지 쓰기모드인지 알 수 있는 방법은 뭔가요?

A. 필자가 알기로는 알 방법이 없다.

 

Q. 이 거 왜 블로그에 올렸나요?

A. 이 작업(flip, compact)는 실제 프로그램 짜면서 쓸 일이 거의 없다. 즉, 머지않아 까먹는다. 그래서, 다시 머리로 풀기 싫어서 써놨다.(써놓은 것을 까먹는 게 함정)

Reference

developer.android.com/reference/java/nio/Buffer

Replies
Reply Write