Java

[펌]자바성능을 향상시키는 코딩

_침묵_ 2005. 6. 15. 03:37

출처 : JavaService Net              

안녕하세요. 絶對高手입니다.
자바를 코딩하면서 본인도 모르게 자바의 성능이 저하시키는 코딩을 하는 경우가 많습니다.
그래서 어제 새벽내내 인터넷을 돌아다니면서 어떻게 하면 자바의 성능을 더 향상할 수 있
는지 조사했지요. 음...벌써 새벽 4시군요... -.- 어제는 새벽4시까지 술로 지새웠는데
오늘은 그래도 보람된 꺼리로 밤을 새는군요.

각설하고 자 그러면 어떻게 하면 좀더 빠르고 효율적인 코딩을 할 수 있는지 알아봅시다.

그리고 아래의 두 가지문제에 대해서 생각을 해보시길 바랍니다. 물론 저도 고민하고 있
습니다. 지금 jsp + JavaBeans로 구성된 프로젝트를 하고 있습니다. 기술회의를 하다가
이런 문제가 제기되었습니다.

1. Beans 객체에서 데이터를 쿼리해올 때 어떤 것이 더 빠른 성능을 낼 것인가에 대해
논란이 있었습니다. 즉 엔터티빈에 해당하는 클래스를 만들어서 그것을 벡터에 집어 넣고
서 화면에 뿌려줄 것인가, 아니면 String 배열을 선언하여 거기에 데이터를 집어넣고
이것을 화면에 뿌려줄 것인가.

2. 빈즈클래스를 상속하여 메서드를 부르는 게 나은지 그냥 new해서 그 클래스의 메서드
를 호출하는 게 좋은지 서로간에 이견이 있었습니다.

이글을 읽어 보시고 어느 것이 더 나을지 판단해보시길 바랍니다.
자 그럼 자바 코딩을 할 때 어떤 것이 더욱 나은 성능을 가져오는 코딩인지 잘못된 예와
이유, 개선방안을 살펴보도록 하죠.

1. String보다는 StringBuffer가 빠릅니다.
예) String str = “aaa”;
    str = str + “bbb”;
    뭐 이런식으로….하거나 +=를 아시는 분은
    str += “bbb”; 하시죠.
문제점) 코드상으로는 str이라는 변수는 하나이지만 실제로는 “aaa”에 “bbb”라는
새로운 스트링객체가 생성되었기 때문에 이 두 개를 카피한 후 원래 스트링객체의 메모리
가 해제(Garbage Collection)되면서 새로운 str=”aaabbb”가 생깁니다.

해결책) StringBuffer의 경우 하나의 객체에 계속적으로 추가하는 식이 되므로 새로운
객체가 생성되지 않을 뿐더러, garbage collection할 필요가 없지요.

그러므로 StringBuffer sb = new StringBuffer();
         sb.append(“aaa”);
         sb.append(“bbb”);
로 코딩해야겠습니다.

2. 같은 기능이더라도 더 빠른 연산자가 있습니다.

(1)증감연산자가 가장 빠릅니다.
   예) int j = 1;
   j = j + 1;
  (j의 값을 1증가 시키려고 이렇게 코딩하는 사람은 많이 못봤지만…혹시나해서)
   이는 j++;로 코딩이 되어야 겠죠.

(2) +=을 사용하는게 빠릅니다.
  예)  a  = a + 10;보다는 a+=10;을 해주셔야겠죠.

(3) 곱셈나눗셈보다는 shift연산이 빠릅니다.
(shift를 하면 2를 곱하거나 나눌수 있지요)

3. 객체를 마구 만들지 않아야 겠습니다.
- 객체(String 포함)는 초기화되면 모두 힙이라는 메모리 영역에 할당이 됩니다.
객체를 마구잡이로 많이 만들면, 여기에 억세스 하는데 로드 및 이것이 해제될 때
Garbage Collection대상이 많아 진다는 점에서 불리합니다
-그래서 반복문안에서 String str = “xxx”;하거나 XXX x = new XXX()하면 얼마나
무리가 가는지 아시겠죠….지금이라도 혹시 while이나 for루프 안에 그런 코드가 들어
있지는 않은지 확인해보세요.
- 또한 인스턴스 변수는 클래스 초기화시 자동적으로 값(String이나 class는 null,
숫자는 0, boolean은 false)이 할당되므로 의도적으로 String var = null;할 필요가
없겠지요.
   
