Skip to main content

4 posts tagged with "java-2"

View All Tags

· 4 min read

Java로 웹에 있는 영상 파일을 다운로드하는 기능을 간단하게 구현하면서 기본함수에 대한 의문이 생겼다.

실제로 영상 파일이 깨지는 문제도 있었다.

1. FileOutputStream write(byte[] b)

아래 구문에서 사용된 write 함수는 byte buffer, offset, length를 parameter로 받는다.

좀 더 간단한 형태인 write(byte[] b)를 사용하는 것으로 코드를 변경하면 문제가 발생한다. (테스트했던 영상파일들에서는)

InputStream is = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(filename);
byte[] buffer = new byte[4096];
int readBytes = -1;
while((readBytes = is.read(buffer)) != -1) {
fos.write(buffer, 0, readBytes);
}

write(byte[] b)의 내용을 보자.

fileoutputstream-write(byte[])내용은 간단하다. byte 배열의 길이만큼 file output stream에 쓰는 기능이다.

2. 왜 문제가 생겼는가?

결과적으로 보자면 HTTP로 들어온 input stream을 읽어들인 byte 수가 byte array의 총 크기보다 작은 경우가 있기 때문이다.

보통의 파일을 읽으면 마지막만 제외하고 buffer로 사용하는 byte array를 꽉꽉 채웠었는데 이 경우는 그렇지 않았다.

궁금해서 살펴보니 HttpInputStream의 read(byte[])는 이렇게 구현되어 있다.

httpinputstream-read(byte[])read(byte[], int, int) 함수를 호출하지만 상속트리를 뒤지다보면 결국 자신이 구현한 read() 함수 호출이 기본이 된다.

read() 함수는 아래 그림처럼 -1이 나오기 전까지 1바이트씩 읽어들이도록 되어있고 read(byte[], int, int) 함수도 -1을 만나기 전까지는 byte array의 길이만큼 읽어들이게 된다.

httpinputstream-read()

그러니까 byte array 만큼 읽어들이지 못하는 경우가 있을 수 있는데 이에 대해서 고려하지 못했던게 문제였다.

실제로 찍어보면 사용된 byte array는 4096만큼 0으로 초기화되어 있고 계속 사용하기 때문에 byte array의 길이는 4096으로 항상 동일하고 읽어들인 실제 byte 수는 그것보다 작은 경우가 지속적으로 발생했다.

이런 상태에서 FileOutputStream의 write(byte[]) 함수를 사용하면 byte array의 길이만큼 쓰게된다. 실제 읽어들인 byte 만큼이 아니라 과거에 사용했던 일부 byte가 섞이게 되는 거다.

3. 결론

가능하면 write(byte[]) 함수는 사용하지 않을 생각이다.

조금 불편해보여도 확실한게 낫다.

· 6 min read

이 글이 작성된 해는 2014년도입니다. 최근(2016년 ~) 유행하고 있는 추천서비스 등과는 관련이 없는 내용이고 개인적인 필요에 의해서 뭔가 실험하고 작성한 내용이에요. 방문자가 별로 없는 블로그이지만 검색 등을 통한 이 글로의 유입량이 많아서 혹시 오해가 있을까하여 미리 밝혀둡니다.


복수의 아이템이 들어있는 자료구조에서 추첨을 해야하는 경우가 있다. 보통 추첨을 한다하면 무작위로 선택되어야 하는데 때에 따라서는 특정 아이템의 선택 확률을 높여야 할 수도 있다.

 

언어에 따라 조금 차이가 있기는 하지만 보통 random number 생성에 관계된 API들은 수학과 관련된 package로 묶여 있거나 연산에 특화된 기능들끼리 모여있다. 따라서 단순 random number 생성 이외의 기능, 가중치를 고려하는 부분은 직접 구현할 수 밖에 없다. 또는 가중치 고려가 가능하게 만들어진 오픈소스 라이브러리가 있을지도 모르겠는데 그리 복잡하지 않을 것 같아 직접 구현하기로 했다.

 

1. 가중치를 통해 선택확률 높이기

아래의 그림을 보면 윗 부분은 가중치가 1로 동일한 상태이고 아래는 특정 아이템에 가중치를 더 높임으로써 선택될 확률을 높인 상태이다. 칸으로 구별된 bar를 다루게 될 아이템이 담겨있는 자료구조로 보자. 각 칸에는 하나의 아이템이 담겨있고 좌측을 시작으로 우측으로 갈 수록 index가 높다고 하자. random_weight

 

