본문 바로가기

JAVA

[책요약] 자바 성능을 결정짓는 코딩습관과 튜닝이야기 - 이상민 저

전 개발자 신입일 때부터 에버노트를 이용해서 프로그래밍 관련된 내용을 정리하고 있습니다. 

티스토리를 시작하면서 포스팅만할 내용을 찾고자 예전에 작성했던 문서들을 보고 있는데 책요약한게 있네요. 

 

이 책을 읽고 난 후 벌써 5년이라는 시간이 지났네요. 개발 2년차 때 읽고 요약했던건데 지금 다시 읽어보니 유용한 내용이 있어서 같이 공유하고자 포스팅합니다. 

 

책 제목 : 자바 성능을 결정짓는 코딩습관과 튜닝이야기

책 저자 : 이상민

 

 

 

디자인 패턴

Service Locator : 서비스와 컴포넌트 검색을 쉽게 하는 패턴

==> 빈번히 사용되는 객체를 찾을 때 소요되는 응답속도를 감소시키기 위해 Map객체에

찾은 객체를 보관하고 있다가 그 객체를 필요로 할 때 메모리에서 찾아서 제공한다.

public class ServiceLocator{

   private InitialContext ic;

  private Map cache;

  private static ServiceLocator me;

   static{

       me = new ServiceLocator();

    }



   private ServiceLocator(){

       cache = Collections.synchronizedMap(new HashMap());

  }

public InitialContext getInitialContext() {

  if(ic == null){

      ic = new InitialContext();

   }

   return ic;

  }



public static ServiceLocator getInstance(){

        return me;

}



publice EJBLocalHome getLocalHome(String jndiHomeName){

   EJBLocalHome home  = null;

   if(cache.containsKey(jndiHomeName)){

       home = (EJBLocalHome)cache.get(jndiHomeName);

    }else{

       home = (EJBLocalHome)getInitialContext().lookup(jndiHomeName);

       cache.put(jndiHomeName, home);

  }

}

 

Transfer Object

 ==> VO(Value Object)라고도 불림. 데이터 전송시 사용하는 패턴, getter, setter 있는..

 ===> Serializable을 구현(implements)하면 객체를 직렬화 할 수 있다. 이로인해 서버 사이의 데이터 전송(원격지 서버에 데이터를 전송하거나 파일로 객체를 저장하는 등)이 가능해진다.

 

 

GC(Garbage Collector)의 역할

- 메모리 할당

- 사용 중인 메모리 인식

- 사용하지 않는 메모리 인식

 

 

 

조건문

If-else 문장 자체에서는 많은 시간이 소요되지 않음

switch : byte, short, int, char 네가지 타입을 사용한 조건분기 가능

sun에서는 숫자 비교시 if보다 가독성이 좋아지므로 정해져 있는 숫자로 분기할 때에는 switch 권장

 

 

반복구문

do-while, while은 무한루프에 돌 수 있으므로 되도록이면 대신 for문을 사용

for문 사용시 for(int i = 0; i < list.size(); i++) 와 같이 코딩할시 list.size를 계속 호출하므로 list.size를 위로 빼서 작성

JDK 5.0부터 추가된 향상된 for( .. in...) 구문은 다른 for문보다 느리지만 10만번 이상 반복할 경우가 아니라면 웹어플에서는 큰 차이가 없다.

반복구문에서의 필요없는 반복이 있는지 확인해봐야 한다.

for(int i = 0; i < treeSet.size(); i++){

   DataVO vo = (DataVO)treeSet.toArray()[i];

}

==> treeSet.size(); 와 toArray()가 불필요하게 반복되고 있음.

 

 

Static

static 클래스 메서드나 변수가 됨(정적) : 어떤 객체라도 동일한 주소의 변수를 참조하게 됨

다른 JVM에서는 static이라고 선언해도 다른 주소나 다른 값을 참조하지만

같은 JVM이나 같은 WAS인스턴스에서는 같은 주소와 같은 값을 참조한다. 

그리고 GC의 대상도 되지 않는다.

1) 자주 사용하고 절대 변하지 않는 변수(메서드)는 final static으로 선언

2) 설정 파일 정보도 static으로 관리

3) 건수가 많지 않고 자주 변경되지 않으면서 조회빈도가 높은 코드성 데이터는 DB에서 한번만 읽어서 관리

==> 단, 서버 인스턴스가 하나만 있다면 코드가 변경되는 것을 걱장할 필요가 없다. 수정되자마자 updateCodes()와 같이 코드를 변경하면 되지만, 서로 다른 JVM에 올라가 있는 코드 정보는 수정된 코드와 상이하므로 대책 마련해야함(코드가 변경되면 서버를 재시작하거나, 절대 변경되지 않는 코드에만 적용을 하는 등)

 

static을 선언한 부분은 GC가 되지 않는다. 어떤 클래스에 데이터를 Vector나 ArrayList에 담을 때 해당 객체에 데이터가 쌓이면 결국 OutOfMemoryError가 발생한다. 

더이상 사용 가능한 메모리가 없어지는 현상 : Memory Leak 