4. 변수도 범위와 타입에 따라서 성능이 다릅니다.
- 범위를 따지면 지역 메쏘드 변수가 스택영역에 저장되므로 가장 빠릅니다.
- 타입을 따지면 int와 참조 변수가 가장 빠릅니다. 혹시 int로 해도 될 것에
long으로 선언해서 쓰시는 분은 안계시죠? (본인이 그런 적이 있었지요…-.-;;)

5. 배열을 초기화에 따른 다른 특성을 잘 이해해야겠습니다.
- 다차원 배열로 정의할 경우 매번 생성자를 호출하기 때문에 꼭 필요한 경우가 아니면
다차원 배열로 정의하지 않아야겠습니다.
- 배열이 지역변수일 경우 메쏘드 호출 시 매번 초기화를 수행하므로, 배열을 static
으로 선언하면 초기화가 반복되는 것을 제거할 수 있습니다.
- 매번 메쏘드 호출 시 배열의 초기화가 필요할 경우, 한번 초기화된 배열을 카피해서
그 카피본을 사용하는 것이 더 빠릅니다.

6. 메소드도 자주 호출하면 부하가 걸립니다.
- 누가 자꾸 자기를 부르면 짜증나겠죠? 메소드를 부른다는 것은 그 메소드가 위치한
메모리를 참조한다는 의미입니다. 특히 자바는 객체지향언어이기 때문에 동적인메서드의
호출이 발생합니다. 즉, 메서드 오버로딩의 경우 정해진 메서드를 부르는게 아니라
그때 상황(메서드 인자, 리턴값)에 따라 판단해서 호출이 이뤄지므로 될 수 있으면 적게
부르는 방향으로 로직을 세워서 코딩을 해야겠습니다.
-  그래서 자주쓰는 메쏘드는 static으로 선언하고, 자주 참조하는 변수의 경우 final
등의 키워드를 사용하여 선언하면, 메모리에 고정이 되므로 성능을 높일 수가 있습니다.

7. 제어구조도 어떻게 하느냐에 따라 실행시간이 다릅니다.
- 즉, 군대에서 연병장을 뺑뺑이 돈다고 생각해보면 간단하죠. 몇바퀴를 돌리느냐, 단독
군장이냐, 완전군장이냐에 따라서 걸리는 시간이 틀리겠죠?
- 앞에서 지적했듯이 불필요한 메서드호출이나, 특히 객체생성은 자제해야겠습니다.
- 마찬가지로 로컬 메서드변수를 써서 카운팅을 해야겠죠. 혹시 인스턴스변수에 잡아두고
그 값을 증가시켜 카운팅하시는 분은 안계시겠죠?
- 배열의 바운드를 체크해야 할 경우, try-catch구문을 쓰시면 비교를 수행할 필요가
없기 때문에 실행시간이 줄어듭니다.
    
8. 적절한 버퍼를 사용하세요.
- 예전에 땅굴에 근무할 때 안에 샘물이 있는데 여기는 물이 한방울씩 똑똑 떨어집니다.
이것을 마시기 위해서 밑에서 입대고 있으면 성도 안차고 힘들겠죠. 뭐 당연한 원리지만
컵이나 주전자를 대놓고 물방울을 모아서 마시면 개운하듯이, 데이터를 읽어오거나 쓸 때
BufferedInputStream, BufferedOutputStream을 써서 버퍼링을 하면 오버헤드를 줄일
수 있습니다.
   
9. 동기화
- 동기화는 쓰레드가 하나의 자원을 놓고 싸우지 말고 락을 가진 쓰레드부터 차례로 그
자원을 쓰고 다음 쓰레드에게 락과 함께 자원을 반납하는 것이죠.
- 이때 주의할 점은 동기화를 안해도 되는 부분을 괜히 동기화하면 쓰레드가 하나밖에
수행되지 못하니까 잘 판단해야 한다는 것입니다.
- 동기화하는 방법은 메서드를 동기화하거나 블락을 동기화할 수 있는데 동기화된 메쏘드
를 호출하는 것이 동기화된 블록을 호출하는 것보다 약간 빠릅니다.
- 동기화된 메서드에서 동기화된 다른 메서드를 부를 경우에는 호출되는 메서드를
private 비동기화메서드로 바꾸면 모니터를 획득하는데 시간을 줄일 수 있겠습니다.

