본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[자바성능] static의 올바른 사용

static에는 static 초기화 블럭이라는 것이 있습니다. static 초기화 블록은 위와 같이 클래스 어느 곳에나 지정할 수 있습니다. 이 static 블록은 클래스가 최초 로딩될 때 수행되므로 생성자 실행과 상관없이 수행됩니다. 위의 코드처럼 사용했을 때, staticVal의 값은 마지막에 지정한 값이 됩니다. static 블록은 순차적으로 읽혀집니다.


package com.perf.statics;
public class StaticBasicSample2 {
     static String staticVal;
     static {
          staticVal = "Static Value";
          staticVal = StaticBasicSample.staticInt + "";
     }
     public static void main(String[] args) {
          System.out.println(StaticBasicSample2.staticVal);
     }
     static {
          staticVal = "Performance is important!!!"
     }
}


static의 특징은 다른 JVM에서는 static이라고 선언해도 다른 주소나 다른 값을 참조하지만, 같은 JVM이나 같은 WAS 인스턴스에서는 같은 주소와 같은 값을 참조한다는 점입니다. 그리고 GC의 대상도 되지 않습니다. 그러므로 static을 잘만 사용하면 성능을 뛰어나게 향상시킬 수 있지만, 잘못 사용하면 예기치 못한 결과를 초래할 수도 있습니다.



Static 잘 활용하기


먼저 static을 잘 활용하기 위해서 간단하게 사용하는 방법은 아래와 같습니다. 


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

만약 자주 변경되지 않고 경우의 수가 단순한 쿼리 문장이 있다면 final static이나 static으로 선언하여 사용합니다. final static으로 선언하면 적어도 1바이트 이상의 GC 대상 객체가 사라지게 됩니다. 또한 JNDI 이름이나 간단한 코드성 데이터들을 static으로 선언해 놓으면 편리합니다.


간단한 데이터들도 static으로 선언할 수 있지만, 템플릿 성격의 객체를 static으로 선언하는 것도 성능 향상에 많은 도움이 됩니다. Velocity를 사용할 때가 좋은 예입니다. Velocity 기반의 성능을 테스트해보면 템플릿을 읽어오는 부분에서 시간이 가장 많이 소요됩니다. 템플릿 파일을 읽어서 파싱(parsing)하기 때문에 서버의 CPU에 부하가 많이 발생하고 대기 시간도 많아집니다. 그러므로 이 부분은 수행되는 메소드에서 아래와 같은 코드로 작성할 수도 있습니다.


static Template template;

static {
     if(template==null) {
          try {
               template=Velocity.getTemplate("TemplateFileName");               
          } catch (Exception e) {
               //exception 처리
          }
     }
}


이와 같이 처리하면 화면을 요청할 때마다 템플릿 객체를 파싱하여 읽을 필요가 없습니다. 클래스가 로딩될 때 한번만 파싱을 수행하므로 성능이 엄청나게 향상됩니다. 실제로 적용했을 때 부하 상황에서 평균 3초가 소요되던 화면이 0.5초로 단축되기도 합니다.



코드성 데이터는 DB에서 한번만 읽자

데이터의 양이 많고 자주 바뀔 확률이 높은 큰 회사의 부서코드나 큰 쇼핑몰의 상품 코드와 같은 것을 제외하고, 부서가 적은 회사의 코드나, 건수가 그리 많지 않되 조회 빈도가 높은 코드성 데이터는 DB에서 한번만 읽어서 관리하는 것이 성능 측면에서 좋습니다.


package com.perf.statics;

import java.util.*;
public class CodeManager {
     private HashMap<String, String> codeMap;
     private static CodeDAO oDAO;
     private static CodeManager cm;

     static {
          cDAO = new CodeDAO();
          cm = new CodeManager();
          if(!cm.getCodes()) {
               // 에러처리
          }
     }

     private CodeManager() {
     }
     public static CodeManager getInstance() {
          return cm;
     }
     private boolean getCodes() {
          try {
               codeMap=cDAO.getCodes();
               return true;
          } catch (Exception e) {
               return false;
          }
     }
     public boolean updateCodes() {
          return cm.getCodes();
     }
     public String getCodeValue(String code) {
          reuturn codeMap.get(code);
     }
}


클래스가 메모리에 로드되면 static 초기화 블록에서 cDAO 객체 및 cm 객체를 초기화하고, getCodes 메소드를 호출합니다. 모든 코드 정보는 codeMap에 저장됩니다. 코드가 수정되었을 때에는 updateCodes() 메소드를 호출하여 코드 정보를 다시 읽어 오도록 해야 합니다. 만약 서버 인스턴스가 하나만 있다면 코드가 변경되는 것을 걱정할 필요가 없습니다. 수정되자마자 updateCodes() 메소드를 호출해버리면 끝이기 때문입니다. 하지만 서로 다른 JVM에 올라가 있는 코드 정보는 수정된 코드와 상이하므로 그부분에 대한 대책을 마련해 두어야 합니다. 만약 코드가 절대 변경되지 않고, 혹시 코드가 변경되면 서버를 재시작한다면 이 부분에 대해 걱정할 필요는 전혀 없습니다.



Static을 잘못 사용하는 경우


package com.perf.statics.bad;

