본문 바로가기
Web Programing!/JAVA / JSP

[JAVA / JSP] 자바 성능 향상 코딩

by 어설픈봉봉이 2011. 9. 23.
반응형
SMALL




일반 코딩 지침


1. 스트링 객체를 병합하는 행위가 많은경우 + 연산자 보다는 StringBuffer를 사용하는것이 쓸데없는 객체를 만드는 것을 방지할 수 있다. 자바에서 객체를 새로 생성하는 것은 객체 생성 자체가 비싼 작업이라는 점, 그리고 추후 가비지 컬렉터가 더 많은 일을 해야 한다는 점에서 성능에 별로 좋지 않은 영향을 미친다. 단순한 문자의 추가가 비효율적인 이유는 String 객체는 불변(immutable)이기 때문이다. 그러므로 "a"라는 문자열을 수정해서 "ab"라는 문자열로 바꿀 수는 없고 "ab"라는 새로운 객체를 생성해서 "a"를 치환해야만 한다. 이와는 달리 StringBuffer 객체는 문자열을 변경할 수 있다. 문자열을 계속적으로 추가해야만 한다면 StringBuffer를 사용하는 것이 훨씬 효율적이다.


2. StringBuffer를 사용할 경우 초기 용량을 지정한다. 기본 크기가 16바이트인데 이보다 커질경우 불 필요한 객체가 생성되기 때문에 가능하면 초기용량을 여유있게 잡아준다.


3. 스트링을 비교할 때 대소분자 구분하여 비교하는 행위는 가급적 피한다.


4. String 클래스에서 getBytes() 메소드는 계산량이 가장 많은 메소드이다. 코드에서 단 한번만이라도 getBytes()를 호출해보면 그것이 성능에 많은 영향을 미친다는 것을 알 수 있다. 이 메소드는 char 배열을 byte 배열로 바꿔주는 메소드인데 각각의 유니코드 캐릭터는 하나나 둘 또는 심지어 3개의 바이트로 변환이 되며 이를 위한 판단 작업도 뒤따라야 하므로 비싼 작업일 수 밖에 없다. 그러나 ASCII 문자의 경우는 문제가 간단해진다. 각각의 ASCII 문자는 2byte 유니코드에서 한 byte를 잘라버리고 남은 1byte만을 변환하면 된다. 그래서 ASCII같은 경우에는 charAt()을 사용하는것이 더 나은 방법이다.


5. StringTokenizer 클래스는 자바에서 있어서 프로그래머가 문자열을 파싱할때 간편하게 사용할 수 있는 강력하면서도 유연한 클래스이다. 그러나 강력하고 유연하다는 말은 고성능을 뜻하지는 않는다. 고성능의 코드는 주로 특수화된 코드이므로 단순화된 가정에 초점을 맞춰 작성된다.
일반적인 목적의 코드를 작성하기 위해서는 많은 제반 사항들을 고려해야 하므로 가정을 너무 많이 단순화시킬 수가 없다. JDK는 많은 자바 응용프로그램을 만족시켜야 하므로 일반적인 형태로 작성이 된다. 그래서 일반화된 StringTokenizer를 사용하기 보다는 가능하다면 indexOf()와 substring()을 사용하는것이 성능을 향상 시킬 수 있는 방법이다.


6. 한 문자를 체크하기 위해서 startsWith()를 사용하지 않는다. 이 경우에는 charAt()이 유용하다.


7. 디버깅용으로 System.out.println()을 사용한 경우는 운영 중에는 remark하는것이 최상이다.
특정 공통클래스에 trace()를 만들고 모든 클래스가 이 공통클래스의 trace()를 사용하는 경우,
대부분 운영자가 이 공통클래스의 trace모드를 off하기만 하는데 실제 Disk I/O를 안 한다 하더라도
이 메소드를 부르는 행위 자체만으로도 쓸데 없는 객체가 만들어지기 때문에 시스템 전체에
영향을 미친다.


8. 벡터를 자료 구조로 사용할 경우 벡터의 중간부분에 추가나 삭제가 빈번히 일어난다면 다른 자료구조를 고려한다. (Linked List, ArrayList etc)


9. 벡터를 사용할 경우 사용할 크기를 어느 정도 예측할 수 있다면 초기 용량을 지정하는것이 쓸데 없는 객체를 만들지 않는 방법이다. 벡터의 엘리먼트는 내부적으로 배열에 저장되어 있다.
기본적으로 객체가 생성되면 배열의 크기는 10이 된다. 만약에 엘리먼트가 늘어나서 엘리먼트의 갯수가 10을 넘어가면 디폴트로 2배의 크기가 되는 배열을 새로 생성하고 이전의 값들을 새로운 배열에 복사한 후 새로운 배열을 사용한다. 이전의 배열은 가비지 컬렉터의 대상이 되면서 버려진다. 이렇듯, 벡터 크기를 확장 시키는 것은 매우 비싼 작업이다.


10. 벡터의 엘리먼트를 얻기위해 반복하는 경우 Enumeration을 사용 하는것 보다는 elementAt( index )을 사용하는 것이 더 빠르다