10. 동적바인딩과 동적클래스로딩
- 동적바인딩이란 프로그램 실행중 함수가 호출될 때 그 메모리 참조를 알아내는 것을
뜻합니다. 이는 메서드 오버로딩이나 오버라이딩 구현개념이죠. 즉, 메서드가 같은 이
름이라도 인자나 리턴값에 따라서 동적으로 호출되니, 호출될 때 그 메서드를 참조해야
하는 것이죠. 이때 메서드가 호출된 메모리참조를 알아내는 작업을 수행하는 시간이 약
간의 시간을 잡아 먹습니다. 그렇다고 메서드 오버로딩이나 오버라이딩을 안할 수는
없겠죠?

동적클래스로딩은 실행중인 클래스가 다른 클래스를 참조하였을 경우에 일어납니다.
이때 그 클래스를 직접 참조로 바꾸어 로딩을 하며, JVM에 의해 안전성을 검사가 이뤄진
후 클래스 초기화가 이뤄집니다. 이 역시 시간이 좀 걸리겠죠. 음….코드안에 마구
new해서 클래스의 인스턴스를 마구 만들어 대면 좋은 성능을 바라는 건 무리겠죠.

자바도 파고들면 그리 쉬운 언어가 아닌것 같네요...쩝...
모두들 열심히 하시길 바라면서, 잘못된 부분이 있으면 리플 부탁드립니다.
그리고 처음에 저희가 고민한 2가지 문제에 대한 의견도 같이 부탁해요.

※주의: 이글은 충남대학교 컴퓨터과학과 김영국(ykim@cs.chungnam.ac.kr)님이
인터넷상에 올린 글을 참조로하여 썼음을 분명히 알려드립니다.

-----------------------------------------------------------------------------

출처:알수없음                 

일반 코딩 지침


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()을
사용하는것이 더 나은 방법이다.

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

----------------------------------------------------------------------------------------


http://www.javastudy.co.kr/javastudy/new_bbs/qna_view.jsp?bbs_name=lecadvancebbs&theid=34&pageNum=1


Java 성능개선을 위한 Programming 기법


JDK1.3버전 이후로 지원되는 HotSpot VM은 기본적으로 Hip에 동적으로 할당된 Object는 거의 회수할 수 있다고 한다. 하지만 이 기능으로 인해서 VM은 엄청난 OverHead를 가지게 된다. 무리한 Object의 생성은 생성 당시에도 많은 OverHead를 초래하지만, 생성된 Object를 회수하기 위해서는 더 많은 작업이 요구된다. 이를 해결하기 위한 몇 가지 Tip이 있는데, Avoiding Garbage Collection, Object 재사용, static instance variable의 사용에 의한 단일 클래스 인스턴스 구현 방법 등이 그것이다. 핵심은 가능한 Object 생성을 피하자는 것이다.


▶ Avoiding Garbage Collection(static method사용)


예제1)
String string="55";
int theInt=new Integer(string).intValue();

 

예제2)
String string="55";
int theInt=Integer.parseInt(string);

 

예제1)에서는 Integer 클래스를 생성한 다음 string에서 정수값을 추출해 냈다. Object를 생성하고 초기화하는 과정은 상당한 cpu 집약적인 작업이고, Hip 영역에 Object가 생성되고 수집되기 때문에 가비지 컬렉터 작업을 수반한다. 가능한 Object의 Instance가 필요 없는 static 메소드를 사용한다. 예제2) 참조.


▶ Avoiding Garbage Collection(임시 Object 생성 금지)


가장 흔한 예로 String Object의 append를 위해서 (+) 연산을 사용하는 것을 들 수 있다. (+) 연산자를 사용해서 String Object를 append할 경우 우리가 생각하는 것 보다 훨씬 더 많은 임시 Object가 생성되고, 가비지 컬렉터에 의해 다시 수집된다. String Object의 append연산을 위해서는 StringBuffer 클래스를 사용한다. 예제1)과 예제2) 참조

 

예제1)
String a="Hello";
a=a+"World";
System.out.println(a);

 

예제2)
StringBuffer a=new StringBuffer();
a.append("Hello");
a.append("World");
System.out.println(a.toString());

 

