Java/자바 최적화

[Java] 자바 최적화(1) - 성능 지표

ready-go 2023. 3. 15. 20:18

Java의 성능

Java는 매우 실용적인 언어다. 태초부터 개발자의 생산성을 위해 어느 정도의 성능을 희생하도록 만들어졌다. C/C++같은 언어와는 달리 JVM 위에서 실행되기 때문이다. JVM은 개발자 대신 로우 레벨의 동작들을 일부 관리해준다. 예를 들면, C/C++에서 개발자들은 명시적으로 메모리를 해제하여 관리해줘야 하지만, JVM은 Garbage Collector가 이 일을 대신해준다. 개발자는 편하지만, 메모리 관리가 잘 되고 있는지는 알 수 없다.

 

JVM의 동작을 예측하는 것은 매우 어렵기 때문에, Java 개발자들은 성능 최적화를 위해서 실험을 해볼 수 밖에 없다. 어플리케이션의 코드와 여러 조건들을 바꿔가면서 서버를 띄우고, 실제 서비스와 비슷한 워크로드로 실험을 진행한다. 이렇게 나온 결과들을 분석하여 최적화하는 과정을 반복한다.

 

여기서 가장 중요한 것은 실험의 결과를 분석하는 일이다. 앞서 말했듯, JVM의 동작은 매우 복잡하고 예측하기 어렵기 때문에 간접적인 지표들이 필요하다. JVM 성능 최적화에서 사용되는 여러 가지 지표들을 살펴보자.

 

처리율(Throughput)

처리율은 단위 시간동안 완료한 작업의 수를 의미한다. '작업'이라는 것은 관심사에 따라 다양하게 적용될 수 있다. 예를 들어, 네트워크 통신에서의 작업은 데이터를 전송하는 것이기 때문에 초당 전송할 수 있는 바이트(BPS)를 측정한다. 웹 어플리케이션에서는 사용자의 요청을 처리하는 것이 목적이기 때문에 초당 처리할 수 있는 트랜잭션(TPS)을 측정한다.

 

처리율은 조건에 따라 크게 달라질 수 있다. 테스트에 사용되는 워크로드가 무엇인지, 실험이 진행된 운영체제가 무엇인지, 또 CPU의 종류와 메모리의 사이즈같은 하드웨어 스펙에 따라서도 달라진다. 따라서 성능을 비교하려면 가급적 동일한 조건에서 테스트하고, 다양한 조건에서 테스트를 할수록 결과의 신뢰성이 높아진다.

 

지연(Latency)

지연은 클라이언트가 요청을 보내고, 응답을 받을 때까지의 시간을 말한다. 처리율이 서버 입장에서의 주요 관심사라면, 지연은 철저하게 클라이언트의 관심사라고 할 수 있다. 응답 시간은 사용자의 만족도와 직결된 문제이기 때문에 매우 중요하다.

 

지연은 단순히 하나의 요청을 보내서 시간을 측정하는 것으로 끝나지 않는다. 서버에 부담이 될 만큼의 요청을 보냈을 때 응답 시간의 분포가 어떻게 되는지를 살펴봐야 한다. 약간 극단적인 예시지만, 99%의 요청은 10ms 내에 처리되었고 나머지 1%는 10초가 걸렸다고 해보자. 이러한 서비스가 있다면 매번 1%의 사용자를 잃게 될 것이다. 따라서 지연을 측정할 때는 응답 시간의 분포를 그래프나 백분위수를 이용해 표현하는 경우가 많다.

 

용량(Capacity)

용량은 동시에 처리할 수 있는 작업의 수로, 병렬성을 측정하는 지표이다. 웹 서버의 경우 스레드 풀의 크기 등으로 표시할 수 있겠지만, 대부분의 경우 측정하기 애매한 경우가 많고 처리율 등 다른 지표로 간접적으로 대체할 수 있기 때문에 잘 사용하지는 않는 것 같다. 실제로 구글 검색을 해 보면 capacity라는 용어는 주로 사용할 수 있는 메모리의 크기를 나타낼 때 사용되고 있다.

 

사용률(Utilization)

