개발 기술/사소하지만 놓치기 쉬운 개발 지식

유의미한 성능 개선은 어디에서 시작할까? Hot Path 이해하기

by GicoMomg 2026. 8. 11.

0. 들어가며…

성능을 개선하기 위해 코드를 살펴보다 보면, 코드 곳곳의 작은 비효율이 눈에 들어온다.
불필요한 반복을 조기에 종료하거나, 중복 계산을 제거하거나, 서로 독립적인 비동기 작업을 병렬로 처리하는 등의 개선 방법을 떠올릴 수 있다.


하지만 코드 수준에서 개선할 수 있다고 해서, 유의미한 성능 개선이라고 볼 수 있을까?

코드 수준의 최적화도 필요하지만, 코드의 형태만으로 개선 가치를 판단해서는 안 된다.

지난달부터 ESLint의 내부 구조를 살펴보던 중, 이와 비슷한 고민을 한 적이 있다.
계기가 된 것은 ESLint 규칙에서 map()과 filter()를 연속으로 사용해 두 개의 중간 배열을 생성하는 로직이었다.

당시에는 해당 코드만 보고, 두 메서드를 하나의 every()로 합치면 불필요한 중간 배열과 반복 횟수를 줄여 성능을 개선할 수 있다고 생각했다.

/**
 * eslint > multiline-comment-style.js
 * 
 * Q. map()과 filter()가 각각 중간 배열을 생성하는데,
 * every()만 사용해 하나의 순회로 합칠 수는 없을까?
 */
function processSeparateLineComments(commentGroup) {
    const allLinesHaveLeadingSpace = commentGroup
        .map(({ value }) => value)              // 중간 배열 생성 1
        .filter(line => line.trim().length > 0) // 중간 배열 생성 2
        .every(line => line.startsWith(" "));

    return commentGroup.map(({ value }) =>
        allLinesHaveLeadingSpace ? value.replace(/^ /u, "") : value,
    );
}

하지만 메인테이너로부터 해당 변경은 유의미한 성능 개선으로 보기 어렵다는 피드백을 받았다.
이 로직은 실행 횟수가 적고 입력 데이터의 크기도 작아, 전체 성능에 미치는 영향이 크지 않았기 때문이다.


그렇다면 코드의 형태만 보고 판단하는 데서 벗어나, 실제로 개선할 가치가 있는 지점은 어떻게 찾을 수 있을까?


이 질문에 대한 답을 찾아가는 과정에서 알게 된 개념이 바로 Hot path다. 이번 시간에는 Hot path가 무엇인지 살펴보고,
이를 실제 성능 개선 지점을 찾는 데 어떻게 활용할 수 있는지 차근차근 알아보겠다.



1. Hot path: 실행 비용이 누적되는 코드

1-1. hot path란 무엇인가

Hot path는 프로그램의 전체 실행 시간이나 처리 비용에서 큰 비중을 차지하는 실행 흐름이다.
여기서 path는 하나의 함수만 의미하지 않는다. 하나의 작업이 완료되기까지 거치는 여러 단계도 포함한다.

입력 → 계산 → 상태 변경 → 렌더링

Hot path가 되는 경우는 크게 두 가지다.

  • 한 번 실행하는 데 오래 걸리는 경우
  • 한 번의 실행은 빠르지만 자주 반복되어 누적 비용이 커지는 경우

예를 들어 다음 함수가 한 번 실행될 때 0.1ms가 걸린다고 가정해 보자.

formatcurrency(item.amount);

한 번의 실행 비용은 작지만, 데이터 10,000개 기준으로 반복하면 누적 실행 시간(0.1ms x 10,000번)은 약 1s가 된다.

items.map(item => formatCurrency(item.amount));

반대로 다음 함수는 데이터가 많아 한 번 실행시 1초가 걸릴 수 있다.

calculateComplexChartData(veryLargeDataSet);

첫 번째 사례는 짧은 작업이 반복되면서 누적 비용이 커지고, 두 번째 사례는 한 번의 작업만으로 큰 비용이 발생한다.
형태는 다르지만, 전체 실행 시간에서 큰 비중을 차지한다면 두 경우 모두 Hot path가 될 수 있다.

전체 비용 ≈ 한 번의 실행 비용 × 실행 횟수