어떤 메소드는 Object의 복사본을 반환하는 경우가 있다. 대표적인 예로 스트링 클래스의 trim() 메소드를 들 수 있다. trim()메소드가 수행되면 기존의 Object는 수집되고, 기존 Object의 복사본이 사용되게 된다. 임시 Object 생성과 복사본을 반환하는 메소드가 루프 안에서 사용될 경우 무수히 많은 Object가 생성되고 수집되기 때문에 심각한 문제를 야기하게 된다. 루프 내부에서 Object를 생성하는 것은 가급적 피해야 한다.

 

예제3)
for(i=0;i<1000;i++){
  Date a=new Date();
  :
}

 

예제4)
Date a;
for(i=0;i<1000;i++){
  a=new Date();
  :
  a=null;
}

 

예제3)과 예제4)는 현재 날짜와 시간을 얻어오기 위해 루프 안에서 Object를 생성했다. 보통 set으로 시작하는 메소드는 Object의 Instance 값을 재정의 한다. API를 충분히 참조한 다음 지원 메소드가 없을 경우, 클래스를 상속받아 요구에 적합한 메소드를 만드는 방법도 고려할 필요가 있다. 기존 API를 이용한 Object 초기화 방법은 아래 Object 재사용(메소드를 사용한 Object 초기화) 부분 참조.


▶ Avoiding Garbage Collection(primitive data type 사용)


Date 클래스나 String 클래스의 값들중 int나 long 형으로 표현하여 사용할 수 있는 경우가 있다. 예를 들어

String a="1492";
String b="1997";

과 같을 경우 a와 b는 굳이 String 형으로 표현하지 않아도 된다. 하지만 여기서 주의할 점은 Object의 값을 기본 데이타형으로 Casting 하는 작업이 오히려 시간이 더 많이 걸릴 수도 있는 것이다.
클래스의 인스턴스 변수로 기본 데이타형을 사용하면 Object의 크기도 줄어들고, Object 생성 시간도 줄일 수 있다.


▶ Object 재사용(Pool Management)


Object 재사용 기법으로 흔히 Pool Management 기법을 사용한다. 이는 임의 갯수의 Object를 미리 생성해 두고 이를 Vector 클래스를 사용해서 관리하는 방법이다. 해당 Object를 사용하기 위해서 Pool의 Vector에 있는 Object를 하나 가져오고, 다 사용하고 나면 다시 Pool에 반납한다. 이는 기존에 공개되어 있는 Hans Bergsten의 Connection-Pool의 PoolManager클래스에서 사용되고 있다.(Professional JAVA Server Programming. 정보문화사. 2000.4.4. 제9장 연결풀링 부분 참조) Object Pool을 사용할 경우, 반환되는 Object가 초기화되어 반환되지 않을 경우 다음에 Pool에서 Object를 가져와서 사용하게 되면 문제를 야기할 수 있기 때문에 초기 클래스 Design시 꼼꼼하게 따져 봐야 한다.


▶ Object 재사용(메소드를 사용한 Object 초기화)


예제1)
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb=null;
sb=new StringBuffer();
sb.append("World");
out.println(sb.toString());

 

예제1)과 같이 사용할 경우 하나의 인스턴스 변수를 사용하기는 하지만, 두 번의 초기화 과정을 거치게 된다.

 

예제2)
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb.setLength(0);
sb.append("World");
out.println(sb.toString());

 

위와 같이 각 클래스에서 지원해 주는 메소드를 사용하여 Object를 재사용 할 수 있다.


▶ static instance variable의 사용에 의한 단일 클래스 인스턴스 구현


다음은 Hans Bergsten의 PoolManager 클래스 코드 중 일부다.