사용률은 얼마나 리소스를 효율적으로 사용하느냐를 측정하는 지표로, 보통 CPU의 사용률을 측정할 때 사용된다. CPU 사용률은 어떤 작업이 완료될 때 까지 걸린 시간 중에 CPU가 일을 한 시간을 백분율로 나타낸다. CPU의 사용률이 높다는 뜻은 그만큼 스레드가 블록되어 낭비되는 시간이 적다는 뜻이다.

 

CPU 사용률은 워크로드에 따라서 달라질 수 있다. 예를 들어, 그래픽 작업같이 계산이 많이 필요한 작업은 CPU가 쉴 틈이 없다. 반면에 디스크 I/O를 하거나, 외부 API를 요청하여 스레드가 블록되는 경우에는 CPU가 대기하는 시간이 길다. 이렇게 블록되어 낭비되는 시간을 줄이기 위한 방법이 리액티브 프로그래밍이다.

 

효율(Efficiency)

효율은 처리율을 사용률로 나눈 값이다. 직관적으로 이해하기 어려울 수 있지만, 풀어서 설명하면 쉽다. 똑같은 양의 리소스를 사용한다고 했을 때, 처리량이 많을수록 효율이 높은 것이다. 반대로, 같은 양의 작업을 처리하는데 CPU 사용량이 많으면 효율이 낮은 것이 당연하다.

 

단순히 처리 시간과 리소스 뿐만 아니라, 실제로 투입되는 비용을 측정하기도 한다. 예를 들어, 같은 일을 처리하는데 AWS를 사용하면 100달러, GCP를 사용하면 110달러가 든다면 효율성이 높은 AWS를 선택하게 될 것이다.

 

확장성(Scalability)

확장성은 더 많은 리소스를 투입했을 때, 처리율이 얼마나 변하는지를 나타내는 지표다. 예를 들어, 어플리케이션 서버를 1대에서 2대로 늘린다면 처리율이 2배 가까이 늘어날 수 있다. 그러나 실제로는 리소스가 2배가 된다고 해서 처리율이 2배로 선형 증가하지는 않는다.

 

이것은 확장성이 다른 요인들에 영향을 많이 받기 때문이다. 위의 예시처럼 어플리케이션 서버를 늘리는 경우, DB는 여전히 1대를 사용하기 때문에 병목현상이 발생할 수 있으며, 세션을 사용했다면 세션을 공유하는 저장소를 추가하거나, 로드밸런서의 구현에 따라서도 처리율이 달라질 수 있다.

 

이렇게 확장에 필요한 추가적인 비용을 고려하면 어느 시점에서는 처리율이 더 이상 증가하지 않거나, 오히려 감소할 수도 있다. 따라서 확장성을 나타낼 때는 정확한 수치보다 대략적인 추세를 사용하는 것이 일반적이다.

 

저하(Degradation)

저하는 시스템이 부하를 많이 받을때, 처리율이 감소하는 정도를 나타낸다. 저하는 사용률과도 관련이 있는데, 대부분의 경우 CPU 사용률이 여유롭다면 더 많은 요청이 들어오는 경우 처리율이 증가하게 된다. 그러다 CPU 사용률이 너무 높아지면 에러가 발생하거나, 지연이 일어나면서 처리율이 떨어진다.

 

성능 트레이드 오프

위에서 7가지 지표들을 살펴보았는데, 사실 이 지표들은 서로 독립적이지 않다. 예를 들어, 확장성을 위해 서버를 여러 대로 늘리면 응답 시간은 당연히 느려진다. 또한 병렬성을 증가시키기 위해 스레드를 늘리면 컨텍스트 체인지에 사용되는 시간이 늘어나 CPU 사용률이 떨어지고, 메모리 사용량도 불필요하게 늘어나게 된다.

 

이렇게 성능 최적화는 모든 지표가 개선되기는 힘들다. 하나가 좋아지면, 하나를 희생해야 할 수도 있다. 이것을 트레이드 오프라고 하고, 어떤 지표를 개선할 것인지 목표를 분명하게 해야 적절한 최적화 방법을 찾을 수 있다.

 

3줄 요약

  1. Java는 JVM 위에서 실행되기 때문에 약간의 성능을 희생하고 편리함을 얻었다
  2. JVM의 성능을 판단하는 지표들을 알아두자
  3. Silver Bullet은 없으니 목표를 확실하게 정하고 최적화를 하자