1. 개요

Java에서 예외는 일반적으로 비용이 많이 드는 것으로 간주되며 흐름 제어에 사용하면 안 됩니다. 이 예제 은 이러한 인식이 옳다는 것을 증명하고 성능 문제의 원인을 정확히 지적합니다.

2. 환경설정

성능 비용을 평가하는 코드를 작성하기 전에 벤치마킹 환경을 설정해야 합니다.

2.1. 자바 마이크로벤치마크 하네스

예외 오버헤드를 측정하는 것은 간단한 루프에서 메서드를 실행하고 총 시간을 기록하는 것만큼 쉽지 않습니다.

그 이유는 JIT(Just-In-Time) 컴파일러가 방해가 되어 코드를 최적화할 수 있기 때문입니다. 이러한 최적화는 프로덕션 환경에서 실제로 수행하는 것보다 코드의 성능을 향상시킬 수 있습니다. 즉, 거짓 양성 결과가 나올 수 있습니다.

JVM 최적화를 완화할 수 있는 제어된 환경을 만들기 위해 Java Microbenchmark Harness (줄여서 JMH)를 사용합니다.

다음 하위 섹션에서는 JMH의 세부 사항으로 이동하지 않고 벤치마킹 환경 설정을 안내합니다. 이 도구에 대한 자세한 내용은 Microbenchmarking with Java 사용방법(예제)를 확인하십시오.

2.2. JMH 아티팩트 얻기

JMH 아티팩트를 얻으려면 다음 두 의존성을 POM에 추가하십시오.

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.35</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.35</version>
</dependency>

JMH CoreJMH Annotation Processor 의 최신 버전은 Maven Central을 참조하십시오 .

2.3. 벤치마크 클래스

벤치마크를 유지하려면 클래스가 필요합니다.

@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ExceptionBenchmark {
    private static final int LIMIT = 10_000;
    // benchmarks go here
}

위에 표시된 JMH 어노테이션을 살펴보겠습니다.

  • @Fork : JMH가 벤치마크를 실행하기 위해 새 프로세스를 생성해야 하는 횟수를 지정합니다. 값을 1로 설정하여 결과를 보기까지 너무 오래 기다리지 않고 하나의 프로세스만 생성합니다.
  • @Warmup : 워밍업 매개변수를 전달합니다. iterations 요소가 2 라는 것은 결과를 계산할 때 처음 두 실행이 무시됨을 의미합니다.
  • @Measurement : 측정 매개변수를 전달합니다. 반복 값 10은 JMH가 각 메서드를 10번 실행함을 나타냅니다 .
  • @BenchmarkMode : JHM이 실행 결과를 수집하는 방법입니다. AverageTime은 JMH가 메서드가 작업을 완료하는 데 필요한 평균 시간을 계산하도록 요구합니다.
  • @OutputTimeUnit : 출력 시간 단위를 나타냅니다. 이 경우 밀리초입니다.

또한 클래스 본문 내부에는 LIMIT 라는 정적 필드가 있습니다 . 각 메서드 본문의 반복 횟수입니다.

2.4. 벤치마크 실행

벤치마크를 실행하려면 기본 메서드가 필요합니다.

public class MappingFrameworksPerformance {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

프로젝트를 JAR 파일로 패키징하고 명령줄에서 실행할 수 있습니다. 물론 지금 그렇게 하면 벤치마킹 방법을 추가하지 않았기 때문에 빈 출력이 생성됩니다.

편의상 maven-jar-plugin 을 POM에 추가할 수 있습니다. 이 플러그인을 사용 하면 IDE 내 에서 기본 메서드를 실행할 수 있습니다.

<groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>com.baeldung.performancetests.MappingFrameworksPerformance</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

최신 버전의 maven-jar-plugin 은 여기 에서 찾을 수 있습니다 .

3. 성능 측정

성능을 측정하기 위한 몇 가지 벤치마킹 방법이 필요할 때입니다. 이러한 각 메서드는 @Benchmark 어노테이션을 전달해야 합니다.

3.1. 정상적으로 반환하는 방법

정상적으로 반환되는 메서드부터 시작하겠습니다. 즉, 예외를 throw하지 않는 메서드입니다.

@Benchmark
public void doNotThrowException(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        blackhole.consume(new Object());
    }
}

blackhole 매개변수 는 Blackhole 의 인스턴스를 참조합니다 . 이것은 적시 컴파일러가 수행할 수 있는 최적화인 데드 코드 제거를 방지하는 데 도움이 되는 JMH 클래스입니다.

이 경우 벤치마크는 예외를 발생시키지 않습니다. 실제로 예외를 throw하는 항목의 성능을 평가하기 위한 참조로 사용할 것입니다.

main 메서드를 실행하면 다음과 같은 보고서가 제공됩니다.

Benchmark                               Mode  Cnt  Score   Error  Units
ExceptionBenchmark.doNotThrowException  avgt   10  0.049 ± 0.006  ms/op

이 결과에는 특별한 것이 없습니다. 벤치마크의 평균 실행 시간은 0.049밀리초로 그 자체로는 의미가 없습니다.

3.2. 예외 생성 및 발생

다음은 예외를 발생시키고 포착하는 또 다른 벤치마크입니다.

