6. static 제대로 한 번 써 보자

de_sj_awa·2021년 8월 29일
0

6. static 제대로 한 번 써 보자

자바 프로그래밍에서 성능을 향상시키는 방법은 여러 가지가 있다. 그 중에서 한가지는 static을 사용하는 것이다. 하지만 잘 모르고 static을 사용하다가는 시스템이 더 느려지거나, 오류를 내뿜는 시스템이 되거나 JVM이 죽어버리는 상황이 발생할 수도 있다.

1. static의 특징

static이라는 단어는 '정적인, 움직이지 않는'이라는 의미이다(반대말은 dynamic이다). 자바에서 static으로 지정했다면, 해당 메서드나 변수는 정적이다.

public class VariableTypes {
    int instance Variable;
    static int classVariable;
    public void method(int parameter) {
        int localVariable;
    }
}

여기서 static으로 선언한 classVariable은 클래스 변수라고 한다. 왜냐하면, 그 변수는 '객체의 변수'가 되는 것이 아니라 '클래스의 변수'가 되기 때문이다. 100개의 VariableTypes 클래스의 인스턴스를 생성하더라도, 모든 객체가 classVariables에 대해서는 동일한 주소의 값을 참조한다.

package com.perf.statics;

public class StaticBasicSample {
  public static int  staticInt = 0;
  public static void main(String[] args) {
    StaticBasicSample sbs1 = new StaticBasicSample();
    sbs1.staticInt++;
    StaticBasicSample sbs2 = new StaticBasicSample();
    sbs2.staticInt++;
    System.out.println(sbs1.staticInt);
    System.out.println(sbs2.staticInt);
    System.out.println(StaticBasicSample.staticInt);
  }
}

이 예제처럼 static 변수는 객체를 생성해서 참조할 필요는 없다. 가장 밑의 라인처럼 밑의 클래스를 참조하면 된다. 그럼 이 결과는 어떻게 될까?

2
2
2

객체를 참조해서 값을 더하든, 클래스를 직접 참조해서 값을 더하든 동일한 값을 참조하므로 같은 결과가 나온다. 즉, sbs1이나 sbs2 모두 동일하게 StaticBasicSample.staticInt라는 변수를 참조한다.

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 초기화 블록은 위와 같이 클래스 어느 곳에나 지정할 수 있다. 이 static 블록은 클래스가 최초 로딩될 때 수행되므로 생성자 실행과 상관없이 수행된다. 또한 위의 예제와 같이 여러 번 사용할 수 있으며, 이와 같이 사용했을 때 staticVal은 마지막에 지정한 값이 된다. static 블록은 순차적으로 읽혀진다는 의미이다. 따라서 결과는 다음과 같다.

Performance is important !!!

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

특히 웹 환경에서 static을 잘못 사용하다가는 여러 쓰레드에서 하나의 변수에 접근할 수 있기 때문에 데이터가 꼬이는 큰 일이 발생할 수도 있다.

2. static 잘 활용하기

먼저 static을 잘 활용하기 위해서 간단하게 사용하는 방법부터 알아보자.

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

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

간단한 데이터들도 static으로 선언할 수 있지만, 템플릿 성격의 객체를 static으로 선언하는 것도 성능 향상에 많은 도움이 된다. Velocity를 사용할 때가 좋은 예이다.

Velocity란 자바 기반의 프로젝트를 수행할 때, UI가 될 수 잇는 HTML 뿐만 아니라 XML, 텍스트 등의 템플릿을 정해 놓고, 실행 시 매개변수 값을 던져서 원하는 형식의 화면을 동적으로 구성할 수 있도록 도와주는 컴포넌트이다.

Velocity 기반의 성능을 테스트해 보면 템플릿을 읽어 오는 부분에서 시간이 가장 많이 소요된다.

try {
    Template template = Velocity.getTemplate("TemplateFileName");

템플릿 파일을 읽어서 파싱(parsing)하기 때문에 서버의 CPU에 부하가 많이 발생하고 대기 시간도 많아진다. 그러므로 수행하는 메서드에서 이 부분을 분리하여 다음과 같이 수정해야 한다.

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

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

그런데, 만약 해당 template 내용이 지속적으로 변경되는 부분이라면 이와 같이 코들를 자성할 경우 A 화면이 보여야 하는 사용자에게 B 화면이 보일 수 있다. 그러니 상황에 맞게 적용하는 것이 중요하다.

설정 파일 정보도 static으로 관리하자

요즘 자바 기반으로 개발할 때 보면 매우 많은 설정 파일들이 존재하는데, 클래스의 객체를 생성할 때마다 설정 파일을 로딩하면 엄청난 성능 저하가 발생하게 된다. 이럴 때는 반드시 static으로 데이터를 읽어서 관리해야 한다.

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

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

package com.perf.statics;

import java.util.HashMap;

public class CodeManager {
    private HashMap<String,String> codeMap;
    private static CodeDAO cDAO;
    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) {
      return codeMap.get(code);
    }
}

소스에 대해서 간단히 설명을 하면, CodeManager 클래스는 코드 정보를 미리 담아 놓는 클래스이다. 내부적으로 cm 객체를 static으로 선언하여, 생성자가 아닌 getInstance() 메서드를 통해서 CodeManager 클래스에 접근하도록 했다.