import java.util.HashMap;
public class BadQueryManager {
     private static String queryURL = null;
     public BadQueryManager(String badUrl) {
          queryURL=badUrl;
     }
     public static String getSql(String idSql) {
          try {
               FileReader reader = new FileReader();
               HashMap document = reader.read(queryURL);
               return document.get(idSql);
          } catch(Exception ex) {
               System.out.println(ex);
          }
          return null;
     }
}


queryURL이라는 문자열을 static으로 지정해 두었습니다. 이 문자열에는 쿼리가 포함된 파일의 이름과 위치가 지정되어 있습니다. 문자열이 있는 생성자로 이 클래스 객체를 생성하면 쿼리 파일이 지정됩니다. getSql(String idSql) 메소드에서는 DAO에서 쿼리를 요청하면, 해당 쿼리 파일을 읽어서 리턴해 줍니다. 이 메소드 또한 static으로 지정되어 있습니다.


일단 위 쿼리는 정상적으로 수행됩니다. 물론 파일을 읽어야 하므로 화면의 응답속도는 느릴 것입니다.


하지만 어떤 화면에서 BadQueryManager의 생성자를 통해서 queryURL을 설정하고, getSql() 메소드를 호출하기 전에, 다른 queryURL을 사용하는 화면에서 BadQueryManager의 생성자를 호출하면, 오류가 발생할 것입니다.


getSql() 메소드와 queryURL을 static으로 선언한 것이 잘못입니다. 직접 접근할 수 있도록 static으로 선언한 것인데, 그로인해 문제가 발생한 것입니다. 웹 환경이기때문에 여러 화면에서 호출할 경우에 queryURL은 그때 그때 바뀌게 됩니다. 다시 말하면, queryURL은 static으로 선언했기 때문에 클래스의 변수이지 객체의 변수가 아닙니다. 모든 스레드에서 동일한 주소를 바라보게 되어 문제가 발생한 것입니다.


어떤 언어로 프로그래밍을 하든 파일 IO가 발생하면 느려집니다. 이 소스는 쿼리를 한 번 호출하기 위해서 매번 파일을 읽을 수 밖에 없는 구조이기 때문에 IO가 발생하면서 대기하는 IO wait가 발생하는 것을 피할 수가 없습니다.



Static과 메모리 릭

static으로 선언한 부분은 GC가 되지 않습니다. 그럼 만약 어떤 클래스에 데이터를 Vector나 ArrayList에 담을때 해단 Collection 객체를 static으로 선언하면 어떻게 될까요? 만약 지속적으로 해당 객체에 데이터가 쌓인다면, 더이상 GC가 되지 않으면서 시스템은 OutOfMemoryError를 발생시킬 것입니다. 즉, 시스템을 재시작해야 하며, 해당 인스턴스는 더 이상 서비스할 수 없습니다.


static Vector v = new Vector();
static StringBuilder dummyStr;
static {
     dummyStr = new StringBuilder("1234567890");
     for(int loop=0; loop<22; loop++) {
          dummyStr.append(dummyStr);
     }
}
v.add(dummyStr.toString());


만약 WAS 메모리가 512MB로 지정되어 있다면, 이 화면은 5~6회 호출되면 OutOfMemoryError가 발생하여 더 이상 서비스가 불가능한 상태가 됩니다.

더이상 사용 가능한 메모리가 없어지는 현상을 메모리 릭(Memory Leak)이라고 하는데, static과 Collection 객체를 잘못 사용하면 메모리 릭이 발생하게됩니다. 메모리 릭이 발생했을 때 가장 크게 나타나는 현상은 사용가능한 메모리가 적어지는 것입니다.


아무리 GC가 수행되더라도 메모리가 어느 정도 이하로는 떨어지지 않는 현상이 발생하게 됩니다. 이 시스템은 며칠 후에는 더이상 GC도 가능하지 않게 되어 WAS를 재기동해야 할 것입니다. 실제 이러한 문제가 발생하는 대부분의 사이트는 하루에 한 번 혹은 3일에 한번씩 시스템을 재기동하면서 운영하고 있습니다.


자바에서 성능을 튜닝하는 것은 툴만 있으면 쉽습니다. 하지만 툴이 있어도 문제점을 찾기 어려운 것이 메모리 릭입니다. 어느 부분에서 메모리 누수가 발생하는지 알 수 없기 때문에 전체 시스템을 수행하여야 하고, 또 수행하더라도 문제가 되는 부분을 쉽게 밝혀낼 수 없기 때문입니다.


메모리 릭을 발생시키는 대상이 여러번 수행되어야만 툴에서도 문제점을 잡을 수 있게 됩니다. 다시 말해 메모리 릭은 시간의 여유를 두고 툴을 사용하면 예상보다 쉽게 잡을 수도 있습니다. 그러나 문제는 사전에 예방하는 최선이고, 잘 알고 사용하는 경우가 아니라면 static과 Collection 관련 객체는 같이 사용하지 않는편이 좋습니다.



마무리

static은 반드시 메모리에 올라가며 GC의 대상이 되지 않습니다. 객체를 다시 생성한다고 해도 그 값은 초기화되지 않고 해당 클래스를 사용하는 모든 객체에서 공유하게 됩니다. 만약 static을 사용하는 것이 걱정된다면, 아예 쓰지 않는 것이 좋습니다. 모르고 시스템이 잘못되는 것보다 아예 안쓰는 것이 더 안전합니다.