@Benchmark
public void throwAndCatchException(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e);
        }
    }
}

출력을 살펴보겠습니다.

Benchmark                                  Mode  Cnt   Score   Error  Units
ExceptionBenchmark.doNotThrowException     avgt   10   0.048 ± 0.003  ms/op
ExceptionBenchmark.throwAndCatchException  avgt   10  17.942 ± 0.846  ms/op

doNotThrowException 메서드 실행 시간의 작은 변화는 중요하지 않습니다. 기본 OS 및 JVM 상태의 변동일 뿐입니다. 중요한 점 은 예외를 발생시키면 메서드 실행 속도가 수백 배 느려진다는 것입니다.

다음 몇 개의 하위 섹션에서는 정확히 무엇이 이러한 극적인 차이를 초래하는지 알아낼 것입니다.

3.3. 던지지 않고 예외 만들기

예외를 만들고, 던지고, 잡는 대신, 그냥 만들겠습니다.

@Benchmark
public void createExceptionWithoutThrowingIt(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        blackhole.consume(new Exception());
    }
}

이제 선언한 세 가지 벤치마크를 실행해 보겠습니다.

Benchmark                                            Mode  Cnt   Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt  avgt   10  17.601 ± 3.152  ms/op
ExceptionBenchmark.doNotThrowException               avgt   10   0.054 ± 0.014  ms/op
ExceptionBenchmark.throwAndCatchException            avgt   10  17.174 ± 0.474  ms/op

결과는 놀라울 수 있습니다. 첫 번째 방법과 세 번째 방법의 실행 시간은 거의 같지만 두 번째 방법은 훨씬 더 짧습니다.

이 시점에서 throw catch 자체가 상당히 저렴하다는 것이 분명합니다. 반면에 예외를 생성하면 높은 오버헤드가 발생합니다.

3.4. 스택 추적을 추가하지 않고 예외 발생

예외를 생성하는 것이 일반 개체를 수행하는 것보다 훨씬 더 많은 비용이 드는 이유를 알아봅시다.

@Benchmark
@Fork(value = 1, jvmArgs = "-XX:-StackTraceInThrowable")
public void throwExceptionWithoutAddingStackTrace(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e);
        }
    }
}

이 방법과 하위 섹션 3.2의 방법 사이의 유일한 차이점은 jvmArgs 요소입니다. 해당 값 -XX:-StackTraceInThrowable 은 스택 추적이 예외에 추가되지 않도록 하는 JVM 옵션입니다.

벤치마크를 다시 실행해 보겠습니다.

Benchmark                                                 Mode  Cnt   Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt       avgt   10  17.874 ± 3.199  ms/op
ExceptionBenchmark.doNotThrowException                    avgt   10   0.046 ± 0.003  ms/op
ExceptionBenchmark.throwAndCatchException                 avgt   10  16.268 ± 0.239  ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace  avgt   10   1.174 ± 0.014  ms/op

예외를 스택 추적으로 채우지 않음으로써 실행 시간을 100배 이상 줄였습니다. 분명히 스택을 살펴보고 해당 프레임을 예외에 추가하면 우리가 본 느림이 발생합니다.

3.5. 예외 발생 및 스택 추적 해제

마지막으로 예외를 던지고 예외를 포착할 때 스택 추적을 해제하면 어떤 일이 발생하는지 살펴보겠습니다.

@Benchmark
public void throwExceptionAndUnwindStackTrace(Blackhole blackhole) {
    for (int i = 0; i < LIMIT; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
            blackhole.consume(e.getStackTrace());
        }
    }
}

결과는 다음과 같습니다.

Benchmark                                                 Mode  Cnt    Score   Error  Units
ExceptionBenchmark.createExceptionWithoutThrowingIt       avgt   10   16.605 ± 0.988  ms/op
ExceptionBenchmark.doNotThrowException                    avgt   10    0.047 ± 0.006  ms/op
ExceptionBenchmark.throwAndCatchException                 avgt   10   16.449 ± 0.304  ms/op
ExceptionBenchmark.throwExceptionAndUnwindStackTrace      avgt   10  326.560 ± 4.991  ms/op
ExceptionBenchmark.throwExceptionWithoutAddingStackTrace  avgt   10    1.185 ± 0.015  ms/op

스택 추적을 해제하는 것만으로도 실행 시간이 약 20배나 엄청나게 증가하는 것을 볼 수 있습니다. 다른 말로 하면 예외를 throw하는 것 외에도 예외에서 스택 추적을 추출하면 성능이 훨씬 더 나빠집니다.

4. 결론

이 사용방법(예제)에서는 예외의 성능 효과를 분석했습니다. 특히 성능 비용이 대부분 예외에 스택 추적을 추가하는 데 있음을 발견했습니다. 나중에 이 스택 추적을 풀면 오버헤드가 훨씬 커집니다.

예외를 던지고 처리하는 것은 비용이 많이 들기 때문에 정상적인 프로그램 흐름에 사용해서는 안 됩니다. 대신 이름에서 알 수 있듯이 예외는 예외적인 경우에만 사용해야 합니다.

전체 소스 코드는 GitHub 에서 찾을 수 있습니다 .

Generic footer banner