이렇게 하는 것이 어떻게 선택될 확률을 높일 수 있을까?

Java를 기준으로 random number 생성을 살펴보면 단순히 Math.random() 호출시 0과 1사이에 있는 double 값을 생성해서 돌려주게 되어있다. (정확히 0.0 이상 1.0 미만의 값) Random number의 range를 0 ~ total weight로 설정하면 위 그림의 bar 상단에서 좌측에서 우측으로 증가하는 1차원 그래프를 상상해볼 수 있다. 즉 생성된 random number 가 찍히는 점의 아래에 있는 아이템이 선택될 수 있다.

Weight가 동일한 경우는 그래프에서 차지하는 영역이 동일하기 때문에 random number가 동일한 확률로 생성된다면 아이템이 선택될 확률도 동일하다고 볼 수 있다. 반면에 특정 아이템의 weight가 높은 경우 차지하는 영역이 넓어지기 때문에 그만큼 그 영역안에서 random number가 생성될 가능성이 다른 영역에 비하면 높아진다.

 

2. 그렇다면 구현은 어떻게 할까?

위 그림과 그래프를 상상해보면 구현은 간단하다. 각 아이템이 갖는 가중치를 모두 더해서 Math.random() 등으로 random number를 생성할 때 0부터 가중치합 사이의 값이 생성되도록 한다. (역시 언어에 따라 다르지만 random number 생성시 seed 값을 설정할 수 있어야 random number 생성의 중복을 방지할 수 있다. Java의 경우 Math 보다는 Random class를 사용하는게 나을 것으로 판단됨) 그렇게 생성된 random number를 가지고 자료구조에서 index를 증가시키며 각각의 아이템이 가진 가중치를 뺀다. (가중치를 빼지 않고 더해서 하는 방법도 있을텐데 그 경우 random number와의 비교구문이 약간 달라져야 할 것이다) 그렇게 진행하다보면 어느 순간 random number가 0이나 음수가 되는데 그 때의 아이템이 가중치에 따라 선택된 아이템이 된다. Pseudo code로 간단하게 작성하면 아래와 같다.

List<Item> list = getItemList();
double total = getTotalWeight(list);
for(int i = 0; i < list.size(); i++) {
total -= list.get(i).getWeight();
if(total <= 0) {
selectedIndex = i;
break;
}
}
return list.get(selectedIndex);

· 5 min read

예전에 구축해 놓은 시스템은 shell과 perl, jar로 구성되어 있다. 누구나 접근하기 쉽기도 하지만 사용해야 하는 다른 시스템도 대부분 shell과 perl로 되어 있었기 때문에 빠른 시간안에 구축하기 위해 선택한 방안이었다.

문제는 script 들로 구성되어 있다보니 재사용성이 낮았고 여기저기 파편화되어 구축 이후의 관리에 애를 먹게 되었는데 이번에 전체적으로 개선할 수 있는 기회가 만들어져서 이전의 문제들을 줄일 수 있는 방법들을 고민하게 되었다.

재사용성이 떨어지는 script 들을 가능한 줄이되 script를 이용하는게 더 효율적인 부분은 groovy를 사용해보기로 마음을 먹게 되었는데 깊은 부분을 몰라서 그런 것도 있지만 해결하기 어려운 부분이 생겼다. 임시 방안으로 땜질해 놓긴 했지만.

1. Groovy의 장점

1-1. 구조적인 script

Shell이나 perl과는 다르게 구조적으로 작성 가능하다는게 가장 큰 장점이라고 생각한다. 일단 표면에 내세운 것도 Object Oriented 인데, 실제 클래스 구현과 객체 생성이 가능하다. 재사용성을 높이면서 동시에 지저분해지기 쉬운 script를 간결하게 정리할 수 있다. 물론 본인이 마음을 먹어야 가능한건데, 작성방식에 따라 다른 script처럼 만들 수도 있겠다.

1-2. Java와의 유사성

