성능과 최적화

잘못된 접근 방식

초기 버전의 자바는 메서드 디스패치 성능이 매우 나빴습니다. 이를 해결하기 위해 일부 자바 개발자들은 작은 메서드를 작성하는 것을 피하고 대신 거대한 메서드를 작성할 것을 권장했습니다.

물론 시간이 지나면서 가상 디스패치의 성능은 크게 향상되었습니다. 뿐만 아니라, 최신 자바 가상 머신(JVM)과 특히 자동 관리 인라이닝 덕분에 이제 대부분의 호출 지점에서 가상 디스패치는 제거되었습니다. "모든 것을 하나의 메서드로 묶어라"는 조언을 따랐던 코드는 이제 현대의 JIT(Just-in-Time) 컴파일러에 매우 불리하게 작용합니다.

참고 사항 자바 코드의 실행 속도는 매우 동적이며 기본 자바 가상 머신에 근본적으로 의존합니다. 오래된 자바 코드는 자바 소스 코드를 다시 컴파일하지 않고도 최신 JVM에서 더 빠르게 실행될 수 있습니다.

성능 엔지니어링을 구성하는 다양한 측면에서 집중 적으로 다루는 것이 이 책의 목표입니다.

- 전체 소프트웨어 라이프사이클 내의 성능 방법론
- 성능에 적용되는 테스트 이론
- 측정, 통계, 도구
- 분석 기술(시스템 및 데이터 모두)
- 기본 기술과 메커니즘

해당 섹션을 건너뛰고 맥락을 제대로 이해하지 못한 상태에서 자세히 설명된 기법을 적용하지 마십시오. 이러한 모든 기법은 제대로 적용하지 않으면 이익보다 해를 끼칠 수 있습니다.

일반적으로 다음과 같은 것들이 있습니다:

- JVM을 더 빠르게 만드는 마법 같은 스위치가 없습니다
- 자바를 더 빠르게 만드는 "팁과 트릭"이 없습니다
- 여러분에게 숨겨진 비밀 알고리즘이 없습니다

자바 성능 개요

초기 자바는 환경이 충분히 빠르기만 하면 개발자 생산성이 향상된다면 원시적인 성능을 희생할 수 있다는 태도를 취했습니다.

따라서 HotSpot과 같은 JVM의 성숙도와 정교함이 증가하면서 자바 환경이 고성능 컴퓨팅 애플리케이션에 적합해진 것은 비교적 최근의 일입니다.

이 실용성은 자바 플랫폼에서 여러 가지 방식으로 나타나지만, 그 중 가장 뚜렷한 예는 관리되는 서브시스템의 사용입니다. 이는 개발자가 낮은 수준의 제어 측면을 포기하고 관리되는 기능의 일부 세부 사항에 대해 걱정하지 않아도 된다는 아이디어입니다. 가장 분명한 예는 물론 메모리 관리입니다. JVM은 프로그래머가 수동으로 메모리를 추적할 필요 없이 플러그형 가비지 컬렉션 서브시스템 형태로 자동 메모리 관리를 제공합니다.

참고 사항

관리되는 서브시스템은 JVM 전반에 걸쳐 존재하며, 이로 인해 JVM 애플리케이션의 런타임 동작에 추가적인 복잡성이 도입됩니다.   
다음 섹션에서 논의할 것처럼, JVM 애플리케이션의 복잡한 런타임 동작은 애플리케이션을 테스트 중인 실험으로 취급해야 함을 요구합니다.   
이는 우리가 관찰된 측정값의 통계에 대해 생각하게 하며, 여기에서 불행한 발견을 하게 됩니다.

JVM 애플리케이션의 관찰된 성능 측정값은 매우 자주 정규 분포를 따르지 않습니다.   
이는 기본 통계 기법(예: 표준 편차와 분산)이 결과 분포의 정규성에 대한 암묵적인 가정을 포함하고 있기 때문에 JVM 애플리케이션의 결과를 처리하는 데 부적합하다는 것을 의미합니다.

