IT_Programming/Java

자바 성능을 결정 짓는 코딩 습관과 튜닝 이야기 정리 3 ~ 12

JJun ™ 2009. 10. 10. 12:00

 

Blog2Book 자바 성능을 결정짓는 코딩 습관과 .zip

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

 

3. 내가 만든 프로그램의 속도를 알고 싶다

 

 구  분

특  징 

 프로파일링 툴

 

소스 레벨의 분석을 위한 툴이다.

애플리케이션의 세부 응답 시간까지 분석 할 수 있다.

메모리 사용량을 객체나 클래스, 소스의 라인 단위까지 분석 할 수 있다.

가격이 APM툴에 비해서 저렴하다.

보통 사용자 수 기반으로 가격이 정해진다.

자바 기반의 클라이언트 프로그램 분석을 할 수 있다.

 

 APM 툴

 

애플리케이션의 장애 상황에 대한 모니터링 및

문제점 진단이 주 목적이다.

서버의 사용자 수나 리소스에 대한 모니터링을

할 수 있다. 실시간 모니터링을 위한 툴이다.

가격이 프로파일링 툴에 비하여 비싸다.

보통 CPU 수를 기반으로 가격이 정해진다.

자바 기반의 클라이언트 프로그램 분석이 불가능하다.

 

 

 

System 클래스

  • static void arraycopy(Object src, int srcpos, Object dest, int destpos, int length): 특정 배열을 복사할 때 사용한다.
  • static Properties getProperties() : 현재 자바 프로퍼티 값들을 받아온다.
  • static String getProperty(String key) : key에 지정된 자바 프로퍼티 값을 받아 온다.
  • static String getProperty(String key, String def) : key에 지정된 자바 프로퍼티 값을 받아온다. def는 해당 key가 존재하지 않을 경우 지정할 기본값이다.
  • static void setProperties(Properties props) : props 객체에 담겨 있는 내용을 자바 프로퍼티에 지정한다.
  • static String setProperties(String key, String value) : 자바 프로퍼티에 있는 지정된 key의 값을 value로 변한한다.
  • static Map<String,String> getenv: 현재 시스템 환경을 스티링 형태의 맴으로 리턴한다.
  • static String getenv(String name) : name에 지정된 환경 변수의 값을 얻는다.
  • static void exit(int status) : 현재 수행중인 자바 VM을 멈춘다. 이 메소드는 절대로 수행되선 안된.
  • static void load(String filename) : 파일명을 지정하여 네이티브 라이브러리를 로딩한다.
  • static void loadLibrary(String libname) : 라이브러리 이름을 지정하여 네이티브 라이브러리를 로딩한다.
  • static void gc() : GC를 수행하는 메소드, 일반 웹에서는 사용하면 안됨
  • static void runFinalization() : Object 객체 있는 finalize()라는 메소드는 자동으로 호출 되는데 가비지 콜렉터가 알아서 해당 객체를 더 이상 참조할 필요가 없을 때 호출한다. 하지만

    이 메소드가 호출되면 참조 해제 작업을 기다리는 모든 객체의 finalize()메소드를 수동으로 수행한다. 일반웹에서는 사용하면 안됨.

  • static long currentTimeMillis() : 현재 시간을 ms로 리턴한다.(1/1000초)
  • static long nanoTime() : 현재의 시간을 ns로 리턴힌다.(1/1,000,000,000 초) JDK5.0이상 가능

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

 

 

4. 왜 자꾸 String을 쓰지 말라는 거야?

  

String vs StringBuffer vs StringBuilder

  1. <%
        final String aValue = "abcde";
        for(int outLoop=0;outLoop<10;outLoop++)
        {
            String a = new String();
            StringBuffer b = new StringBuffer();
            StringBuilder c = new StringBuilder();
           
            for( int loop=0;loop<10000;loop++)
            {
                a+=aValue;
            }
            for( int loop=0;loop<10000;loop++)
            {
                b.append(aValue);
            }
            String temp = b.toString();
            for( int loop=0;loop<10000;loop++)
            {
                c.append(aValue);
            }
            String temp2 = c.toString();
        }
    %>
    OK
    <%= System.currentTimeMillis() %>

