디버거의 속도 저하 문제 해결 방법

다른 언어: English Español Português 中文

일반적으로 디버거의 오버헤드는 최소한이지만, 특정 상황에서는 Java 디버거가 상당한 런타임 비용을 초래할 수 있습니다. 더 안 좋은 시나리오에서는 디버거가 VM을 완전히 멈출 수도 있습니다.

이 글에서는 이러한 문제의 원인과 가능한 해결책에 대해 살펴보겠습니다.

Info icon

이 글에서 설명하는 사항들은 JetBrains IDE에 특정하며, 언급된 기능 중 일부는 다른 도구에서 사용할 수 없을 수 있습니다. 그렇지만, 일반적인 문제 해결 전략은 그대로 적용 할 수 있습니다.

원인 진단

가능한 해결책을 살펴보기 전에 문제를 식별해야 합니다. 디버거가 애플리케이션을 느리게 만드는 가장 흔한 원인은 다음과 같습니다.


IntelliJ IDEA는 디버거의 오버헤드 (Overhead) 탭에서 자세한 통계를 제공하여 디버거 성능 문제의 원인을 식별하는 데 있어 추측을 배제합니다.

IntelliJ IDEA에서의 Overhead 탭 IntelliJ IDEA에서의 Overhead 탭

이 탭에 접근하려면, 레이아웃 설정 (Layout Settings) 탭에서 오버헤드 (Overhead) 를 선택하세요. 오버헤드 (Overhead) 탭은 중단점과 디버거 기능의 목록을 보여줍니다. 각 중단점 또는 기능 옆에는 각 디버거 기능이 사용된 횟수와 실행하는 데 걸린 시간을 볼 수 있습니다.

Tip icon

오버헤드 (Overhead) 탭에서는 해당 기능의 체크박스를 선택 취소하여 리소스를 많이 사용하는 기능을 일시적으로 끌 수도 있습니다.

성능 문제의 원인을 식별하는 방법을 살펴보았으니, 가장 흔한 원인과 이를 어떻게 해결하는지 살펴보겠습니다.

메서드 중단점

Java에서 메서드 중단점을 사용할 때, 사용하는 디버거에 따라 성능 저하를 경험할 수 있습니다. 이는 Java Debug Interface에서 제공하는 해당 기능이 두드러지게 느리기 때문입니다.

이 문제를 해결하기 위해 IntelliJ IDEA는 에뮬레이션된 메서드 중단점을 제공합니다. 이는 일반적인 메서드 중단점과 같이 작동하지만, 더 효율적으로 작동합니다. 에뮬레이션된 메서드 중단점은 내부적으로 트릭이 포함되어 있습니다. 실제 메서드 중단점을 설정하는 대신, IDE는 프로젝트 전체의 모든 메서드 구현 내에서 일반 줄 중단점으로 대체합니다.

기본적으로, IntelliJ IDEA의 모든 메서드 중단점은 에뮬레이션 처리됩니다:

'Emulated' 옵션이 활성화된 중단점 설정 팝업 'Emulated' 옵션이 활성화된 중단점 설정 팝업

이 기능이 없는 디버거를 사용하고 있고 메서드 중단점과 관련된 성능 문제가 생긴다면, 같은 방법을 수동으로 적용하면 됩니다. 모든 메서드를 구현하는 것은 지루할 수 있지만, 디버그하는 동안 시간을 절약할 수 있어 보람이 있을 수 있습니다.

‘에뮬레이션된 메서드 중단점의 클래스 처리 중’가 너무 오래 걸림

메서드에 구현이 너무 많이 있으면, 해당 메서드에 메서드 중단점을 설정하는 데 시간이 걸릴 수 있습니다. 이 경우, IntelliJ IDEA와 Android Studio는 에뮬레이션된 메서드 중단점의 클래스 처리 중 (Processing classes for emulated method breakpoints) 라는 대화 상자를 보여줍니다.

메서드 중단점을 에뮬레이션 처리하는 과정이 너무 오래 걸린다면, 대신에 줄 중단점을 사용하는 것을 고려해 보세요. 또는, 중단점 설정에서 에뮬레이션 처리됨 (Emulated) 체크박스를 해제하여 런타임 성능과 절충하는 것도 가능합니다.

핫 코드의 조건부 중단점

핫 코드에서 조건부 중단점을 설정하면, 이 코드가 얼마나 자주 실행되는지에 따라 디버그 세션을 급격히 느리게 할 수 있습니다.

다음 코드 스니펫을 고려해 보세요:


public class Loop {

    public static final int ITERATIONS = 100_000;

    public static void main(String[] args) {
        var start = System.currentTimeMillis();
        var sum = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            sum += i;
        }
        var end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.printf("The loop took: %d ms\n", end - start);
    }
}
const val ITERATIONS = 100_000

fun main() = measureTimeMillis {
    var sum = 0
    for (i in 0 until ITERATIONS) {
        sum += i
    }
    println(sum)
}.let { println("The loop took: $it ms") }

sum += i 에서 중단점을 설정하고 false 를 조건으로 지정하겠습니다. 이는 디버거가 이 중단점에서 절대 멈춰서는 안된다는 것을 의미합니다. 그러나, 이 줄이 실행될 때마다 디버거는 false 를 평가해야 합니다.

'false'로 설정된 조건이 있는 중단점 설정 대화 상자 'false'로 설정된 조건이 있는 중단점 설정 대화 상자

이 경우, 중단점 있는 코드와 또는 중단점 없는 코드의 실행 결과를 비교하면, 각각 39 ms29855 ms였습니다. 특히, 100,000회의 반복만으로도 차이가 굉장히 큰 것을 알 수 있습니다!

