시스템의 성능이 느릴 때 가장 먼저 해야 하는 작업은 병목 지점을 파악하는 것이다. 자바 기반의 시스템에 대하여 응답 속도나 각종 데이터를 측정하는 프로그램은 많다. 애플리케이션의 속도에 문제가 있을 때 분석하기 위한 툴로는 프로파일링 툴이나 APM 툴이 있다. 이 툴을 사용하면, 고속도로 위에서 헬기나 비행기로 훑어보듯이 병목 지점을 쉽게 파악할 수 있다. 하지만 대부분의 프로젝트나 운영 사이트에서는 예산상의 이유로 분석 툴을 사용하지 않는다. 이러한 현실에서 우리가 할 수 있는 방법에 대해서 알아보자.
구분 | 특징 |
프로파일링 툴 |
- 소스 레벨의 분석을 위한 툴이다.
- 애플리케이션의 세부 응답 시간까지 분석할 수 있다. - 메모리 사용량을 객체나 클래스, 소스의 라인 단위까지 분석할 수 있다. - 가격이 APM 툴에 비해서 저렴하다. - 보통 사용자 수 기반으로 가격이 정해진다. - 자바 기반의 클라이언트 프로그램 분석을 할 수 있다. |
APM 툴 |
- 애플리케이션의 장애 상황에 대한 모니터링 및 문제점 진단이 주 목적이다.
- 서버의 사용자 수나 리소스에 대한 모니터링을 할 수 있다. - 실시간 모니터링을 위한 툴이다. - 가격이 프로파일링 툴에 비하여 비싸다. - 보통 CPU 수를 기반으로 가격이 정해진다. - 자바 기반의 클라이언트 프로그램 분석이 불가능하다. |
그럼 프로파일링 툴이 기본적으로 제공하는 기능은 어떤 것들이 있을까? 각 툴이 제공하는 기능은 다양하고 서로 상이하지만, 응답 시간 프로파일링과 메모리 프로파일링을 기본적으로 제공한다.
CPU 시간과 대기 시간
프로파일링 툴에 대해서 한가지 더 덧붙이면, APM 툴이건 프로파일링 툴이건 자동으로 분석해주는 툴은 없다. 간혹 툴만 사면 모든 것이 자동으로 해결된다는 사람들이 있는데, 잘못된 생각이다. 일단 툴에서 분석을 하려면 해당 메서드가 수행이 되어야 한다. 수행되지 않은 메서드는 분석 자체가 되지 않는다. 또한 문제가 되는 메서드가 수행되어야 하므로, 메모리 부족 현상이 가장 분석하고 찾아내기 어렵다.
더 간단하게 프로그램의 속도를 측정할 수 있는 방법은 없을까? 가장 간단하게 측정할 수 있는 방법은 System 클래스에서 제공하는 메서드를 활용하는 것이다.
모든 System 클래스의 메서드는 static으로 되어 있고, 그 안에서 생성된 in, out, err과 같은 객체들도 static으로 선언되어 있으며, 생성자(Constructor)도 없다. 다시 말하면, System 객체는 우리가 생성을 할 수 없으며, System.XXX와 같은 방식으로 써야 한다. System 클래스에서 자주 사용하지는 않지만 알아두면 매우 유용한 메서드에는 어떤 것들이 있는지 알아보자.
package com.perf.timer;
public class SystemArrayCopy {
public static void main(String[] args) {
String[] arr = new String[] {"AAA", "BBB", "CCC", "DDD", "EEE"};
String[] copiedArr = new String[3];
System.arraycopy(arr, 2, copiedArr, 1, 2);
for(String value : copiedArr) {
System.out.println(value);
}
}
}
위 코드를 실행하면 원본 배열의 2번 위치부터 복사를 하기 때문에 'CCC'부터 복사를 시작한다. copiedArr 배열의 1번 위치부터 복사한 데이터를 저장하고, 복사하는 개수가 두 개 이기 때문에 'DDD'까지 복사를 수행한다. 결과 값은 다음과 같다.
null
CCC
DDD
여기서 만약 length를 2가 아닌 3으로 하거나 그 이상으로 지정하게 되면 copiedArr의 크기가 3개밖에 되기 않기 때문에 ArrayIndexOutOfBoundsException이 발생한다.
자바에서 JVM에서 사용할 수 있는 설정은 크게 두가지로 나뉜다. 하나는 속성(Property)값이고, 다른 하나는 환경(Environment)값이다. 속성은 JVM에서 지정된 값들이고, 환경은 장비(서버)에 지정되어 있는 값들이다. 자바에서는 영어 단어 그대로 "속성"은 Properties로, "환경"은 env로 사용한다. 먼저 Properties를 사용하는 메서드에 대해서 알아보자.
이러한 자바 속성 관련 메서드를 어떻게 사용하는지 다음의 예를 통해 알아보자.
package com.perf.timer;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
public class GetProperties {
public static void main(String[] args) {
System.setProperty("JavaTuning", "Tune Lee");
Properties prop = System.getProperties();
Set key = prop.keySet();
Iterator it = key.iterator();
while(it.hasNext()) {
String curKey = it.next().toString();
System.out.format("%s=%s\n", curKey, prop.getProperty(curKey));
}
}
}
이 소스는 'JavaTuning'이라는 키를 갖는 시스템 속성에 'Tune Lee'라는 값을 지정한 후, 시스템 속성 전체 값을 화면에 출력해주는 프로그램이다. 이 프로그램을 수행하면 수십 개의 자바의 시스템 속성 값을 출력한다. 그 결과 우리가 지정한 'JavaTuning' 키를 갖고 'Tune Lee' 값을 가지는 속성이 추가되어 출력될 것이다.
시스템 환경 변수 관련 메서드를 사용하는 것은 자바 속성 관련 메서드를 사용하는 것과 비슷하다.
package com.perf.timer;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class GetEnv {
public static void main(String[] args) {
Map<String,String> envMap = System.getenv();
Set key = envMap.keySet();
Iterator it = key.iterator();
while(it.hasNext()) {
String curKey = it.next().toString();
System.out.format("%s = %s\n", curKey, envMap.get(curKey));
}
}
}
위의 코드 수행 결과는 윈도의 커맨드 창에서 'set' 명령어를 치는 결과와 동일하다.
네이티브 라이브러리를 활용할 때 사용할 수 있는 System 클래스는 다음과 같다.
그리고, 운영중인 코드에 절대로 사용해서는 안되는 메서드가 있다.
시간 관련 메서드는 다음과 같다.
currentTimeMillis() 메서드에서 리턴해 주는 결과 값은 ms(밀리초)이다. UTC라는 시간 표준 체제를 따른는데, 1970년 1월 1일부터의 시간을 long 타입으로 리턴해 준다. 따라서 호출할 때마다 다르다. 이 시간 값을 변환하면 현재 날짜를 구할 수도 있다.
package com.perf.timer;
import java.util.ArrayList;
import java.util.HashMap;
public class CompareTimer {
public static void main(String[] args) {
CompareTimer timer=new CompareTimer();
for(int loop=0;loop<10;loop++) {
timer.checkNanoTime();
timer.checkCurrentTimeMillis();
}
}
private DummyData dummy;
public void checkNanoTime() {
long startTime=System.nanoTime();
dummy=timeMakeObjects();
long endTime=System.nanoTime();
double elapsedTime=(endTime-startTime)/1000000.0;
System.out.println("nano="+elapsedTime);
}
public void checkCurrentTimeMillis() {
long startTime=System.currentTimeMillis();
dummy=timeMakeObjects();
long endTime=System.currentTimeMillis();
long elapsedTime=endTime-startTime;
System.out.println("milli="+elapsedTime);
}
public DummyData timeMakeObjects() {
HashMap<String,String> map=new HashMap<String,String>(1000000);
ArrayList<String> list=new ArrayList<String>(1000000);
return new DummyData(map,list);
}
}
package com.perf.timer;
import java.util.ArrayList;
import java.util.HashMap;
public class DummyData {
HashMap<String, String> map;
ArrayList<String> list;
public DummyData(HashMap<String, String> map, ArrayList<String> list) {
this.map = map;
this.list = list;
}
}
사용 중인 자바 버전이 JDK 5.0 이상이면 시간 측정용으로 만들어진 nanoTime() 메서드를 사용하는 것이 좋다. 초기에 성능이 느리게 나오는데, 그 이유는 클래스가 로딩되면서 성능 저하도 발생하고, JIT Optimizer가 작동하면서 성능 최적화도 되기 때문이라고 보면 된다.
작성된 메서드의 성능을 측정하는 여러 가지 방법이 존재한다. 위에서 살펴본 nanoTime()과 같은 메서드로 측정할 수도 잇지만, 전문 측정 라이브러리를 사용하는 것도 좋은 방법이다.
이 중에서 JMH와 Caliper를 제외한 나머지 툴들은 JUnit으로 만든 테스트 코드들을 실행하는데 사용된다. JMH(Java Microbenchmark Harness)는 JDK를 오픈 소스로 제공하는 OpenJDK에서 만든 성능 측정용 라이브러리다. 위에서 살펴본 HashMap와 ArrayList 객체 생성 속도를 JMH로 확인하는 예제 코드를 살펴보자.
package com.perf.timer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.GenerateMicroBenchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
@BenchmarkMode({ Mode.AverageTime }) // 1.
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 2.
public class CompareTimerJMH {
@GenerateMicroBenchmark // 3.
public DummyData makeObjectWithSize1000000() {
HashMap<String, String> map = new HashMap<String, String>(1000000);
ArrayList<String> list = new ArrayList<String>(1000000);
return new DummyData(map, list);
}
}
1) JMH는 클래스 선언시 반드시 어노테이션을 지정할 필요는 없다. 하지만 그렇게 하면 기본 옵션으로 수행되기 때문에 평균 응답 시간을 측정하기 위해서 @BenchmarkMode로 옵션을 지정하였다.
2) @OutputTimeUnit 어노테이션을 사용하여 출력되는 시간 단위를 밀리초로 지정하였다. 기본이 밀리초이지만, 나중에 옵션을 지정하는 것을 확인하기 위해서 명시적으로 지정을 해 주었다.
3) @GenerateMicroBenchmark는 측정 대상이 되는 메서드를 선언할 때 사용한다. 해당 클래스에 메서드가 많이 있더라도 이 어노테이션을 지정하지 않으면 테스트 대상에서 제외된다.
JHM을 실행한 결과에서 가장 중요한 부분은 다음과 같다.
Run statics "makeObject":min=2.015,avg=2.089,max=2.127,stdev=0.064
min은 최소, avg는 평균, max는 최대 값을 의미한다. 그리고 stdev는 표준편차를 말한다. 그러므로 이와 같은 수치를 볼 때는 평균과 표준편차를 중심으로 보면 된다. 즉, 이 실험은 2.089ms 정도가 소요되었다고 보면 된다.
이처럼 JMH를 사용하면 일일이 케이스를 구성할 필요 없이 각 케이스별로 테스트를 수행하고 그 결과를 확인할 수 있다.
참고