결과

 주요 소스 부분

 응답시간

 비고

   a+="a";  95,801ms  95초

  b.append(aValue);

  String temp = b.toString();

 247ms

 14ms

 0.24초

  c.append(aValue);

  String temp2 = c.toString();

 174ms

 13ms

 0.17초

 주요 소스부분

 메모리 사용량

 생성된 임시 객체수

 비고

 a+=aValue;  100,102,000,000  4,000,000  약 95GB

b.append(aValue);

String temp = b.toString();

 29,493,600

 10,004,000

 1,200

 200

 약 28MB

 약 9.5MB

c.append(aValue);

String temp2 =

             c.toString();

 

 29,493,600

 10,004,000

 1,200

 200

 약 28MB

 약 9.5MB

 

위의 결과에서 확인한 바와 같이 StringBuffer & StringBuilder의 사용을 추천함.

 

원인은 String의 경운 새로운 메모를 생성하고 추가하여 저장하지만 StringBuffer & StringBuilder는 기존의

객체에 변경된 값을 저장한다. String은 짧은 문자열을 더할 경우 사용한다.

 

StringBuffer는 스레드에 안전한 프로그램이 필요할 때나, 개발 중인 시스템의 부분이 스레드에 안전한지

모를 경우 사용하면 좋다. StringBuilder는 스레드에 안전한지 여부가 전혀 관계 없는 프로그램을 개발할 때

사용하면 좋다.

 

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

 

5. 어디에 담아야하는지....

 

Collection : 가장 상위 인터페이스

Set : 중복을 허용하지 않는 집합을 처리하기 위한 인터페이스이다.

SortedSet : 오름차순을 갖는 Set 인터페이스이다.

 

List : 순서가 있는 집합을 처리하기 위한 인터페이스이기 때문에 인덱스가 있어 위치를 지정하여 값을

        찾을 수 있다. 중복을 허용하며, List 인터페이스를 상속받는 클래스 중 가장 많이 사용하는 것으로

        Vector가 있다.

 

Queue : 여러 개의 객체를 처리하기 전에 담아서 처리할 때 사용하기 위한 인터페이스이다.

             기본적으로 FIFO를 따른다.

 

Map : 키와 값이 쌍으로 구성된 객체의 집합을 처리하기 위한 인터페이스이다. 중복키를 허용하지 않는다.

SortedMap : 키를 오름차순으로 정렬하는 Map 인터페이스이다.

 

 

** Set

  • HashSet : 데이터를 해쉬테이블에 담는 클래스로 순서없이 저장된다.
  • TreeSet : red-black 이라는 트리에 데이터를 담는다. 값에 따라서 순서가 정해진다. 데이터를 담으면서 정렬할때 유용하다.
  • LinkedHashSet : 해쉬 테이블에 데이터를 담는데 저장된 순서에 따라서 순서가 결정된다.

 

** List

  • Vetor : 크기를 객체 생성시에 지정할 필요가 없는 배열 클래스이다.
  • ArrayList : Vector와 비슷하지만, 동기화 처리가 되어 있지 않다.
  • LinkedList : ArrayList와 동일하지만, Queue 인터페이스를 구현했기때문에 FIFO규 작업을 수행한다.

 

** Map

  • Hashtable : 데이터를 해쉬 테이블에 담는 클래스이다. 내부에서 관리하는 해쉬 테이블 객체가 동기화 되어 있으므로, 동기화가 필요한 부분에서는 이 클래스를 사용하기 바란다.
  • HashMap : 데이터를 해쉬 테이블에 담는  클래스이다. HashTable 클래스와 다른 점은 null 값을 허용한다는 것과 동기화되어 있지 않다는 것이다.
  • TreeMap : red-black 트리에 데이터를 담는다 TreeSet과 다른 점은 키에 의해서 순서가 정해진다는 것이다.
  • LinkedHashMap : HashMap과 거의 동일하며, 이중 연결 리스트 라는 방식을 사용하여 데이터를 담는다.

 

** Queue

  • PriorityQueue : 큐에 추가된 순서와 상관없이 먼저 생성한 객체가 먼저 나오도록 되어 있는 큐이다.
  • LinkedBlockingQueue : 선택적으로 저장할 데이터의 크기를 정할 수도 있는 FIFO기반의 링크 노드를 사용하는 블로킹 큐이다.
  • ArrayBlockingQueue : 저장되는 데이터의 크기가 정해져 있는 FIFO 기반의 블로킹 큐이다.
  • ProrityBlockingQueue : 저장되는 데이터의 크기가 정해져 있지 않고, 객체의 생성 순서에 따라 저장되는 블로킹 큐이다.
  • DelayQueue : 큐가 대기하는 시간을 지정하여 처리하도록 되어 있는 큐이다.
  • SynchronousQueue : put() 메소드를 호출하면, 다른 스레드에서 take()메소드가 호출될 때까지 대기하도록 되어 있는 큐이다. 이큐에는 저장되는 데이터가 없다.

 