false 와 같이 사소한 조건을 평가하는 것이 이렇게 많은 시간을 필요로 하는 것이 놀랍게 보일 수 있습니다. 이는 소요된 시간이 표현식의 결과 계산만으로 인한 것이 아니라, 디버거 이벤트 처리와 디버거 프론트엔드와의 통신 등도 포함되기 때문입니다.

해결 방법은 간단합니다. 조건을 애플리케이션 코드에 직접 통합하면 됩니다:


public class Loop {

    public static final int ITERATIONS = 100_000;

    public static void main(String[] args) {
        var start = System.currentTimeMillis();
        var sum = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            if (false) { // condition goes here
                System.out.println("break") // breakpoint goes here
            }
            sum += i;
        }
        var end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.printf("The loop took: %d ms\n", end - start);
    }
}
fun main() = measureTimeMillis {
    var sum = 0
    for (i in 0 until ITERATIONS) {
        if (false) { // condition goes here
            println("break") // breakpoint goes here
        }
        sum += i
    }
    println(sum)
}.let { println("The loop took: $it ms") }

이 설정을 사용하면, VM은 조건의 코드를 직접 실행하며, 이 코드를 최적화할 수도 있습니다. 반면에, 디버거는 중단점을 만났을 때만 작동합니다. 대부분의 경우에는 필요하지 않지만, 이 변경은 핫 경로에서 조건부로 프로그램을 일시 중단해야 하는 경우에 시간을 절약할 수 있습니다.

설명된 기술은 소스 코드가 사용가능한 클래스에서 완벽하게 작동합니다. 그러나, 컴파일된 코드, 라이브러리와 같은 경우에는 방법을 성공적으로 적용하는 것이 더 어려울 수 있습니다. 이는 특별한 경우로, 별도의 글에서 다루겠습니다.

묵시적인 평가

중단점 조건감시와 같이 표현식을 직접 지정할 수 있는 기능 외에도 묵시적으로 표현식을 평가하는 기능도 있습니다.

다음은 그 예제입니다:

IntelliJ IDEA의 변수 보기는 컬렉션 구현의 세부 정보를 숨기고
컬렉션의 내용을 보기 쉬운 형태로 보여줍니다 IntelliJ IDEA의 변수 보기는 컬렉션 구현의 세부 정보를 숨기고
컬렉션의 내용을 보기 쉬운 형태로 보여줍니다

프로그램을 일시 중단할 때마다, 디버거는 현재 컨텍스트에서 사용 가능한 변수의 값을 표시합니다. 어떤 유형은 보기와 탐색이 어려운 복잡한 구조를 갖고 있을 수 있습니다. 편의를 위해, 디버거는 렌더러라는 특별한 표현식을 사용하여 여러분의 변수들을 변형합니다.

렌더러는 toString() 과 같이 사소할 수도 있고, 컬렉션의 내용을 변환하는 것과 같이 더 복잡할 수도 있습니다. 이는 내장형이거나 사용자 정의형일 수 있습니다.

Tip icon

IntelliJ IDEA의 디버거는 데이터를 어떻게 표시할지에 대해 매우 유연한 방식을 취합니다. IDE는 어노테이션을 사용하여 렌더러를 구성할 수 있도록 허용하여, 여러 참여자가 있는 프로젝트에서 일관된 클래스 표현을 보장합니다. 데이터를 표시하는 형식을 구성하는 방법에 대해 더 자세히 알아보려면, IntelliJ IDEA의 문서를 참조하세요.

일반적으로, 디버그 렌더러가 가져오는 오버헤드는 무시할 수 있지만, 그 영향은 특히 사용 사례에 따라 달라집니다. 실제로, toString() 구현 중 일부에 암호화 마이닝을위한 코드가 포함되어 있다면, 디버거는 해당 클래스의 toString() 값을 보여주는 데 어려움을 겪을 것입니다.

특정 클래스를 렌더링하는 것이 느리다면 해당 렌더러를 끌 수 있습니다. 더 유연한 대안으로 필요할 때만 렌더러를 사용하도록 설정할 수도 있습니다. 온디맨드 렌더러는 결과를 표시하도록 명시적으로 요청할 때만 실행됩니다.

원격 디버그 세션에서의 높은 지연 시간

기술적인 관점에서 보면, 원격 애플리케이션을 디버그하는 것은 로컬에서 디버그하는 것과 다르지 않습니다. 어느 경우에든 연결은 소켓을 통해 이루어집니다 – 여기서는 공유 메모리 모드를 제외하겠습니다 – 그리고 디버거는 호스트 JVM이 어디에서 작동하는지조차 인식하지 못합니다.

그러나, 원격 디버그에 특유한 요소 중 하나는 네트워크 지연시간이 있습니다. 특정 디버거 기능은 사용될 때마다 여러 번 네트워크를 왕복합니다. 높은 지연 시간과 결합하면, 이는 디버그 성능의 큰 감소로 이어질 수 있습니다.

이 경우, 프로젝트를 로컬에서 실행하는 것을 생각해 보세요. 이 방법은 시간을 많이 절약할 수 있습니다. 아니면, 일부 고급 기능을 일시적으로 끄는 것이 유익할 수 있습니다.

결론

이 글에서는 디버거 성능이 떨어지는 가장 일반적인 이슈를 어떻게 해결하는지 알아보았습니다. IDE가 때로는 이 문제를 해결하기는 하지만, 기본적인 메커니즘을 이해하는 것이 중요하며, 이는 여러분이 일상적인 디버그 작업에서 더 유연하고 효율적이며 창의적으로 만들어 줄 것입니다.

이 팁과 방법이 유용했기를 바라며, 의견이 있다면 언제든 연락주세요. X, LinkedIn, 또는 Telegram을 통해 저에게 연락해 주세요.

즐겁게 디버그하세요!

all posts ->