즉, Hot path를 찾으려면 개별 작업의 실행 비용과 실행 횟수를 함께 살펴봐야 한다.
또한 여기서 말하는 ‘비용’은 실행 시간에만 한정되지 않는다.


Hot path는 주로 CPU 실행 시간을 기준으로 찾지만, 상황에 따라 메모리 할당, 디스크 I/O, 네트워크 처리 등 다른 비용을 기준으로 판단할 수도 있다.

Hot path는 한 번의 실행 비용이 크거나 작은 비용이 반복적으로 누적되어,
프로그램의 전체 처리 비용에서 큰 비중을 차지하는 실행 흐름이다.


1-2. Hot path는 실행 상황에 따라 달라진다

특정 코드가 항상 Hot path인 것은 아니다.
사용자가 수행하는 동작과 측정하려는 비용에 따라 Hot path는 달라질 수 있다.

따라서 Hot path를 찾기 전에 다음 세 가지 기준을 먼저 정해야 한다.

  1. 측정 기준: CPU 실행 시간, 메모리 사용량, 네트워크 요청 등 어떤 비용을 측정할 것인가?
  2. 사용자 동작: 검색어 입력, 목록 스크롤, 페이지 이동 등 어떤 동작을 측정할 것인가?
  3. 측정 구간: 한 번의 요청, 일정 시간 동안의 상호작용, 애플리케이션의 전체 실행 과정 중 어디까지를 측정 범위로 삼을 것인가?

예를 들어 사용자가 검색어를 입력할 때마다 자동 완성 로직이 실행된다고 가정해 보자.

function onSearchInput(keyword: string) {
    const matches = products
        .filter(product => product.name.includes(keyword))
        .sort(compareByRelevance);

    renderInputText(matches);
}

사용자가 검색어를 연속으로 입력하면 filter, sort, renderInputText가 반복해서 실행된다.
따라서 이 흐름이 Hot path일 가능성은 있지만, 반복해서 실행된다는 사실만으로 단정할 수는 없다.
전체 처리 비용에서 실제로 얼마나 큰 비중을 차지하는지 확인해야 하기 때문이다.


이를 판단하려면 먼저 측정 조건을 구체적으로 정해야 한다.

구분 내용
측정 기준 메인 스레드의 CPU 실행 시간
사용자 동작 상품 10,000개를 불러온 상태에서 검색어를 연속으로 입력
측정 구간 검색어 입력부터 filter → sort → renderInputText 실행이 완료될 때까지

이처럼 측정 조건을 구체적으로 정하면 다음 내용을 명확하게 확인할 수 있다.

  • 어떤 사용자 동작을 측정했는지
  • 어떤 코드가 반복해서 실행되었는지
  • 어떤 비용을 측정 기준으로 삼았는지
  • 해당 코드가 전체 비용에서 얼마나 큰 비중을 차지하는지

즉, 코드가 자주 실행된다는 이유만으로 Hot path라고 판단해서는 안 된다.
측정할 상황과 기준을 먼저 정한 뒤, 해당 코드가 전체 처리 비용에서 차지하는 비중을 확인해야 한다.


1-3. Hot path를 판단하는 방법

(1) 사용자 관점의 목표를 정한다

먼저 어떤 사용자 경험을 개선할 것인지 구체적으로 정해야 한다.
단순히 ‘화면을 빠르게 만든다’는 목표만으로는 어떤 동작과 실행 흐름을 측정해야 하는지 판단하기 어렵다.


따라서 측정할 사용자 동작과 구간, 실행 환경, 목표 시간을 함께 정해야 한다.

구분 내용
실행 환경 전체 사용자의 40% 이상이 이용하는 모바일 환경
사용자 동작 상품 목록에서 필터 버튼 클릭
측정 구간 필터 버튼을 누른 시점부터 변경된 목록이 화면에 표시될 때까지
목표 시간 300ms 이하

이처럼 사용자 관점의 목표를 구체적으로 정하면, 어떤 환경에서 어떤 동작과 실행 흐름을 측정해야 하는지 명확해진다.


(2) 실제 사용자와 비슷한 조건에서 측정한다

