주요글: 도커 시작하기
반응형
TPTP를 사용하여 어플리케이션의 병목 지점을 찾아내는 방법에 대해서 살펴본다.

TPTP를 활용한 병목 지점 찾기

20/80 법칙에 대해서 들어본 적이 있을 것이다. 이를 소포트웨어의 실행 시간에 맞춰서 표현하면, 전체 코드의 20%가 전체 실행 시간의 80%를 차지한다로 설명할 수 있다. 소프트웨어란 대부분 반복적으로 실행되는 코드를 갖고 있으며 for, while 등의 코드가 전체 실행 시간의 80% 이상을 차지하는 경우가 다반사다. 예를 들어, 웹 서버의 경우는 다음과 같은 코드를 갖고 있을 것이다.

    while(isAlive) {
        // 클라이언트 연결 대기
        ...
        // 클라이언트 요청 처리
        ...
    }

웹 서버에서 while 문 안에서 실행되는 코드가 전체 실행 시간의 90% 이상을 차지한다는 것은 누구라도 쉽게 예측할 수 있다. (실제로 웹 서버를 초기화와 종료를 제외한 나머지는 위와 같은 루프를 통해서 실행된다.)

20/80 법칙으로부터 생각해볼 수 있는 것은 전체 실행 시간의 80%를 차지하는 코드에서 병목 현상이 발생한다면 이는 곧 전체 어플리케이션 실행 시간에 영향력을 미친다는 것이다. 즉, 20%의 코드에서 병목이 발생하면 전체 실행 시간의 80%가 병목을 일으킨다는 뜻이 된다. 따라서, 성능이 중요한 어플리케이션의 경우 주요 코드의 병목 부분을 찾아서 해결하게 되면 전체 어플리케이션의 실행 속도를 향상시킬 수 있게 되며, 이는 곧 어플리케이션의 전체 처리량(throughput)을 증가시켜준다.

본 글에서는 TPTP를 활용하여 어떤 코드에서 병목이 발생하는 지 찾아내는 방법을 살펴볼 것이며, 이를 통해 여러분이 만든 어플리케이션의 성능을 향상시킬 수 있는 토대를 만들 수 있을 것이다.

예제 프로그램

먼저 병목 부분을 갖고 있는 예제 어플리케이션을 하나 작성해보자. 참고로 이 예제는 이클립스 사이트의 TPTP 설명 문서인 'Java Application Profiling using TPTP'에 있는 예제를 그대로 사용하였다.

본 예제는 XML 문서로부터 정보를 읽어와 그 내용을 출력해주는 코드인데, test.bottleneck.util.ProductCatalog 클래스 XML 문서를 파싱해서 상품 정보를 읽어오는 기능을 수행한다. ProductCatalog 클래스에서 SAXParser를 구해서 파싱을 시작하는 코드는 다음 부분과 같다. (완전한 소스 코드는 첨부한 파일에 포함되어 있으니 참고하기 바란다.)

    protected void parseContent(File file)
    {
        try {
            SAXParser parser = createParser();            
            InputStream is = new FileInputStream(file);

            if (is == null) { 
                return;
            }
            parser.parse(is, this);
        } 
        catch (Exception e) {
            e.printStackTrace();
        }
        return;
    }

    protected SAXParser createParser() throws ParserConfigurationException, SAXException {
        SAXParserFactory f = SAXParserFactory.newInstance();        
        f.setValidating(false);
        return f.newSAXParser();
    }

이 부분이 본 글에서 살펴볼 병목 부분인데, TPTP를 사용해서 병목을 찾아낸 뒤 어떻게 해결해 나가는 지 차례대로 살펴보도록 하겠다.

실행 시간 분석 도표 활용한 병목 찾기

'Run' > 'Profile As' 메뉴를 사용하여 Product 클래스를 실행해보자. Product 클래스는 제품 정보를 담고 있는 XML 파일이 위치한 디렉토리의 경로를 명령행 인자로 입력받으므로, 프로파일을 실행할 때 'Arguments' 탭에 알맞게 경로를 입력해야 한다. (예를 들면, c:\eclipse3.2\workspace\TestBottleNeck\product 와 같은 경로를 입력)

프로파일 모드로 어플리케이션을 실행한 뒤, 프로파일 좌측 메뉴에서 'Execution Time Analysis'를 더블 클릭해서 실행 시간 분석 결과를 살펴보도록 하자. 그럼, 아래 그림과 비슷한 결과가 출력될 것이다. 아래 그림은 메소드 단위로 출력한 것으로서 패키지나 클래스 단위로도 결과를 볼 수 있다. (본 글에서는 test.bottleneck.* 에 해당하는 클래스를 제외한 나머지는 분석 결과에서 제외하도록 설정하였다.)