기본 문법이 Java와 꽤 유사하다. 예약어나 접근 제한의 의미는 약간 다르고 문법적인 규칙의 강도는 Java에 비해서는 약한 편이다. 예를 들어 세미 콜론은 붙여도 되고 안붙여도 무방하며, println의 경우 괄호로 내용을 감싸도 되지만 풀어놔도 괜찮다. return도 마찬가지로 꼭 넣을 필요가 없고, data type을 굳이 명시하지 않고 def로 표현해도 되는데 이 부분은 Javascript와도 닮은 부분이다.

 

2. 문제가 되는 부분

2-1. Bash command

나의 경우엔 groovy에서 외부 프로세스를 생성해서 호출해야만 하는 상황이 여럿 존재하는데, 그 중에 source command로 메모리에 올려두고 다음 shell script에서 사용해야 하는 경우는 정말 방법이 없어보였다.

2-2. 연속된 command의 호출

연속된 shell script의 호출이나 command의 호출은 && 연산자로 어떻게 해볼 수는 있겠지만 근본적으로 깔끔하게 groovy에서 해결할 수는 없었다. groovy로 그 기능들이나 내용을 다시 구현하지 않는 이상.

 

3. 해결

결국은 기존의 shell script가 꼭 필요한 경우는 그대로 쓰기로 하고 groovy는 wrapping 하는 정도에서 마무리하게 되었다. Wrapping이야 shell script로 불가능한 것은 아닌데 훨씬 좋아진 점은 가독성이고, shell script가 위에서부터 아래 방향으로 순차적으로 해석되어 실행되는 것에 반해 groovy는 그렇지 않기 때문에 필요한 함수를 script 하단에 정의할 수도 있었기 때문에 훨씬 정리된 script를 만들 수 있었다. 특히 사용 가능한 라이브러리 (예를 들어, json 관련 클래스)가 많아서 짧게 구현하는데 유용했다.

· 4 min read

JSP로 간단한 페이지를 만드는데 로컬에 있는 로그파일을 읽어서 화면에 보여주기로 했다.

이미 WAS의 context path 안에 있는 파일이고 txt 라서 url 호출만으로도 빠르게 파일 내용을 보여주는데는 문제가 없었는데, 정말 raw 파일이라서 보기도 힘들고 예쁘지 않아 table 안에 넣어서 보여주기로 결정했는데 단순하게 BufferedReader class에 있는 readline을 사용해보니 너무 느렸다.

속도도 문제였지만 로그파일이 보통 1MB 이상이고 반복적인 로딩이 예정된 상태라 과부하를 신경쓰지 않을 수 없는 상태여서 다른 방안을 생각할 수 밖에 없었다.

readline 함수는 BufferedReader 라는 클래스에 정의된 함수로 유사한 함수들이 많다. 그 중 read 라는 함수가 있는데 정의를 살펴봤는데, 우선 BufferedReader는 Reader 클래스를 상속한 함수로 Reader 클래스에 정의된 간단한 형태의 read(char[] cbuf)는 read(char[] cbuf, int off, int len)을 호출하도록 정의되어 있다. 그리고 read(char[] cbuf, int off, int len)은 abstract 함수로 각각의 서브클래스에서 정의되는 형태이다.

그래서 BufferedReader 클래스의 read와 readline을 비교해 본 결과 아래와 같은 차이를 발견할 수 있었다.

- read는 loop 내부의 함수 호출이 작다.

- readline은 loop 내부의 함수 호출이 많다

- read는 system 함수나 원시 함수 위주의 호출

- readline은 read 보다 상위의 함수나 클래스 사용

- read는 buffer size 지정하지 않았을 때 기본 8K를 한꺼번에 읽어들임

- readline은 size와 관계없이 line feed까지만 읽어들임

 

요약해보면, readline은 무조건 linefeed를 기준으로 읽어들이는데 동작 자체가 무겁고 read 함수는 정의된 block 단위로 읽어들이는데 정의된 동작 자체가 가볍다. 당연히 line이 많아질수록 readline 함수는 반복횟수가 많아질 수 밖에 없는 구조로 보이는데 이런 단순한 차이가 실제로는 체감할 수 있는 엄청난 차이를 가져오는 것 같다.

 

구현에 있어서 편의성과 가독성, 성능을 모두 고려해야 하겠지만, 그리고 고려하겠지만, 무작정 습관적인 사용과 맹목적인 신뢰는 언제나처럼 금기시해야 한다는 걸 느낀다.