public class PoolManager{
  static private PoolManager instance;
  :
  private PoolManager(){
    init();
  }
  static synchronized public PoolManager getInstance(){
    if (instance == null){
      instance = new PoolManager();
    }
    :
    return instance;
  }
  private void init(){
    :
  }

 

PoolManager형의 인스턴스가 static으로 선언되어 있다. getInstance() 메소드는 현재 생성되어 있는 PoolManager의 Object를 조사하고 만약 Object가 있으면 Object를 반환하고 없으면 생성자를 호출해서 PoolManager의 Object를 생성한 후 반환한다. 결국 JVM 내부에는 하나의 PoolManager Object가 존재하게 된다. 단일 클래스 인스턴스 기법을 사용할 경우 하나의 인스턴스를 사용하기 때문에 해당 인스턴스의 무결성 부분이 문제가 된다. 이를 위해 다양한 Synchronization 기법을 사용하게 된다. 아래 I/O 퍼포먼스 개선 부분 참조.


▶ clone() 메소드 사용으로 Object 생성에 따른 OverHead를 피함

 

private static int[] data = new int[2][2];
int[] someMethod(){
  int[] a = (int[])this.data.clone();
  return a;
}

 

대부분의 클래스들에는 clone() 메소드가 존재한다. clone() 메소드가 호출되면 Object의 복사본을 반환하는데, 대신 클래스의 생성자를 호출하지 않기 때문에 생성자 호출에 의한 OverHead를 피할 수 있다. clone() 메소드를 사용할 때의 trade-off 문제는 다음에 예제를 참조 할 것.

 

static int[] Ref_array1={1,2,3,4,5,6,7,8,9};
static int[][] Ref_array2={{1,2},{3,4},{5,6},{7,8}};

int[] array1={1,2,3,4,5,6,7,8,9}; //faster than cloning
int[] array1=(int[])Ref_array1.clone(); //slower than initializing

int[][] array2={{1,2},{3,4},{5,6},{7,8}}; //slower than cloning
int[][] array2=(int[][])Ref_array2.clone(); //faster than initializing


▶ Method Inline에 의한 method호출 감소


예제1)
public class InlineMe{
  int counter=0;
  public void method1(){
    for(int i=0;i<1000;i++){
      addCount();
      System.out.println("counter="+counter);
    }
    public int addCount(){
      counter=counter+1;
      return counter;
    }
    public static void main(String args[]){
      InlineMe im=new InlineMe();
      im.method1();
    }
}

 

예제1)에서 addCount() 메소드를 다음과 같이 수정하면

public void addCount(){
  counter=counter+1;
}

 

위와 같이 수정할 경우 addCount() 메소드는 컴파일시 Inline 되어서 실제 메소드를 호출하지 않고 같은 결과를 반환한다. 즉 method1()이 실제 수행될 때는 다음과 같이 수행.

 

public void method1(){
  for(int i=0;i<1000;i++){
    counter=counter+1;
    System.out.println("counter="+counter);
  }


▶ 생성자 설계


예제1,2,3) 모두 같은 역할을 하는 생성자들로 구성되어 있다. 하지만 퍼포먼스 측면에서 보면 예제3)이 가장 효율적이다. 하지만 클래스를 설계 할 때 예제1)과 같이 해야 할 때도 있다. 클래스가 요구하는 조건에 따라 생성자의 설계방법이 다르겠지만, Object를 생성할 때 가능한 생성자를 적게 호출하는 방법을 사용하는 것이 퍼포먼스 면에서 좋다는 것은 당연한 일이다.

 

예제1)
예제2)
예제3)
class SlowFlow{
  private int someX, someY;
  SlowFlow(){
    this(777);
  }
  SlowFlow(int x){
    this(x,778);
  }
  SlowFlow(int x, int y){
    someX=x;
    someY=y;
  }
}
class SlowFlow{
  private int someX, someY;
  SlowFlow(){
    this(777,778);
  }
  SlowFlow(int x){
    this(x,778);
  }
  SlowFlow(int x, int y){
    someX=x;
    someY=y;
  }
}
class SlowFlow{
  private int someX, someY;
  SlowFlow(){
    someX=777;
    someY=778;
  }
  SlowFlow(int x){
    someX=x;
    someY=778;
  }
  SlowFlow(int x, int y)
    someX=x;
    someY=y;
  }
}

 

▶ "extends" VS "implements"


예제1) extends
import java.awt.event.*;
import java.awt.*;
public class MyWindowAdapter extends WindowAdapter{
  public void windowClosing(WindowEvent we){
    Container source=(Container)we.getSource();
    source.setVisible(false);
  }
}

 

예제2) implements