자바 성능 측정에 현혹되기 매우 쉽습니다. 환경의 복잡성으로 인해 시스템의 개별 측면을 격리하기 매우 어렵습니다.   
측정 자체에도 오버헤드가 있으며, 빈번한 샘플링(또는 모든 결과 기록)은 기록되는 성능 수치에 영향을 미칠 수 있습니다.   
자바 성능 수치는 일정 수준의 통계적 정교함을 요구하며, 단순한 기법은 자바/JVM 애플리케이션에 적용될 때 종종 잘못된 결과를 초래합니다.

실험 과학으로서의 성능

자바/JVM 소프트웨어 스택은 대부분의 현대 소프트웨어 시스템처럼 매우 복잡합니다. 실제로, JVM의 고도로 최적화되고 적응적인 특성 덕분에 JVM 위에 구축된 생산 시스템은 믿을 수 없을 정도로 미묘하고 복잡한 성능 동작을 가질 수 있습니다. 이러한 복잡성은 무어의 법칙과 이를 대표하는 하드웨어 능력의 전례 없는 성장을 가능하게 했습니다.

따라서 JVM 성능 튜닝은 기술, 방법론, 측정 가능한 양, 도구의 종합입니다. 그 목적은 시스템의 소유자나 사용자가 원하는 방식으로 측정 가능한 출력을 효과적으로 만드는 것입니다.

원하는 결과를 달성하기 위해 다음과 같은 과정을 거칩니다

1. 원하는 결과 정의
2. 기존 시스템 측정
3. 요구 사항을 달성하기 위해 무엇을 해야 하는지 결정
4. 개선 작업 수행
5. 재테스트
6. 목표 달성 여부 확인

원하는 성능 결과를 정의하고 결정하는 과정은 정량적인 목표 세트를 구축합니다. 무엇을 측정해야 하는지 설정하고 목표를 기록하는 것이 중요하며, 이는 프로젝트의 산출물과 전달물의 일부가 됩니다. 이를 통해 성능 분석은 비기능적 요구 사항을 정의하고 이를 달성하는 데 기반을 두고 있음을 알 수 있습니다.

성능을 위한 분류학

이 섹션에서는 몇 가지 기본 성능 메트릭을 소개합니다. 이는 성능 분석을 위한 어휘를 제공하고, 튜닝 프로젝트의 목표를 정량적인 용어로 설정할 수 있게 해줍니다. 이러한 목표는 성능 목표를 정의하는 비기능적 요구 사항입니다. 일반적인 기본 성능 메트릭 세트는 다음과 같습니다:

  • 처리량(Throughput)

  • 지연 시간(Latency)

  • 용량(Capacity)

  • 활용도(Utilization)

  • 효율성(Efficiency)

  • 확장성(Scalability)

  • 저하(Degradation)

각각을 간략히 논의하겠습니다. 대부분의 성능 프로젝트에서 모든 메트릭을 동시에 최적화하는 것은 아닙니다. 단일 성능 반복에서 몇 가지 메트릭만 개선되는 경우가 훨씬 더 일반적이며, 이는 한 번에 조정할 수 있는 만큼의 수일 수 있습니다. 실제 프로젝트에서는 하나의 메트릭을 최적화하면 다른 메트릭이나 메트릭 그룹이 손해를 볼 수 있는 경우가 많습니다.

성능 메트릭

정의

처리량(Throughput)

처리량은 시스템이나 서브시스템이 수행할 수 있는 작업의 속도를 나타내는 메트릭입니다. 이는 보통 일정 시간 동안의 작업 단위 수로 표현됩니다.

지연 시간(Latency)

지연 시간은 단일 트랜잭션을 처리하고 결과를 보는 데 걸리는 시간으로, 일반적으로 종단 간 시간으로 인용됩니다.

용량(Capacity)

용량은 시스템이 가지고 있는 작업 병렬성의 양—즉, 시스템에서 동시에 진행할 수 있는 작업 단위(예: 트랜잭션)의 수를 나타냅니다. 용량은 특정 지연 시간이나 처리량 값에서 사용 가능한 처리 능력으로 인용됩니다.

