이전글에 이어서 실제 프로젝트에서 발생했던 문제와 그 처리 방안에 대해서 적어보고자 한다.
1. 프로젝트 내용
1) 특정 파일의 데이터를 읽어서 가공한 후 Web에 전달한다.
2) 특정 파일의 내용을 다른 프로세스에 의해서 계속 가공되고 있으며, 따라서 파일에 접근했을
당시에 파일의 내용이 완료되지 않은 상태일 수 있다.
3) 데이터 전송 시간의 보장을 위해서 일정 시간동안만 데이터를 읽어서 가공한 후 Web에 전달하
며 다음 요청시에는 지난번에 읽었던 부분 이후부터 데이터를 읽는다.
4) 파일의 최종 크기는 GB단위일 수 있다.
2. 처리 방안
1) BufferedReader를 이용해서 데이터를 읽어드린다.
2) 1-3)의 처리를 위해서 web과 파일의 offset을 공유하는 방식을 이용한다. 즉 input/output에 현재까지 읽은 파일의 offset을 실어서 통신한다.
3) 파일의 offset까지의 이동은 skip()함수를 이용한다.
3. 문제
2의 처리 방안으로 개발해 본 결과 성능적인 면에서 문제가 발생했다.
성능의 문제 부분은 skip()함수에 있었다. (자세한 내용은 이전글 참조)
파일의 사이즈가 GB이상 되면서 offset의 위치도 GB가 넘어가게 되면서 skip에서 10초이상이 걸리
게 되었다. 따라서 이에 대한 대안이 필요하게 되었다.
4. 문제 해결
가장 문제가 되는 skip의 처리를 위해 RandomAccessFile을 통해서 파일을 다루도록 해본 결과 이전글에서 다루었듯이 실제 데이터 처리에서의 성능 저하가 심했다. 따라서 RandomAccessFile의 seek과 BufferedReader를 같이 이용하는 방법을 사용했다.
즉 RandomAccessFile는 seek을 통한 file pointer의 이동만을 담당하고 실제 데이터는 BufferedReader의 readLine을 통해 처리하는 것이다. 이를 위해 FileDescription를 사용한다.
ex) RandomAccessFile RASeek = new RandomAccessFile(strFileName, "r");
BufferedReader reader = new BufferedReader(new InputStreamReader(new
FileInputStream(RASeek.getFD()),"UTF-8"), defaultBufferSize);
위와 같이 RandomAccessFile을 이용해 file을 open한 후 FileInputStream을 FileDescription을 이용해 생성하면 된다. 단 이 경우 주의해야할 점은 BufferedReader를 생성하기 전에 seek을 수행해아 한다는 것이다. 실제 해본 결과 BufferedReader를 생성한 후 seek을 수행하면 seek는 되나 BufferedReader는 기존의 위치에서 data를 읽어들인다. (이유는.. 모른다.. -_-;) 그러니 꼭 seek을 수행한 후에 BufferedReader를 생성해야 한다.
그렇다면 만약 seek수행전에 데이터를 읽어야 하는 경우는 어떻할까? 필자의 경우 version이나 기타 메타 데이터를 읽은 후에 seek을 수행해야 하는 경우였으며 어쩔 수 없이 RandomAccessFile로 데이터를 처리한 후 seek을 수행했다. (다른 좋은 방법이 있으면 알려주세요~)
seek을 수행한 후에는 BufferedReader를 읽어서 데이터를 처리하면 된다.
남은 문제는 offset을 계산하는 것이었다. 기존에는 BufferedReader로 읽은 데이터(String)의 length를 계산하고 합산해서 전달했었다. 그러나 이는 캐릭터 사이즈로 seek에서 사용하는 byte 단위와는 호환(?)이 되지 않는다. 따라서 다른 방법이 필요했다.
처음 사용한 방법은 string.getBytes().length를 이용하는 것이었다. 그러나 이 방법은 byte변환 작업을 수행해야 하므로 성능이 엄청나게 저하되었다. (BufferedReader의 성능을 10이라고 하면 이 경우 2정도로 성능이 저하된다. RandomAccessFile만을 이용하는 경우는 1의 성능정도이다.)
두번째 방법은 RandomAccessFile의 getFilePointer를 이용하는 것이었다. 그러나 이 경우의 문제점은 얻어온 offset의 위치가 실제 데이터를 읽은 위치가 아니라는 것이다. 그 이유는 BufferedReader로 데이터를 읽은 경우 BuffereedReader가 Buffering을 통해 데이터를 읽어들임으로써 file pointer의 위치가 buffer가 읽어들인 파일의 위치를 가리키고 있다는 것이다.
따라서 마지막으로 선택한 방법은 buffer가 읽어들인 최종 위치에서부터 일정 부분 앞으로 이동한
후 지금 읽은 데이터의 위치를 찾아서 그 위치를 return하는 방식이다.
즉 buffer의 최종 file pointer에서 buffer size만큼 앞으로 이동한 후 다시 데이터를 읽어들이면서 마지막에 읽은 데이터를 찾아서 그 부분의 위치를 찾아서 offset을 이용하는 것이다.
ex) protected long calcFileByte(RandomAccessFile reader, String lastData) throws Exception
{
String strDiff = new String(lastData.getBytes("UTF-8"),"ISO-8859-1");
long lSize = 0;
boolean isMatch = false;
lSize = reader.getFilePointer() - defaultBufferSize*2;
if(lSize <= 0){
lSize = 0;
}
reader.seek(lSize);
String strLine = "";
while((strLine = reader.readLine()) != null){
if(strLine.equals(strDiff)) {
isMatch = true;
break;
}
}
if(isMatch == false){
System.out.println("error");
}
return reader.getFilePointer();
}
소스를 올리니 참고하시기 바란다.
'Language > Java' 카테고리의 다른 글
[펌] 자바 암호화 (0) | 2012.05.24 |
---|---|
Retry JDBC Query execution under specified sql error code (Vendor specific) (0) | 2012.05.24 |
[펌] RandomAccessFile과 BufferedReader의 한계점과 해결 방안 (0) | 2012.05.21 |
Start java application in background mode on Window (0) | 2012.05.06 |
[펌] 프로그램의 프로세스를 하나만 허용하기 (0) | 2012.05.05 |