Sun에서 정리한 안정적인 클래스는 아래와 같다.

 인터페이스

 클래스

 Set

 HashSet

 List

 ArrayList

 Map

 HashMap

 Queue

 LinkedList

 

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

 

6. 지금까지 사용하던 for 루프를 더 빠르게 할 수 있다고 ?

 

조건문의 속도는?

  • if~else
  • switch

 

일반적으로 if문에서 분기를 많이하면 시간이 많이 소요된다고 생각하나, 그렇지 않다.

if 조건 안에 들어가는 비교구문에서 속도를 잡아먹지 않는 한, if문장 자체에서는 그리 많은 시간이 소요되지 않는다. Sun에 있는 문서를 보면 switch는 숫자 비교 시 if 보다 가독성이 좋아지므로 정해져 있는 숫자로

분기를 할 때에는 switch를 권장한다고 되어 있다.

 

 

반목문의 속도는?

  • for
  • do-while
  • while

일반적으로 for문을 사용하기를 권장한다. 각 구문의 큰 차이는 존재 하지 않음.

 

반복문 작성시 성능 향상 Tip !

- for문 조건 안에 들어가는 메서드는 밖으로 빼자. 매번 메서드가 실행되면서 오버 헤드 현상이 발생하기 때문

- 반복이 필요없는 구간은 반드시 빼내자. 즉, 아무 생각없이 반복문을 쓰지 마라는 말이다.

 

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

 

 

7. static 제대로 한번 써 보자

 

static이란?

지정된 해당 메소드 혹은 변수는 정적으로 된다. 예를 들어 클래스의 변수를 static으로 선언하면 그 변수는

객체의 변수가 되는 것이 아니라 클래스의 변수가 된다. 클래스의 변수이기 때문에 어떤 객체라도 동일한 주소의 변수를 참조하게 된다.

 

static 잘 활용하기

  1. 자주 사용하고 절대 변하지 않는 변수는 final stiatic으로 선언하자.

    만약 자주 변경되지 않고 경우의 수가 단순한 쿼리 문장이 있다면 final static이나 static으로 선언하여 사용하자. 자주 사용되는 로그인 관련 쿼리들이나, 간단한 목록조회 쿼리를 final static으로 선언하면 적어도 1바이트 이상의 GC 대상에서 사라지게 된다. 또한 JNDI 이름이나 간단한 코드성 데이터들은 static으로 선언해 놓으면 편리하다.

  2. 설정 파일 정보도 static으로 관리하자. (static으로 설정 데이터를 읽어서 관리)
  3. 코드성 데이터는 DB에서 한번만 읽자.

static은 원리를 알고 잘 사용하면 시스템의 성능을 향상시킬수 있는 마법의 예약어이지만,

static은 메모리에 올라가면 GC 대상이 아니므로 시스템 다운을 시킬 수도 있다.

만약 static 사용하는 것이 걱정된다면, 차라리 쓰지 않는것이 좋다.

그리고 절대 collection 클래스와 static 키워드는 같이 써서는 안된다. (메모리 릭 발생!)

 

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

 

 

8. 클래스 정보 어떻게 알아 낼수 있을까?

 

자바 API에 reflection이라는 패키지가 있다.

이 패키지는 클래스를 써서 JVM에 로딩되어 있는 클래스와 메소드 정보를 읽어 올 수 있다.

 

Class 클래스 : 클래스에 대한 정보를 얻기에 좋은 클래스이다. 현재 클래스의 이름을 알고 싶으면

                        보통 아래와 같이 사용한다.

                        String currentClassName = this.getClass().getName();

                        단. getName()은 패키지 정보까지 리턴해준다.

                        클래스명만 필요한 경우는 getSimpleName()을 사용하면 된다.

 

Method 클래스 : 메소드에 대한 정보를 얻을 수 있다.
                          Class 클래스의 getMethods()를 사용하거나 getDeclaredMethod() 메소드를 사용한다.

 