위 결과에서 먼저 간단하게 각 컬럼에 대해 설명을 하자면 아래와 같다.

  • Base Time: 메소드 전체 실행 시간 중에서, 메소드 내에서 다른 메소드를 호출할 때 소요된 시간을 제외한 나머지 시간
  • Average Base Time: Base Time을 호출 회수로 나눈 것
  • Cumulative Time: 메소드의 전체 실행 시간
따라서, Base Time의 값이 가장 높은 메소드가 전체 실행 시간 중에서 가장 많은 비중을 차지한다는 것을 알 수 있다. 위 그림의 경우 % 단위를 사용해서 해당 메소드를 실행하는 데 소요된 시간이 전체 어플리케이션 실행 시간의 몇 %에 해당하는 지를 보여주고 있는데, ProductCatalog.createParser() 메소드가 16.30%로 가장 높은 비중을 차지하는 것을 알 수 있다.

따라서 createParser() 메소드는 병목 현상을 일으키는 후보로 생각할 수 있다. 통계표에서 createParser() 부분을 더블 클릭하면 메소드 호출에 대한 상세 정보가 호출되는데, 그 화면은 다음과 같다.


위 그림에서 각 영역은 다음과 같다.

  • Selected method: 선택한 메소드
  • Selected method invoked by: 선택한 메소드를 내부적으로 호출하는 메소드
  • Selected method invokes: 선택한 메소드가 내부적으로 호출하는 메소드
즉, 위 그림의 경우 createParser() 메소드에 대한 정보를 보여주고 있는데, 이 중에서 ProductCatalog.parseContent() 메소드는 내부적으로 createParser() 메소드를 호출하고, createParser() 메소드가 내부적으로 호출하는 메소드는 없다는 것을 의미한다. (실제적으로는 다른 클래스의 메소드를 호출하고 있으나, 본 예제에서는 프로파일링 정보를 test.bottlenck.* 에 대해서만 수집하였기 때문에, 없는 것으로 나왔다).

이 결과에서 중요한 건, parseContent() 메소드의 전체 실행 시간 28.16% 중에서 createParser() 메소드 호출과 관련된 비중이 17% 가깝다는 점이다. 따라서 createParser() 메소드가 병목 지점일 가능성을 다시 한번 확인할 수 있게 되었다. 다시 한번 createParser() 메소드를 살펴보면 다음과 같이 실행 될 때 마다 매번 파서 객체를 생성하는 것을 알 수 있다.

    protected SAXParser createParser() throws ParserConfigurationException, SAXException {
        SAXParserFactory f = SAXParserFactory.newInstance();        
        f.setValidating(false);
        return f.newSAXParser();
    }

이 예제의 경우 SAXParser를 매번 생성하지 않더라도 상관없기 때문에 다음과 같이 처음만 SAXParser를 생성하고 이후에는 이미 생성한 객체를 리턴하도록 코드를 수정할 수 있을 것이다.

    private SAXParser parser = null;
    protected SAXParser createParser() throws ParserConfigurationException, SAXException {
        if (parser == null) {
            SAXParserFactory f = SAXParserFactory.newInstance();        
            f.setValidating(false);
            parser = f.newSAXParser();
        }
        return parser;
    }

위와 같이 변경한 다음에 다시 한번 프로파일링을 해보자. 메소드 실행 정보를 보면 아래와 같이 createParser()가 차지하는 실행 시간 비중이 줄어든 것을 알 수 있으며, 이에 전체적인 실행 시간이 향상된 것을 확인할 수 있다. (퍼센트 단위가 아닌 시간 단위로 결과를 보면 알 수 있다.)



결론

본 글에서는 어플리케이션에서 병목을 일으키는 부분을 TPTP를 사용하여 발견하는 방법에 대해서 살펴보았다. TPTP가 제공하는 'Execution Time Analysis' 기능을 사용해서 실행 시간 중 많은 부분을 차지하는 메소드를 찾는 방법을 알게 되었으며, 이를 통해 병목이 예상되는 메소드를 추려낼 수 있게 되었다. 병목 부분은 전체 어플리케이션의 성능에 영향을 미치기 때문에 반드시 찾아서 없애는 것이 좋으며, TPTP가 이 작업을 도와줄 것이다. 다음 글에서는 마지막으로 에이전트 서버를 사용하여 원격지 JVM에서 실행되는 자바 어플리케이션을 프로파일링 하는 방법에 대해서 살펴보도록 하겠다.

관련링크:

+ Recent posts