개발자의 고성능 기기에서만 측정하면 실제 사용자가 겪는 병목을 놓칠 수 있다.
따라서 실제 사용 환경을 대표할 수 있는 조건에서 사용자 동작을 재현해야 한다.

  • 사용자가 주로 이용하는 기기와 브라우저
  • 실제와 비슷한 네트워크 속도
  • 현실적인 데이터 크기
  • 최초 진입과 재방문
  • 캐시가 있는 경우와 없는 경우

로컬에서 사용자 환경을 재현할 때는 다음과 같은 도구를 활용할 수 있다.

도구 주요 용도
Chrome DevTools CPU·네트워크 속도를 제한한 수동 측정
Puppeteer Chrome 기반 사용자 동작의 반복 실행과 Trace 수집
Playwright 여러 브라우저와 모바일 환경에서 반복 테스트
Lighthouse 페이지 초기 로딩과 특정 사용자 흐름의 성능 분석

Chrome DevTools에서는 CPU와 네트워크 속도를 제한해 중·저사양 모바일 환경을 간단히 재현할 수 있다.
특히 CPU throttling 보정 기능을 사용하면 현재 개발 기기를 기준으로 중간 등급과 저사양 모바일 환경을 추정할 수 있다.


동일한 동작을 여러 번 반복해서 측정하려면 Puppeteer를 활용할 수 있다.
Puppeteer는 브라우저를 자동으로 제어할 수 있으며, Tracing API를 이용해 Chrome DevTools에서 분석할 수 있는 Trace 파일도 생성할 수 있다.


여러 브라우저에서 같은 동작을 확인해야 한다면 Playwright를 사용할 수 있다.
Playwright는 Chromium, Firefox, WebKit을 지원하며, 화면 크기·User Agent·터치 여부 등을 포함한 모바일 기기 환경을 에뮬레이션할 수 있다.


다만 에뮬레이션은 브라우저와 화면 조건을 유사하게 재현할 뿐, 실제 기기의 CPU·GPU 성능과 발열, 메모리 제약까지 완전히 재현하지는 못한다.
따라서 서비스에서 중요하게 다루는 사용자 흐름은 실제 점유율이 높은 기기에서도 확인해야 한다.


(3) 프로파일링에서 실행 경로와 비용을 확인한다

Chrome DevTools의 Performance 패널을 사용하면 메인 스레드에서 오래 걸린 작업과 함수의 호출 관계를 확인할 수 있다.

<측정하기>

Performance 패널 열기
→ CPU·네트워크 조건 설정
→ Record 시작
→ 측정할 사용자 동작 재현
→ Record 종료

<결과 확인하기>

Main 트랙에서 오래 걸린 작업 선택
→ Total time과 Self time 확인
→ 호출 스택을 따라 실행 경로 확인
  • Total time: 하위 함수의 실행 시간을 포함한 전체 실행 시간
  • Self time: 하위 함수의 실행 시간을 제외하고, 해당 함수 자체에서 사용한 시간

여기서 주의할 점은 특정 함수의 실행 시간이 길다는 이유만으로 최적화 대상으로 판단해서는 안 된다.
해당 함수가 어떤 사용자 동작에서 호출되었는지, 그리고 측정 구간의 전체 비용에서 얼마나 큰 비중을 차지하는지 함께 확인해야 한다.


한 번의 측정 결과만으로 판단하면 안된다.
그 이유는 백그라운드 프로세스, 네트워크 상태, 캐시, JIT 컴파일 등의 영향으로 측정 결과가 달라질 수 있기 때문이다.


따라서 다음 조건을 동일하게 유지한 상태에서 같은 동작을 여러 번 측정해야 한다.

동일한 기기 또는 CPU 제한 조건
동일한 브라우저와 버전
동일한 네트워크 조건
동일한 데이터 크기
동일한 캐시 상태
동일한 사용자 동작

여러 번 측정한 결과는 한 번의 최솟값이나 최댓값이 아니라 중앙값을 중심으로 비교하는 것이 좋다.

1회: 280ms
2회: 310ms
3회: 295ms
4회: 470ms
5회: 300ms

중앙값: 300ms

(4) 중요한 구간을 나누어 측정한다

User Timing API를 사용하면 특정 작업의 시작과 종료 시점을 기록하고, 각 구간에 걸린 시간을 비교할 수 있다.