Field 클래스 : 클래스에 있는 변수들의 정보를 제공하기 위해서 사용한다.
                      Class 클래스의 getField(), getDeclaredField() 메소드를 사용한다.

 

클래스의 메타 데이터 정보는 JVM의 perm 영역에 저장된다.

그래서 많이 사용하게 되면 OutOfMemoryError 가 발생한다.

 

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

 

9. synchronized는 제대로 알고 써야 한다.

 

프로세스와 스레드

하나의 프로세스에는 여러개의 스레드가 생성된다. 프로세스와 스레드는 1:다의 관계이다.

 

Thread 클래스 상속과 Runnable 인터페이스 구현

스레드의 구현은 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법 두 가지가 있다.

기본적으로 Thread 클래스는 Runnable을 구현한 것이기 때문에 어느 것을 사용해도 상관은 없으나, Runnable을 구현하면 원하는 기능을 추가할 수 있다.

 

synchfornized를 이해하자.

  • 하나의 객체를 여러 스레드에서 동시에 사용할 경우
  • static으로 선언한 객체를 여러  스레드에서 동시에 사용할 경우

    위의 경우에 동기화를 수행한다.

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

 

10. I/O에서 발생하는 병목현상

 

IO는 성능에 영향을 가장 많이 미친다.

IO에서 발생하는 시간은 CPU를 사용하는 시간과 대기 시간 중 대기 시간에 속하기 때문이다.

 

* 한번 열은 스트림은 반듯이 닫아 주어야 한다. 스트림을 닫지 않으면 나중에 리소스가 부족해 질 수 있다.

 

 

 버퍼 없이 FileReader

 버퍼 포함한 FileReader

 BufferedReader 사용

 응답 속도

 2,480ms

 400ms

 350ms

 

자바에서 데이터를 읽는다면 어떤 프로세스로 진행이 될까?

  1. 여러분이 파일을 읽으라는 메소드를 자바에 전달한다.
  2. 파일명을 전달받은 메소드가 운영체제의 커널에게 파일을 읽어달라고 부탁한다.
  3. 커널이 하드디스크로부터 파일을 읽어서 자신의 커널에 있는 버퍼에 복사하는 작업을 수행한다.
  4. 자바에서 마음대로 커널의 버퍼를 사용하지 못하므로, JVM으로 그 데이터를 전달한다.
  5. JVM에서 메소드에 있는 스트림 관리 클래스를 사용하여 데이터를 처리한다.

 

자바에서는 3~4번 작업시 대기하는 시간이 발생할 수 밖에 없다.

이러한 단점을 보완하기 위해서 NIO가 탄생했다. 그로 인해 새로 도입된 개념들은 아래와 같다.

  • 버퍼의 도입
  • 채널의 도입 

  • 문자열의 엔코더와 디코더 제공
  • perl스타일의 정규 표현식에 기초한 패턴 매칭 방법 제공
  • 파일을 잠그거나 메모리 매핑이 가능한 파일 인터페이스 제공
  • 서버를 위한 복합적인 Non-blocking IO 제공

10메가의 파일을 처리할때의 응답속도

 

 BufferedReader

 MapperdByteBuffer

 transferTo

 응답 속도

 422ms

 320ms

 310ms

 

 

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

 

 

11. 로그는 반드시 필요한 내용만 찍자

 

System.out.println의 문제점

  • 파일에 로그를 남기거나 콘솔에 로그를 남길 경우를 생각해보자 앞서 프린트하는 내용이 완전히 프린트되거나 저장될 때까지, 뒤에 프린트 하려는 부분에서는 대기 할 수 밖에 없다.
  • 특히 콘솔에 로그를 남길 경우가 더더욱 그렇다. 그렇게 되면 애플리케이션에서는 대기 시간이 발생한다. 그 대기 시간은 시스템의 속도에 따라서 의존적이다. 만약 디스크에 로그를 남긴다면
  • 서버디스크 RPM이 높을수록 로그 처리의 속도는 빨라질 것이다.

 

System.out.format 메소드 (디버그): C에서 프린트하던 방식으로 처리할 수 있어 소스가 더 간결해 진다.

 

시스템 로그를 더 간결하게 처리하는 방법

      1. 자체 로거 클래스를 만드는 방법

      2. 시스템 로그를 컴파일할 때 삭제 하도록 하는 방법

 

예외 처리 방법

  1. try{
  2. ....
  3. }
  4. catch( Exception e )
  5. {
  6.      e.printStackTrace();
  7. }