import java.awt.event.*;
import java.awt.*;
public class MyWindowListener implements WindowListener{
  public void windowClosing(WindowEvent we){
    Container sourve=(Container)we.getSource();
    source.setVisible(false);
  }
  public void windowClosed(WindowEvent we){}
  pubic void windowActivated(WindowEvent we){}
  public void windowDeactivated(WindowEvent we){}
  public void windowIconified(WindowEvent we){}
  public void windowDeiconified(WindowEvent we){}
  public void windowOpened(WindowEvent we){}

 

"implements"의 경우에는 특정 메소드를 구현하고 인터페이스에 정의된 모든 메소드를 코딩해야 하기 때문에 코드의 낭비를 초래하는 반면, “extends"의 경우에는 슈퍼 클래스에 정의된 메소드들 중 필요한 메소드만 overriding 하면 된다. ==> 설계 시 추상클래스를 사용할 것인지 인터페이스를 사용할 것인지 고려.


▶ 클래스 집약


예제1)
public class Person{
  private Name name;
  private Address address;
}
class Name{
  private String firstName;
  private String lastName;
  private String[] otherNames;
}
class Address{
  private int houseNumber;
  private String houseName;
  private String streetName;
  private String town;
  private String area;
  private String greaterArea;
  private String country;
  private String postCode;
}

 

예제1)에서 정의된 Person 클래스는 Name형의 name과 Address형의 address, 두 개의 인스턴스 변수를 가진다. 클래스를 설계할 때 가능한 클래스의 수를 줄여서 수행 시 동적으로 생성되는 Object의 수를 줄일 수도 있다. 예제1)에서 정의된 세 개의 클래스는 하나의 클래스로 집약될 수 있다.

 

예제2)
public class Person{
  private String firstName;
  private String lastName;
  private String[] otherNames;
  private int houseNumber;
  private String houseName;
  private String streetName;
  private String town;
  private String area;
  private String greaterArea;
  private String country;
  private String postCode;
}


▶ I/O 퍼포먼스 개선


자바에서는 자료를 읽거나 쓰기 위해 stream을 사용한다. 자바는 두가지 형태의 stream을 지원한다. Readers and Writers와 Input and Output stream이 그것이다. Reader and Writers는 high-level의 I/O(예. String)를 지원하고 Input and Output stream은 low-level의 I/O(byte)를 지원한다. 속도 향상을 위해서는 Buffered stream을 사용한다. Buffered stream을 사용할 경우 버퍼의 기본값은 2K이다. 이 값은 조정될 수 있으나, 자료의 용량이 클 경우 메모리가 많이 필요하기 때문에 Buffered stream을 사용할 경우 여러 가지 사항을 고려해야 한다.

 

예제1) Simple file copy
public static void copy(String from, String to) throws IOException{
  InputStream in=null;
  OutputStream out=null;
  try{
    in=new FileInputStream(from);
    out=new FileOutputStream(to);
    while(true){
      int data=in.read();
      if(data==-1)
        break;
     out.write(data);
    }
    in.close();
    out.close();
  }finally{
    if(in!=null){in.close();}
    if(out!=null){out.close();}
  }
}

 

Buffered stream을 사용하지 않고 I/O를 했을 경우의 예제이다. 370K의 JPEG 파일을 복사하는데 10800ms.

 

예제2) Faster file copy
public static void copy(String from, String to) throws IOException{
InputStream in=null;
OutputStream out=null;
try{
in=new BufferedInputStream(new FileInputStream(from));
out=new BufferedOutputStream(new FileOutputStream(to));
while(true){
int data=in.read();
if(data==-1)
break;
out.write(data);
}
}finally{
if(in!=null){in.close();}
if(out!=null){out.close();}
}
}

 

Bufferd stream을 사용해서 퍼포먼스를 개선한 예제이다. 예제1)과 같은 파일을 복사하는데 130ms.

 

예제3) Custom buffered copy
public static void copy(String from, String to) throws IOException{
InputStream in=null;
OutputStream out=null;
try{
in=new FileInputStream(from);
out=new FileOutputStream(to);
int length=in.available();
byte[] bytes=new byte[length];
in.read(bytes);
out.write(bytes);
}finally{
if(in!=null){in.close();}
if(out!=null){out.close();}
}
}

 

while루프를 사용하지 않고 배열을 사용함으로서 퍼포먼스를 개선한 예제이다. 예제1)과 같은 파일을 복사하는데 33ms. 하지만 예제3)은 byte배열로 선언되는 메모리 버퍼의 크기가 실제 파일의 크기와 동일해야 한다. 이에 따라 두 가지의 문제점이 발생할 수 있다. 첫 번째는 파일의 용량이 클 경우 상당한 메모리 낭비를 초래한다는 점이다. 두 번째 문제점은 copy()메소드가 수행될 때마다 new byte[]에 의해 버퍼가 새로 만들어진다는 점이다. 만일 파일의 용량이 클 경우 버퍼가 만들어지고 Garbage Collector에 의해 수집될 때 상당한 OverHead를 초래할 수 있다.

 