async function loadTransferHistory() {
    performance.mark("history:start");

    const response = await fetch("/api/transfers");
    const rawData = await response.json();

    performance.mark("history:load-end");

    const transfers = normalizeTransfers(rawData);

    performance.mark("history:normalize-end");

    renderTransferHistory(transfers);

    performance.mark("history:render-end");

    performance.measure(
        "history:load",
        "history:start",
        "history:load-end",
    );

    performance.measure(
        "history:normalize",
        "history:load-end",
        "history:normalize-end",
    );

    performance.measure(
        "history:render",
        "history:normalize-end",
        "history:render-end",
    );
}

위 코드는 전체 작업을 세 구간으로 나누어 측정한다.

측정 구간 포함된 작업
history:load API 요청부터 응답 본문의 JSON 변환까지
history:normalize 응답 데이터를 화면에서 사용할 형태로 변환
history:render renderTransferHistory() 함수 실행

측정 결과를 비교하면 load, normalize, render 중 어느 구간이 전체 처리 시간에서 가장 큰 비중을 차지하는지 확인할 수 있다.


다만 위 코드의 history:render는 renderTransferHistory() 함수를 호출하는 데 걸린 시간만 측정한다.
React나 Vue에서 상태를 변경하는 함수라면, 프레임워크가 DOM을 업데이트하고 브라우저가 실제 화면을 그리는 시간까지 포함하지 않을 수 있다.


만약 목표가 ‘변경된 목록이 사용자 화면에 표시될 때까지’라면 프레임워크의 DOM 반영 완료 시점과
브라우저가 화면을 그리는 시점을 고려해 측정 종료 지점을 별도로 설정해야 한다.



2. Amdahl의 법칙으로 최적화 우선순위 정하기

Hot path를 찾았다고 해서 해당 실행 흐름 전체를 무조건 수정해야 하는 건 아니다.
측정 결과를 바탕으로, 실제 성능 개선 효과가 큰 부분을 선택해야 한다.


최적화 대상을 정할 때는 다음 항목을 확인한다.

  • 목표로 정한 성능 지표에 영향을 주는가?
  • 전체 실행 시간에서 차지하는 비중이 큰가?
  • 비용을 줄일 현실적인 방법이 있는가?
  • 변경으로 코드 복잡도나 메모리 사용량이 지나치게 늘어나지 않는가?
  • 기존 기능의 정확성을 유지할 수 있는가?

특정 부분의 개선이 전체 성능에 얼마나 영향을 주는지는 Amdahl의 법칙으로 설명할 수 있다.

  • P: 개선 대상이 기존 전체 실행 시간에서 차지하는 비율
  • S: 개선 대상 자체가 빨라진 배수
  • S_{total}: 전체 프로그램이 빨라진 배수

공식보다 중요한 핵심은
“전체 실행 시간에서 차지하는 비중이 작다면,
해당 부분을 크게 개선해도 전체 성능은 조금만 좋아진다”는 것이다.


예를 들어 전체 실행 시간이 10초이고, 개선 대상이 그 중 2초를 차지한다고 가정해 보자.

개선 전

개선 대상: 2초
나머지 작업: 8초
전체: 10초

개선 대상을 10배 빠르게 만들면 해당 작업의 실행 시간은 2초에서 0.2초로 줄어든다.

개선 후

개선 대상: 0.2초
나머지 작업: 8초
전체: 8.2초

개선 대상 자체는 10배 빨라졌지만, 전체 실행 시간은 10초에서 8.2초로 줄어든다.

전체 성능 향상

10초 ÷ 8.2초 ≈ 1.22배

즉, 일부 코드를 10배 빠르게 만들더라도 전체 프로그램은 약 1.22배만 빨라진다.
개선하지 않은 나머지 8초가 그대로 남아 있기 때문이다.


따라서 최적화 우선순위를 정할 때는 다음 두 가지를 함께 살펴봐야 한다.

판단 기준 확인할 내용
부분 개선 폭 해당 코드를 현실적으로 몇 배 빠르게 만들 수 있는가?
전체 시간 비중 해당 코드가 전체 실행 시간에서 얼마나 큰 비중을 차지하는가?

단순히 크게 개선할 수 있는 코드보다, 전체 실행 시간에서 큰 비중을 차지하면서 현실적으로 개선할 수 있는 코드를 먼저 선택해야 한다.



