먼저 "자바 최적화" 라는 책을 읽게 된 계기는 난 자바 백엔드 개발자라고 소개하지만, "자바" 언어 자체에 그렇게 딥하게 공부한 적이 없는 거 같다는 생각이 들었고, 프레임워크에 의존하지 않고, 언어 자체만으로 최적화를 해보고 싶었다.
들어가며
JIT와 GC 성능이 향상되면서 작은 메서드를 적절히 인라이닝하고 인터페이스 및 타입 체크는 저렴하게 처리하면서 JIT컴파일러가 만든 네이티브 코드는 간결하고 효율적으로 유지하는, "좋은" 코딩 패턴을 따를게 맞습니다.
그러나 경우에 따라 사람이 직접 코드를 작성하고 컴파일러 및 CPU 한계를 감안해 추상화 아키텍처를 재조정하애할 때도 있습니다.
모든 성능 문제는 한가지 정답이 있는게 아니라 여러개 정답이 있고 그 중 요건에 가장 알맞은 해결책을 조합하는 게 기술이다.
성능과 최적화
성능 분석은 경험 주의와 인간 심리학이 교묘히 어우러진 분야이다. 정작 중요한 건, 관측 지표의 절대 수치, 그리고 엔드 유저(최종 사용자, 실제로 시스템을 사용하는 사람)와 기타 이해 관계자들이 그 수치를 어떻게 맏아들이는가 하는 점이다. 결국 개발자는 문제를 해결해서 사용자가 만족하게끔 성능을 내면 되는것이다. 너무 완벽주의 성향에 이끌려서 오버엔지니어링으로 시간을 쓰는 것보다 "사용자가 이정도면 만족한다" 라고 하면 거기서 만족해도 되는것이라고 생각한다.
자바 초창기의 자바 디스패치 성능 최악 -> 최신 JVM에서는 자동 인라이닝 덕분에 가상 디스패치조차 대부분의 호출부(콜 사이트)에서 사라지게됨. 모든 코드를 한 메서드에 욱여넣어라는 현대 JIT 컴파일러와 어울리지 않는 퇴물 -> 책에서 고전 java 최적화에 대해 랭킹을 차지한 책을 읽고 최적화하였지만 오히려 성능이 떨어진.... 인터넷글만 읽고 따라하지 말고 무조건 검증 가능한 방식으로 성능을 다루어라
자바의 탄생
자바 성능의 본질을 이해하기 위해 자바 창시자인 제임스 고슬링의 한마디를 인용하고자 한다.
자바는 블루 칼라(주로 생산직에 종사하는 육체 노동자) 언어입니다. 박사 학위 논문 주제가 아니라 일을 하려고 만든 언어죠
자바는 성능에 초점을 둔 언어보다는 개발자의 생산성을 높이기 위해 만들어진 언어라고 한다. 성능을 높이려면 저수준 언어를 사용하면 훨씬 빠르다(C, C#, 어셈블리어...)
자바언어는 처음부터 실용적인 언어이다. 개발자 생산성이 높아지면 성능은 희생해도 된다. 자바 플랫폼의 성격을 보면 알 수 있다. (저수준으로 제어 가능하나 일부 기능을 포기) 그중 관리되는 서브시스템이 가장 대표적이다. 서브시스템은 개발자가 일일이 용량을 세세하기 관리하는 부담을 덜어주고, 대신 저수준으로 제어가능한 일부 기능을 포기하자는 발상이다.
JVM에서 메모리를 어떻게 관리하는 지 생각해보자. JVM이 탈착형 GC 서브시스템 형태로 메모리를 자동 관리하는 덕분에 개발자의 수고를 덜어준 것으로 볼 수 있다. 이렇게 서브시스템이 개발자 생산성을 높여주었다.(서브시스템은 그 존재 자체로 JVM 애플리케이션 런타임 동작에 복잡도를 유발)
런타임 동작이 복잡하니까 JVM을 항상 분석하고 측정하며 통계치를 내며 확인해볼 수 밖에 없다.
또한 JVM 애플리케이션의 성능 측정값은 정규 분포를 따르지 않는 경우가 많아서 기초 통계 기법만 갖고는 측정 결과를 제대로 처리하기에 역부족이다.측정 값을 샘플링(표본추출)하면 특이점(아웃라이너)을 일으킨 가징 중요한 이벤트를 놓칠 수 있다. 쉬운 예로, JVM 애플리케이션(예: 저지연 거래 애플리케이션) 에서 특이점(아웃라이어)은 매우 중요한 의미를 내포할 수 있다. 즉, 측정값을 샘플링(표본추출)하면 특이점을 일으킨 가장 중요한 이벤트가 묻혀버릴 가능성이 크다.
또한 환경이 복잡하니까 시스템을 개별적으로 따로 떼어내 생각하기 몹시 어렵다. -> 서로 영향을 주고 받는 시스템이 복잡하게 얽혀있으므로 이를 따로 떼어내고 분석하는건 실제 런타임에서 발생하는 환경과 다르므로 분석하는 의미가 없다고 생각
측정하는 행위 자체도 오버헤드(과부하)를 일으킨다고 한다. 모니터링툴, heap dump를 생성하는 것도 자원을 활용하여 모니터링하고, 로그를 생성하는 것이므로, 이런 행위자체가 성능 결과 수치에 적잖은 영향을 끼친다.
성능은 실험과학이다.
대부분 최신 소프트웨어 시스템이 그렇듯, 자바/JVM 소프트웨어 스택 역시 아주 복잡하다. 애플리케이션 런타임 환경에 맞춰 최적화하는 JVM을 기반으로 구축된 운영 시스템의 성능 양상은 상당히 미묘하고 복잡하게 나타날 수 있다. 이렇게 복잡한 지경까지 이른 건 무어의 법칙과 그로인한 하드웨어 용량의 전무후한 발전 때문일 것이다.
소프트웨어 산업의 가장 경이적인 성과는 하드웨어 산업에서 꾸준히 이루어낸 혁신을 끊임없이 무용지물로 만드로 있는 것이다.
- 헨리 페트로스키
JVM은 엔지니어링의 개가라고 하지 않을 수 없다. JVM도 다른 복잡한 고성능 시스템처럼 최상의 성능을 발휘하려면 어느 수준 이상의 스킬과 경험이 필요하다. JVM 성능 튜닝은 기술, 방법론, 정략적 측정값, 툴 을 모두 포함하는 개념이다. 그 목표는 시스템 소유자/유저가 추구하는 측정 결과를 얻는 것이다.
측 성능 튜닝 = 실험 과학(다음과 같은 단계를 거치면서 원하는 결과를 얻기 위한 실험과학)
1. 원하는 결과를 정의
2. 기존 시스템을 측정
3. 요건을 충족시키려면 무슨 일을 해야 할지 정한다.
4. 개선 활동을 추진
5. 다시 테스트
6. 목표가 달성됐는지 판단
무엇을 측정할지 대상을 확정하고 목표를 기록하는 행위가 중요하다. 이런 활동이 프로젝트 아티팩트(결과물)와 제품 일부를 형성한다.
성능 분석은 비기능 요건을 정의하고 달성하는 활동이다.
통계치에 근거해 적절히 결과를 처리하는 활동이다. 분석에 초점이 아니라 요구사항에 만족하는가가 우선이다.
성능 지표
성능 목표를 정의한 비기능 요건이다.
처리율
(서브)시스템이 수행 가능한 작업 비율을 나타낸 지표, 보통 일정 시간 동안 완료한 작업 단위 수로 표시(초당 처리 가능한 트랜잭션 수)처리율이 실제 성능을 반영하는 의미있는 지표가 되려면 수치를 얻은 기준 플랫폼에 대해서도 내용을 기술해야함.(하드웨어 스펙, os, 소프투웨어 스택, 테스트한 시스템이 단일 서버인지, 클러스터 환경인지)
지연
하나의 트랜잭션을 처리하고 그 결과를 반대편 수도관 끝에서 바라볼 때까지 소요된 시간. 종단 시간이라고도 함. 대개 그래프에서 *워크로드에 비례하는 함수로 표시함.
* 워크로드: 시스템이 주어진 시간 내에 처리해야할 작업 할당량
용량
시스템이 보유한 작업 병렬성의 총량, 즉 시스템이 동시 처리 가능한 작업 단위(트랜잭션) 개수
사용률
성능 분석 업무 중 가장 흔택 태스크는 시스템 리소스를 효율적으로 활용하는 거다. 가령 CPU라는 리소스는 놀리는(또는 OS에 시간을 빼앗기거나 다른 관리 태스크를 수행하는) 것보다 실제 작업 단위를 처리하는 데 쓰여야 온당하다.
사용률은 워크로드에 따라서 리소스 별로 들쑥날쑥할 수 있다. 계산 집약적인 워크로드(예: 그래픽 처리, 암호화)주면 CPU 사용률은 100%에 육박하겠지만, 메모리 사용률은 얼마 안 나온다.
리소스(CPU 등)를 얼마나 효율적으로 활용하는가?
효율
(처리율 / 리소스 사용율)
전체 시승템의 효율은 처이율을 리소스 사용률로 나눈 값으로 측정한다. 같은 처리율을 더 많은 리소스를 쏟아부어야 달성할 수 있다면 효율이 낮은거다.
대형 시스템에서는 원가 회계 형태로 효율을 측정하는 방법도 있다. 처리율이 동일하다는 조건하에 A 솔류션의 총 소유비용(TCO)이 B 솔루션의 2배라면 말할것도 없이 효율은 절반밖에 안되는 셈이다.
확장성
리소스 추가에 따른 처리율 번화는 시스템/애플리케이션의 확장성을 가늠하는 척도. 시스템 확장성은 궁극적으로는 정확히 리소스를 투입한 만큼 처리율이 변경되는 형태를 지향
저하
시스템이 더 많은 부하를 받으면 지연, 처리율 측정값에 변화가 생긴다. 어느정도 부하가 되었을때 처리율이 올라가지 않으면 지연이 증가하는 양상을 띠고 이런 현상을 부하 증가에 따른 저하라고 한다.
측정값 사이의 연관관계를 잘 생각해보자
위에서 설명한 성능 지표들은 서로 연결되어 있다. 구체적인 상호관계는 시스템이 풀 가동 중인지 여부에 따라 달라진다. 예를 들어서, 일반적으로 시스템 부하가 증가하면 사용률도 달라지지만, 시스템을 많이 사용하지 않은 시간에는 부하가 늘어도 사용률은 별로 눈에 띄게 증가하지 않을 수도 있다. 반대로 이미 부하가 걸려있는 상태면 부하가 조금만 늘어도 다른 측정값이 크게 요동칠 수 있다.
확장성과 저하 이 둘을 지표로하는 그래프가 있다고해보면, 확장성과 저하는 부하가 증가함에 따라 시스템 양상이 어떻게 봐뀌는지 반영하는 지표이다. 확장성을 감안하면 부하가 늘 때 가용 리소스도 함께 늘려야하는데, 시스템이 이렇게 확장한 리소스를 제대로 활용할 수 있을지가 관건이다. 한편 부하가 늘었는데 리소스는 그대로라면 마땅히 저하되는 성능 측정값(즉, 지연)이 있을 것이다.
-> 핫스팟 JIT 컴파일러일러도 좋은 예이다.(부하가 많아 메서드 호출 빈도가 증가하면 JIT 컴파일러 대상이 되고 결국 같은 메서드지만 나중에 호출하는게 처음보다 빨리 실행) JIT 컴파일 대상이 되는 메서드는 충분히 빈번하게 인터프리터 모드로 실행되어야한다. 그래서 부하가 적을 경우 핵심 메서드가 인터프리터로 인해 느린데, 부하가 많아 메서드 호출 빈도가 높아지면 JIT 컴파일 대상이되어 결국 같은 메서드지만 나중에 호출하는게 처음보다 훨씬 빠르게 실행된다.
위 지표가 절대적인 것은 아니고 실무에서 맞닥뜨릴 성능 측정값을 적어놓은것이다. 대부분 시스템 성능 튜닝을 가이드할 때 쓰이는 용도의 지표이고, 실제로 특정 도메인, 관심있는 시스템 성능을 논할 때 체계적인 분류 기준으로 활용된다.
성능 그래프 읽기
성능 테스트에서 자주 등장하는 패턴을 소개하겠다.
성능 엘보 그래프
성능 엘보 그래프는 부하가 증가하면서 예기치 않게 저하(지연)가 발생한 그래프이다.
이와 반대로 준-선형적 확장 그래프는 클러스터에 장비를 추가함에 다라 거의 선형적으로 처리율이 확장되는 운이 좋은 케이스이다.(아주 이상적인 그래프). 이런 이상적인 모습에 가까운 결과는 환경이 극단적으로 순조로울때(서버 하나에 *세션 어피니티(세션 고정)가 필요없는, *무상태 프로토콜을 확장하는 경우)나 가능
* 세션 어피니티: 로드 밸런서가 사용자 세션을 특정 서버에 고정되도록 바인딩하는 기술
*무상태 프로토콜: 어떤한 이전 요청과도 무관한 각각의 요청을 독립적인 트랜잭션으로 취급하는 통신 프로토콜, 통신이 독립적인 쌍의 요청과 응답을 이룰 수 있게 하는 방식, 무상태 프로토콜은 서버가 복수의 요청 시간대에 각각의 통신 파트너에 대한 세션 정보나 상태 보관을 요구하지 않는다. <-> 상태 프로토콜
암달의 법칙
암달에 따르면 확장성에 제약이 따른다. 태스크를 처리할 때 프로세스 개수를 늘려도 실행 속도를 최대 어느 정도까지 높일 수 있는지를 나타낸 그래프이다.
-> 워크로드에 순차 실행해야하는 작업이 하나라도 있으면 선형 확장은 처음부터 불가하나(그래프에서 병렬화를 기본 전제로 깔고 있음)
-> 최선의 경우라도 선형 확장을 불가능한 미션이라는 말
암달의 법칙에 따르면 의외로 제한이 많음.
순차 비율이(겨우) 5%인 알고리즘도 12배 시간을 단축하려면 32개의 프로세서가 필요... 코어를 늘린다해도 20배 이상 시간 단축은 어려움. 실제로 순차 비율이 5%보다 훨씬 높은 알고리즘이 태반이라 최대 속도 향상은 더욱 제약을 받는다.
실제 예시로 보는 메모리 할당률
아래 사진은 피보나치 수열 애플리케이션 실행시 떨어지는 메모리 할당률
피보나치 수열을 객체를 사용해서 구현하면 피보나치 함수가 재귀적으로 발생하면서 힙에 객체가 엄청나게 쌓이고 STW가 발생한다. -> 이때 힙 영역을 비워야하므로 JVM은 모든 객체 생성을 중단하는 현상이다.
여기서 가비지 컬렉터를 수행하는 여러 멀티 스레드가 수많은 컨텍스트 스위칭과 CPU를 점유하려 경쟁하게 되고, STW가 길어지면 객체 생성이 중단되니까 메모리 사용량이 급격하게 줄어드는 것이다.
피보나치 코드가 실행되면서 4GB/s라는 미친 속도로 메모리를 할당하고 있다. -> 재귀적으로 엄청난 속도로 많은 양의 객체를 만들어내니까
부하가 높을때 지연과 트랜잭션 수의 관계
시스템 리소스가 누수될 때 흔히 나타나는 징후이다. 부하가 증가하면서 지표(지연)가 차츰 악화되다고 시스템 성능이 급락하는 변곡점에 이르게 된다.
다음 장은 JVM의 주요 파트를 보면서 JVM 기반의 성능 최적화가 왜 그렇게 복잡한지 알아보겠다.
'언어 > JAVA' 카테고리의 다른 글
[자바 최적화] 2장 JVM 이야기 (3) | 2025.08.02 |
---|---|
자바에서 비동기 처리로 성능 개선하기 - CompletableFuture (8) | 2025.07.30 |
[Java] Java Collection Framework (JCF) (1) | 2025.03.23 |
[JAVA] EOF 사용법 (0) | 2024.04.10 |
Optional 올바르게 사용하자 (0) | 2024.03.30 |