위와 같이 처리를 하면 스택 정보를 확인하게 되고, 그 정보를 콘솔에 프린트하며, 다른 로그들과 섞이기

때문에 정보의 가독성도 떨어지며, 최대 100개 까지의 로그를 프린트 하므로 서버 성능에 많은 여향을 미친다.

 

Exception 클래스에서 스택 정보를 배열로 리턴해주는 메소드가 있으며,

그 메소드를 통하여 필요한 정보만을 가공하여 처리하는 방법이 존재한다.

 

  1. try {
  2. ....
  3. }
  4. catch( Exception e )
  5.      StackTraceElement[] ste = e.getStackTrace();
  6.      String className = ste[0].getClassName();
  7.      String methodName = ste[0].getMethodName();
  8.      int lineNumber = ste[0].getLineNumber();
  9.      String fileName = ste[0].getFileName();
  10.      logger.serve("Exception" + e.getMessage());
  11.      logger.serve(className + ", "+ methodName + ", " + fileName + ": " + lineNumber +"라인");
  12. }

       : StackTrace 타입의 배열 ste의 0번째에는 예외가 발생한 클래스 정보가 있으며,

         마지막에는 최초 호출된 클래스의 정보가 있다.

         일반적으로 WAS에서는 WAS 관련 클래스 정보가 포함될 것이므로, 필터링 되도록 하면

         더 깔끔하게 에러로그를 정리할 수 있을 것이다.

     

     

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

     

     

    12. JSP와 서블릿에서 발생할 수 있는 여러 문제점

     

     

    JSP의 라이프 사이클

    1. JSP URL 호출
    2. 페이지 번역
    3. JSP 페이지 컴파일
    4. 클래스 로드
    5. 인스턴스 생성
    6. jspinit 메소드 호출
    7. _jspService 메소드 호출
    8. _jspDestory 메소드 호출

     

    여기서 해당 JSP 페이지가 이미 컴파일되어 있고, 클래스가 로드되어 있고, JSP파일이 변경되지 않았다면,

    가장 시간이 많이 소요되는 ②~④ 프로세스는 생략 된다.

     

     

    JSP include 방식

     

    1. 정적인 방식 : JSP 라이프 사이클 중 JSP 페이지 번역 및 컴파일 단계에서 필요한 JSP를 읽어서

                            메인 JSP의 자바 소스 및 클래스에 포함을 시키는 방식이다.
                           <%@include file="URL" %>

     

    2. 동적인 방식 : 페이지가  호출될 때마다 지정된 페이지를 불러들여서 수행하도록 되어 있다.
                            <jsp:include page="relativeURL"/>

     

    위의 2가지 방식 중 정적인 방식이 동적인 방식보다 빠르다. 하지만 모든 화면을 정적인 방식을 사용하면,

    잘 수행되던 화면에서 오류가 발생 할 수 있다. 정적인 방식은 메인 JSP에 추가되는 JSP가 포함된다.

    이 때 추가 되는 JSP와 메인 JSP에 동일한 이름의 변수가 있으면 심각한 오류가 발생할 수 있다.

     

     

    태그 라이브러리 : JSP에서 반복되는 공통 부분의 코드를 클래스로 만들고,

                               그 클래스를 HTML 태그와 같이 정의된 태그로 사용할 수 있도록 하는 라이브러리다.

                               목록을 처리하면서 대용량의 데이터를 처리할 경우에는 태그 라이브러리의 사용을

                               자제해야 한다. 왜냐하면 태그 라이브러리는 태그 사이에 있는 데이터를 넘겨주어야 하기

                               때문에 대부분 넘겨주는 데이터 형태는 문자열 타입이다.

                               즉, 데이터가 많으면 많을수록 처리를 해야하는 내용이 많아진다.

     

     

    자바 빈즈 : UI에서 서버측의 데이터를 담아서 처리하기 위한 컴포넌트이다. 너무 많이 사용하면 JSP에서

                     소요되는 시간이 증가될 수 있다. 따라서 TO(Transfer Object) 패턴을 사용해야 한다. 

                     하나의 TO 클래스를 만들고, 각각의 데이터를 HashMap, List를 그 클래스의 변수로 지정하여 

                     사용하면 이 화면을 수행하는데 소요되는 시간이 절약될 것이다.

     

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




    Blog2Book 자바 성능을 결정짓는 코딩 습관과 .zip
    1.95MB