3. Hot path를 판단할 때 자주 생기는 오해

Hot path를 찾고 최적화 우선순위를 정하는 과정에서는 코드의 호출 횟수나 개별 함수의 실행 시간처럼 눈에 잘 띄는 수치만으로 판단하기 쉽다.
하지만 하나의 수치만으로 Hot path와 최적화 대상을 결정하면 실제 사용자 경험과 관계없는 부분을 개선할 수 있다.


앞에서 살펴본 판단 기준을 바탕으로, Hot path를 찾고 최적화할 때 자주 생기는 오해를 정리해 보자.

오해 1. 자주 호출되는 코드는 모두 Hot path다?

호출 횟수만으로는 Hot path인지 판단할 수 없다.
자주 실행되더라도 한 번의 실행 비용이 매우 작다면 전체 성능에 미치는 영향도 작을 수 있다.

중요한 것은 호출 횟수 자체가 아니라, 반복 실행으로 발생한 누적 비용이 전체 비용에서 차지하는 비중이다.

오해 2. 가장 오래 걸린 함수가 Hot path다?

Hot path는 하나의 함수가 아니라, 여러 함수가 이어지는 실행 흐름일 수 있다.
한 번 실행하는 데 오래 걸리는 함수보다 짧은 함수들이 반복되는 흐름의 누적 비용이 더 클 수도 있다.

따라서 개별 함수의 실행 시간뿐만 아니라, 해당 함수가 어떤 흐름에서 얼마나 자주 호출되는지도 함께 확인해야 한다.

오해 3. CPU 사용량이 가장 큰 경로부터 개선해야 한다?

사용자가 기다리는 원인이 네트워크 요청이나 디스크 I/O와 같은 대기 작업이라면 CPU 사용량을 줄여도 목표로 정한 성능 지표는 크게 개선되지 않을 수 있다.

먼저 어떤 사용자 경험과 비용을 개선하려는지 정한 뒤, 그 목표에 영향을 주는 실행 흐름을 찾아야 한다.

오해 4. Hot path는 무조건 최적화해야 한다?

Hot path라고 해서 반드시 수정해야 하는 것은 아니다. 전체 비용에서 차지하는 비중뿐만 아니라 개선 가능성, 구현 복잡도, 메모리 사용량, 변경 위험도 등을 함께 고려해야 한다.

최적화로 얻는 효과보다 구현과 유지보수에 드는 비용이 크다면 현재 상태를 유지하는 편이 나을 수 있다.

오해 5. 일부 코드가 10배 빨라지면 전체 성능도 10배 향상된다?

전체 성능의 향상 폭은 해당 코드가 기존 실행 시간에서 차지했던 비중에 따라 달라진다. 일부 코드가 10배 빨라지더라도 나머지 작업의 실행 시간이 그대로라면 전체 성능은 그보다 적게 향상된다.

또한 개발 환경에서 확인한 개선이 실제 사용자 환경에서도 동일하게 나타나는지 같은 조건에서 다시 측정해야 한다.



4. 마치며…

처음에는 중간 배열을 줄이거나 더 빠른 문법으로 바꾸는 것만으로 성능을 개선할 수 있다고 생각했다.
하지만 코드 수준에서 최적화할 수 있다는 사실만으로, 해당 코드가 실제 성능 문제라고 판단할 수는 없었다.


중요한 것은 먼저 개선하려는 사용자 경험과 측정 조건을 구체적으로 정하는 것이다.
그다음 프로파일링을 통해 전체 비용에서 큰 비중을 차지하는 실행 흐름을 찾고, 개선 가능성과 구현 비용,
변경에 따른 위험을 함께 고려해 최적화 우선순위를 정해야 한다.

사용자 관점의 목표 설정
→ 측정 조건 정의
→ 프로파일링
→ Hot path 확인
→ 개선 대상 선정
→ 동일한 조건에서 재측정

결국 성능 최적화는 단순히 느려 보이는 코드를 더 빠른 코드로 바꾸는 작업이 아니다.
측정 결과를 바탕으로 사용자 경험에 가장 큰 영향을 주는 지점을 찾고, 한정된 개발 비용을 가장 효과적인 곳에 투자하는 과정이다.


반응형

댓글