codeMap이라는 HashMap 객체에 필요한 코드 정보들을 담을 예정이다. CodeDAO 객체는 DB에서 코드 정보를 갖고 오도록 되어 있는 DAO 클래스이다.

클래스가 메모리에 로드되면 static 초기화 블록에서 cDAO 객체 및 cm 객체를 초기화하고, getCodes() 메서드를 호출한다. 모든 코드 정보는 codeMap에 저장된다. 이제부터 코드 정보를 가져올 때는 getCodeValue() 메서드를 호출하여 메모리에서 코드 정보를 읽어온다.

만약 코드가 수정되었을 때에는 updateCodes() 메서드를 호출하여 코드 정보를 다시 읽어 오도록 해야 한다. 그런데 이와 같은 클래스를 만들어서 코드를 가져오는 일은 가장 큰 문제가 되기도 한다. 만약 서버 인스턴스가 하나만 있다면 코드가 변경되는 것을 걱정할 필요가 없다. 수정되자마자 updateCodes() 메서드를 호출하면 끝이기 때문이다. 하지만 서로 다른 JVM에 올라가 있는 코드 정보는 수정된 코드와 상이하므로 그 부분에 대한 대책을 마련해야 한다. 만약 코드가 절대 변경되지 않고, 혹시 코드가 변경될 경우 서버를 재시작하면 이 부분에 대해 걱정할 필요가 없다.

이러한 JVM 간에 상이한 결과가 나오는 것을 방지하기 위해서 요즘에는 memcached, EhCached 등의 캐시(Cache)를 많이 사용한다.

3. static 잘못 쓰면 이렇게 된다

package com.perf.statics.bad;

import java.io.FileReader;
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<String,String> document = reader.read(queryURL);
        return document.get(idSql);
      } catch (Exception ex) {
        System.out.println(ex);
      }
      return null;
    }
}

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

먼저 이 소스를 수행하면, 제대로 된 결과가 나올지 생각해 보자. 처음엔 수행이 된다. 쿼리가 같은 파일에 있는 화면은 수행이 되고, 만약 어떤 화면의 수행 결과가 다른 파일의 쿼리인 경우에도 처음에 그 화면이 호출되었다면, 정상적으로 수행될 것이다. 물론 파일을 읽어야 하므로 화면의 응답 속도는 느릴 것이다.

그런데 만약 어떤 화면에서 BadQueryManger의 생성자를 통해서 queryURL을 설정하고 getSql() 메서드를 호출하기 전에, 다른 queryURL을 사용하면 화면의 스레드에서 BadQueryManager의 생성자를 호출하면 어떤 일이 발생할까?

그때부터는 시스템이 오류를 발생시킨다. 먼저 호출한 화면에서는 생성자를 호출했을 때의 URL을 유지하고 있을 것이라 생각하고 getSql() 메서드를 호출하겠지만, 이미 그 값을 변경되고 난 후다.

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

어떤 언어로 프로그래밍을 하든 파일 IO가 발생하면 느려진다. 이 소스는 쿼리를 한 번 호출하기 위해서 매번 파일을 읽을 수 밖에 없는 구조이기 때문에 IO가 발생하면서 대기하는 IO wait가 발생하는 것을 피할 수 없다. static을 잘못 사용한 또 다른 예를 보자.

private static boolean successFlag;

이 소스는 서블릿에서 응시자의 합격 여부를 잠깐 담아 놓기 위해 successFlag로 지정해 놓은 부분이다. 어떤 일이 발생했을까? 물론 이 소스는 개발자가 본인의 PC에서 테스트할 때는 전혀 문제가 되지 않는다. 그리고 성능 테스트를 하거나 통합 테스트를 하더라도 문제점을 쉽게 발견할 수 없다.

그러나 만약 수십 명이 동시에 자신이 정보를 확인하기 위해서 위의 서블릿을 호출하는 경우를 생각해 보자. successFlag를 true로 처리해 놓은 상황에서 다른 사용자의 요청이 처리되어 false로 바뀐다면, 그 사람은 완전히 다른 결과를 받게 된다.

4. static과 메모리 릭

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

<%@ page import="java.util.*" %>
<%!
    static ArrayList list = new ArrayList();
    static StringBuilder dummyStr;
    static {
      dummyStr = new StringBuilder("1234567890");
      for(int loop=0; loop<22; loop++) {
        dummyStr.append(dummyStr);
      }
    }
%>
<%
    list.add(dummyStr.toString());
%>
<%=list.size() + " " + dummyStr.length()%>

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

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

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

이러한 메모리릭의 원인은 메모리의 현재 상태를 (메모리의 단면을) 파일로 남기는 HeapDump라는 파일을 통해서 확인 가능하다. JDK/bin 디렉터리에 있는 jmap이라는 파일을 사용하여 덤프를 남길 수 있으며, 남긴 덤프는 eclipse 프로젝트에서 제공하는 MAT와 같은 툴을 통해서 분석하면 된다.

참고

  • 자바 성능 튜닝 이야기
profile
이것저것 관심많은 개발자.

0개의 댓글