성능 테스트 툴로도 문제점을 찾기 어렵다. 메모리 릭을 발생키시는 대상이 여러번 수행되어야만 툴에서 문제점을 잡을 수 있다. ==> 되도록이면 static과 collection 관련 객체 같이 사용하지 말자.

 

또한 static으로 선언한 변수를 동시에 접근해서 값을 변경/사용하는 경우가 있을 경우는 절대 static을 사용해선 안된다. 

 

 

클래스 정보를 알아오는 방법.

reflection 패키지 사용: JVM에 로딩되어 있는 클래스와 메서드 정보를 읽어올 수 있다. 

Class 클래스  clazz.getClass().getName() 등..

단, 클래스를 비교하고자 할 경우 reflection이 아닌 instanceof 를 사용하자.

 

 

쓰레드, synchronized

클래스를 하나 수행하거나 WAS를 기동하면, 서버에 자바 프로세스 하나 생성(서버 마다 다름 여러개 생성할 수 있음)

하나의 프로세스에서는 여러 개의 쓰레드 생성되고 수행된다.

웹 기반 시스템에서 스레드 관련 가장 많이 사용하는 것이 synchronized

public synchronized void sampleMethod(){} //메서드를 동기화

public void sampleBlock(){

    synchronized(obj){ //특정부분만을 동기화

    ...}

}

 

동기화 사용 시기

1) 하나의 객체를 여러 쓰레드에서 동시에 사용할 경우

2) static으로 선언한 객체를 여러 쓰레드에서 동시에 사용할 경우(static을 사용한 변수를 공유할 경우)

 

synchronized는 각각의 객체에 대한 동기화를 하는 것이지 클래스에 대한 동기화를 하는 것은 아니다. 

메서드를 통해 static 변수를 수정할 경우, 해당 메서드도 static으로 선언해야 한다. 

JDK5.0부터 추가된 java.util.concurrent 패키지의 주요 개념

1) Lock : 실행 중인 스레드를 간단한 방법으로 정지시켰다가 실행시키도록 한다. 

상호 참조로 인해 발생하는 데드락을 피할 수 있다. 

2) Executors : 스레드를 더 효율적으로 관리할 수 있는 클래스들을 제공한다. 

스레드풀도 제공하므로 필요에 따라 유용하게 사용

3) Concurrent 콜렉션 : BlockingQueue, ConcurrentMap 등 동기화 관련 컬렉션 객체

4) Atomic 변수 : 동기화가 되어 있는 변수를 제공. 이 변수를 사용하면 synchronized 식별자를 메소드에 지정할 필요없이 사용가능

http://java.sun.com/docs/books/tutorial/essential/concurrency/highlevel.html

 

 

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

자바를 이용하여 하드 디스크에 있는 데이터를 읽을 경우 프로세스

1) 파일을 읽으라는 메서드를 자바에 전달

2) 파일명을 전달받은 메서다그 운영체제의 커널에게 파일을 읽어달라고 부탁

3) 커널이 하드 디스크로부터 파일을 읽어서 자신의 커널에 있는 버퍼에 복사하는 작업 수행(DMA라는 것이 이 작업 수행)

4) 자바에서는 마음대로 커널의 버퍼를 사용하지 못하므로 JVM으로 그 데이터를 전달

5) JVM에서 메소드에 있는 스트림 관리 클래스를 사용하여 데이터 처리

 

자바에서는 3번, 4번 작업 수행시 대기하는 시간이 발생한다. 

이런 단점을 보완하기 위해 NIO(New IO) 탄생(JDK 1.4부터 추가)

==>3번 작업을 자바에서 직접 통제하여 시간 더 단축

 

NIO에서 새롭게 도입된 개념

1) 버퍼의 도입

2) 채널의 도입

3) 문자열의 엔코더와 디코더 제공

4)Perl 스타일의 정규 표현식에 기초한 패턴 매칭 방법 제공

5) 파일을 잠그거나 메모리 매핑이 가능한 파일 인터페이스 제공

6) 서버를 위한 복합적인 Non-blocking IO 제공

http://java.sun.com/developer/technicalArticles/releases/nio/

예) 파일복사 프로그램

//기존 IO
BufferedReader br = new BufferedReader(new FileReader(from));
BufferedWriter bw = new BufferedWriter(new FileWriter(to));
String data;
while((data = br.readLine())!=null){
   bw.write(data);
   bw.newLine();
}


//New IO
FileInputStream fis = new FileInputStream(from);
FileOutputStream fos = new FileOutputStream(to);
FileChannel in = fis.getChannel();
FileChannel out = fos.getChannel();
MappedByteBuffer m = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
out.write(m);


//다른 New IO방법
MappedByteBuffer m = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
out.write(m);
//대신 아래처럼 한줄로 처리 가능(운영체제의 메모리르 사용하여 복사)
in.transferTo(0, in.size(), out);

 

 

 

로그를 사용할 경우 성능에 영향을 미치는 이유

파일에 로그를 남기거나, 콘솔에 로그를 남길 경우

앞서 프린트하는 내용이 완전히 프린트되거나 저장될 때까지, 뒤에 프린트하려는 부분에서는 대기할 수 밖에 없음. 결국 애플리케이션에서는 대기 시간이 발생