사용률(Utilization)

사용률은 시스템 자원의 효율적인 사용을 달성하는 것을 목표로 하며, 이상적으로는 CPU가 작업 단위를 처리하는 데 사용되어야 합니다.

효율성(Efficiency)

효율성은 시스템의 처리량을 사용된 자원으로 나눈 값으로 측정되며, 동일한 처리량을 생성하는 데 더 많은 자원이 필요하다면 이는 덜 효율적임을 의미합니다.

확장성(Scalability)

확장성은 처리량이나 용량이 처리에 사용 가능한 자원에 따라 어떻게 변하는지를 측정하며, 자원이 추가됨에 따라 처리량이 그에 맞춰 변하는지를 평가하는 척도입니다.

저하(Degradation)

저하는 시스템에 부하를 증가시킬 때 지연 시간이나 처리량의 변화가 발생하는 것을 의미하며, 시스템이 과소 활용되고 있을 때와 자원이 완전히 활용되고 있을 때의 차이를 포함합니다.

연결된 관찰치들 사이의 관계

다양한 성능 관찰치들의 동작은 보통 어떤 식으로든 연결되어 있습니다. 이 연결의 세부 사항은 시스템이 최대 활용도에서 실행되고 있는지 여부에 따라 달라집니다. 예를 들어, 일반적으로 부하가 증가함에 따라 활용도도 변할 것입니다. 그러나 시스템이 과소 활용되고 있다면, 부하를 증가시켜도 활용도가 크게 증가하지 않을 수 있습니다. 반대로, 시스템이 이미 스트레스 상태라면 부하를 증가시키는 영향이 다른 관찰치에 나타날 수 있습니다.

또 다른 예로, 확장성과 저하는 모두 더 많은 부하가 추가될 때 시스템의 동작 변화입니다. 확장성의 경우, 부하가 증가함에 따라 사용 가능한 자원도 증가하며, 중요한 질문은 시스템이 이를 활용할 수 있는지 여부입니다. 반면에, 부하가 추가되었지만 추가 자원이 제공되지 않는다면, 저하된 성능 관찰치(예: 지연 시간)가 예상되는 결과입니다.

참고 사항 드문 경우지만, 추가 부하는 직관에 반하는 결과를 초래할 수 있습니다. 예를 들어, 부하의 변화가 시스템의 일부가 더 많은 자원을 소모하지만 더 높은 성능 모드로 전환되게 한다면, 더 많은 요청이 들어오더라도 전체적인 지연 시간이 줄어들 수 있습니다.

예를 들어, 9장에서 HotSpot의 JIT 컴파일러에 대해 자세히 논의할 것입니다. JIT 컴파일 대상이 되려면 메서드가 "충분히 자주" 인터프리터 모드에서 실행되어야 합니다. 따라서 낮은 부하에서는 주요 메서드들이 인터프리터 모드에 머물러 있을 수 있지만, 메서드 호출 빈도가 증가함에 따라 컴파일 대상이 되어 나중에 동일한 메서드 호출이 훨씬 더 빠르게 실행될 수 있습니다.

다양한 워크로드는 매우 다른 특성을 가질 수 있습니다. 예를 들어, 금융 시장의 거래는 전체적으로 볼 때 실행 시간(즉, 지연 시간)이 몇 시간 또는 며칠이 될 수 있습니다. 그러나 주요 은행에서는 동시에 수백만 건의 거래가 진행 중일 수 있습니다. 따라서 시스템의 용량은 매우 크지만, 지연 시간도 큽니다.

하지만 은행 내의 단일 서브시스템만을 고려해 보겠습니다. 매수자와 매도자를 매칭하는 것(본질적으로 가격에 동의하는 당사자들)이 주문 매칭이라고 알려져 있습니다. 이 개별 서브시스템은 동시에 처리 중인 주문이 수백 건에 불과할 수 있지만, 주문 수락부터 완료된 매칭까지의 지연 시간은 1밀리초(또는 "저지연" 거래의 경우 그보다 더 짧을 수 있습니다)일 수 있습니다.

Last updated