예제4) Improved custom buffered copy
static final int BUFF_SIZE=100000;
static fianl byte[] buffer=new byte[BUFF_SIZE];
public static void copy(String from, String to) throws IOException{
InputStream in=null;
OutputStream out=null;
try{
in=new FileInputStream(from);
out=new FileOutputStream(to);
while(true){
synchronized(buffer){
int amountRead=in.read(buffer);
if(amountRead==-1)
break;
out.write(buffer,0,amountRead);
}
}finally{
if(in!=null){in.close();}
if(out!=null){out.close();}
}
}

 

크기가 100K인 byte 배열을 임시버퍼로 지정하고, 이를 static으로 선언함으로서 퍼포먼스를 개선했다. 예제1)과 같은 파일을 복사하는데 22ms. static buffer의 사용으로 I/O작업을 수행할 경우 발생할 수 있는 문제점을 해결하기 위해 synchronized block을 사용했다. 비록 synchronization을 사용함에 따라 성능 저하를 초래하지만, 실제 while 루프에 머무는 시간이 극히 짧기 때문에 퍼포먼스에 문제는 없다. 테스트에 의하면 synchronized 버전과 unsynchronized 버전 모두 같은 시간에 수행을 완료했다.


▶ 웹 환경에서 Caching을 이용한 자바 퍼포먼스 개선


웹 환경에서 Object caching 기법은 주로 DB나 파일에서 동일한 내용을 가져오는 루틴에서 사용된다. DBMS에 sql문을 던지고 결과를 받아오는 부분의 내용이 거의 변동이 없을 경우 요청 시마다 매번 sql문을 실행시켜서 결과를 받아오는 것이 아니라 최초 실행된 값을 그대로 반환하는 기법이다. 그리고 시간 Check 기법을 이용해서 특정 시간 경과 후 요청이 들어오면 이전 요청에 의해 수행되어진 값을 갱신해서 반환한다. 결과에 의한 반환값이 메모리에 부담이 되지 않을 정도로 크지 않은 경우, 실시간으로 변경된 정보를 반환값으로 사용하는 루틴이 아닐 경우 유용하게 사용될 수 있다.

 

▶ Performance CheckList


. 임시로 사용하기 위해 Object를 생성하는 것을 피하라. 특히 Loop에서..
. 빈번하게 호출되는 메소드에서 Object를 생성하는 것을 피하라.
. 가능한 Object를 재사용 하라.
. 임시 Object의 생성을 줄이기 위해 데이타 타입 컨버전 메소드를 재정의 하는 방법을 고 려하라.
. 메소드 설계 시 데이타를 유지하고 있는 Object를 반환하는 메소드보다 데이타로 채워진 재사용 가능한 Object에 접근하는 메소드를 정의하라.
. string이나 Object를 integer 로 대치하라. Object 비교를 위해 equal() 메소드를 호출하지 말고 기본 데이타 타입의 == 연산자를 사용하라.
. 인스턴스 변수로 기본 데이타 타입을 사용하라.
. 단지 메소드 호출을 위해 Object를 생성하는 것을 피하라.
. String 연속 연산자 (+)를 사용하는 것보다 StringBuffer 클래스를 사용하라.
. 복사본을 생성하는 메소드 보다 Object를 직접 수정하는 메소드를 사용하라.
. 생성자는 간단하게... 상속 계층은 얕게...
. 인스턴스 변수를 초기화 하는 것은 한 번 이상 하지 말 것.
. 생성자 호출을 피하기 위해 clone() 메소들 사용할 것.
. 간단한 배열일 경우에는 초기화를.. 복잡한 배열일 경우에는 clone() 메소드 호출.
. 프로그램 내부에서 Object 생성 시기를 조절해서 Object 생성에 따른 bottlenecks를 없앤 다.
. 어플리케이션 내부에서 여분의 시간이 허용된다면 가능한 Object를 빨리 생성하라. 생성 된 Object를 내부적으로 유지하고 있다가 요청이 발생하면 할당하라.
. 사용 가능성이 희박하거나, 분산처리에 의해 Object를 생성할 경우 Object는 생성 시기를 늦춰라.