의미없는 디버그용 로그를 프린트하기 위해 서버의 리소스와 디스크 낭비

 

로거(자바 Logger클래스) 사용시의 문제점

로그를 처리하기 위한 한 줄을 처리하기 위해서는 어차피 객체를 생성해야 한다. 

즉, 하나 이상의 객체가 필요하고, 그 객체를 생성하는데 메모리와 시간이 소요되고,

메모리에서 제거를 하기 위해서는 GC를 수행해야 하고, GC를 수행하면서 시간이 또 소요됨.

 

따라서 아래와 같이 디버그용 로그를 if조건문을 통해 제어를 하는 것이다. 

if(logger.isLoggable(Level.INFO){

  //로그처리

}

 

예외처리할 경우도 e.printStackTrace(); 를 그대로 사용하면 시스템 성능에 영향을 미친다. 

따라서 아래와 같이 필요한 데이터만 가져와서 로거를 이용해서 출력하자.

try{

   ...

}catch(Exception e){

  StackTraceElement[] ste = e.getStackTrace();
  String className = ste[0].getClassName();
  String methodName = ste[0].getMethodName();
  String lineNumber = ste[0].getLineNumber();
  String fileName = ste[0].getFileName();
  logger.severe("Exeception : " + e.getMessage());
  logger.severe(className + methodName + lineNumber + fileName);
}

Exception클래스에서 스택 정보를 배열로 리턴해주는 getStackTrace()메서드를 이용

배열의 0번째에는 예외가 발생한 클래스 정보가 있으며, 마지막에는 최초 호출된 클래스의 정보가 있다. 

 

Logger 사용시 로그가 찍힌 클래스의 이름을 알아내는 방법은..

위와같이 예외를 일부러 발생시켜서 그 예외의 StackTrace 저보에 있는 클래스와 메서드 정보를 알아내는 것이다. 

StackTraceElement stack[] = (new Throwable()).getStackTrace();

 

 

JSP의 라이프사이클

JSP가 처음에 호출되는 경우에만 시간이 소요되고, 그 다음부터는 컴파일된 서블릿 클래스가 수행된다.

  1. JSP URL호출
  2. 페이지 번역
  3. JSP페이지 컴파일
  4. 클래스 로드
  5. 인스턴스 생성
  6. jspInit 메서드 호출
  7. jspService 메서드 호출
  8. jspDestroy 메서드 호출

JSP Include

jsp include 중 정적인 방식이 빠르다.

 

정적인 방식 : <%@include file="URL" %>

JSP라이프사이클 중 JSP 페이지 번역 및 컴파일 단계에서 필요한 JSP를 읽어서 메인 JSP의 자바 소스 및 클래스에 포함을 시키는 방식

 

동적인 방식 : <jsp:include page="relativeURL" />

페이지가 호출될 때마다 지정된 페이지를 불러들여서 수행하도록 되어 있음.

이는 JSP 페이지 컴파일해서 생성되는 자바파일의 형태를 보면 알 수 있다. 

 

정적인 경우는 그냥 include파일 자체를 덧붙이는 형식이지만

oup.println("include 파일 내용들...");

동적인 경우는 실행 자체가 넘어가는 형식이다. 

org.apache.jasper.runtime.JspRuntimLibrary.include(request, response, "include URL", out, true);

 

 

DataSource와 DB connection Pool의 차이

DataSource는 JDK1.4버전부터 생긴 자바표준이다. Connection Pool로 연결을 관리해야 하고, 트랜잭션 관리도 가능하도록 만들어야 한다. 즉, DataSource가 DB Connection Pool을 포함한다고 생각하면된다.

DB connection Pool은 자바 표준으로 지정되어 있는 것이 없어서 WAS벤더에 따라 사용법이 상이할 수 있다. 

 

 

XML데이터 다루기

자바에서는 XML을 파싱하기 위해 JAXP를 제공한다. JAXP는 SAX, DOM, XSLT에서 사용하는 기본 API를 제공한다. 

JAXP :  Java API for XML Processing  (javax.xml.parsers)

SAX : Simple API for XML (org.xml.sax)

DOM : Document Object Model (org.w3c.dom)

XSLT Xml Stylesheet Language for Transformations (javax.xml.transform)

 

SAX는 순차적 방식으로 XML을 처리한다. 

 ==> 각 XML의 노드를 읽는 대로 처리하기 때문에 메모리에 부담이 DOM에 비해 적다. 

 ==> 이미 읽은 데이터의 구조를 수정하거나 삭제하기 어렵다.

DOM은 모든 XML을 읽어서 트리(tree)를 만든 후 XML을 처리한다.

 ==> 모든 XML을 메모리에 올려서 작업하기 때문에 메모리에 부담이 간다. 

 ==> 노드를 추가, 수정, 삭제하기 쉬운 구조로 되어 있다. 

 

단순히 XML파일을 읽고 처리하는 거라면 SAX방식이 DOM방식에 비해 3배 정도 빠르며 사용하는 메모리도 5배 정도 적다.