11. Java2 에서는 Vector에서 동기화가 빠진 ArrayList라는 클래스를 제공한다.
그래서 만약에 단일 쓰레드 환경이라면 Vector 대신 ArrayList를 사용하는 것이 더 좋은 방안이다.


12. 벡터 클래스가 가지고 있는 또다른 문제점은 다음의 몇가지 메소드를 수행하기 위해서 엘리먼트를 처음부터 끝까지 다 훑어야 한다는 점이다.

- contains()
- indexOf()
- lastIndexOf()
- removeElement()
- remove()
- removeAllElements()
- clear()

이러한 것들은 다 비싼 작업이며, 그 비용은 Vector의 size에 비례할 것이다.


13. 해쉬테이블 및 Hash용 객체에서 버킷의 링크드 리스트의 길이는 가능한 짧을수록 좋다.
그러기 위해서는 객체가 삽입될때 동일한 버킷으로 삽입되는 일은 가능하면 없어야 한다. 이는 곧 키의 hashCode() 값이 가능하면 넓게 분포되는 것이 좋다는 말이다
해쉬테이블에서 링크드 리스트의 길이를 결정하는 또하나의 요인은 초기 용량(capacity)과 
부하율(load factor)이다. Hashtable의 초기 용량은 곧 버킷의 수이다. 또한 부하율은 현재의 버킷이 얼마만큼 찼을때 버킷의 수를 두배로 늘릴것인가(rehashing)하는 것이다.이 작업은 비싼 작업이다. 그래서 가능하면 초기 용량을 1.33배로 잡아 주는 것이 rehashing하는 것을 막고 성능을 향상 시킬 수 있는 방법이다.


14. 지속적으로 변하긴 하되 상대적으로 긴 수명을 가지는 값들에 대해서는 캐싱 전략을
세우는 것이 적절하다.


15. System.currentTimeMillis()는 매우 비싸다. 자바는 system clock에 의존하기 때문에 이를
위해서는 반드시 native call이 필요하다. java에서 Java Native Interface(JNI)를 거쳐가는 것은 비용이들기에 System.currentTimeMillis()에 드는 비용을 가볍게 보아서는 안된다.


16. Date 클래스는 랭귀지에 상관없이 처리하기에 매우 비싼 자원이다. 현재 시간을 물어본다면 1970년 1월 1일 이후 현재 시간까지를 초로 바꾼 다음 매우 복잡한 처리 과정을 거쳐서 'Fri Jul 02 16:38:41 PDT 1998'과 같이 변환해주어야 한다. 게다가 자바에서는 locale 까지도 반영을 하니 현재 시간 하나를 얻기 위해 얼마나 많은 비용을 지불해야 하는지 짐작할 수 있다.


17. 루프내에서 변치 않는 값을 미리 계산해 놓는 것도 고전적인 최적화 방법의 하나이다. 이는 루프가 실행되는 동안 값이 변하지 않으므로 static value 범주에 들어간다


18. 버퍼링은 바이트당 오버헤드를 최소화 할 수 있는 기법이다. 데이터를 보내기 위해서 자바에서는 OS 자체의 함수를 콜해야 하는데, OS 함수 자체는 한번 콜하는데 드는 비용이 한 바이트를 보내나 여러 바이트를 보내나 비슷하다(그 비용 또한 만만치도 않다).
그러므로 한번에 한 바이트씩만 보낸다면 여러 바이트를 묶어서 한번에 보내는 것보다 엄청나게 비싼 댓가를 치러야 되는 것은 자명한 사실이다. 자바에서 I/O stream을 이용해서
버퍼링을 하는 것은 아주 쉽다. 단지 원래의 output stream을 buffered stream으로 감싸기만 하면 된다.


19. 버퍼에 담겨 있는 데이터를 내보내는 flushing은 버퍼가 다 차면 자동으로 내보내는 식으로만은 사용할 수 없다. 예를 들어 일단 클라이언트 쪽에서 요청 명령을 보낸다음 서버쪽으로부터 어떤 데이터를 받는다고 했을 때 요청이 버퍼링이 되어있다면 서버쪽에 전달이 되질 않을테니 그러한 경우는 버퍼가 다 차지 않아도 즉각 데이터를 내보내야 한다.
반면에 그 후로 이어지는 일련의 데이터가 있다면 버퍼가 찰 때까지 기다렸다 한꺼번에 보내주는게 효율적이다. 이렇듯 플러슁은 그 시점이 나름대로 중요하므로 수동으로 적절히 조절할 필요가 있다. 그러나 비적절한 시점에 자주 플러슁을 하면 성능은 떨어진다.


20. Output stream은 대략 유니코드 문자열을 다루는 writer와 바이트 배열을 다루는 것으로 나눌 수 있다. 유니코드 문자열을 writer를 이용하여 내보낼 때 그 끝단은 아마 socket이나 file쯤이 될 것이다.
그런데 여기서의 문제점은 soket이나 file은 유니코드가 아니라 단지 바이트로만 처리 할 수 있다는데 있다.
그러므로 어딘가에서는 유니코드를 바이트로 변환을 해주어야 하는데 이는 매우 비싼 작업이다.


반응형