디버깅과 성능 개선 순서
오류와 성능 문제는 코드를 무작정 바꾸기 전에 재현 조건과 관찰 지점을 고정해야 합니다.
디버거는 중단점, 조건부 중단점, 스택 프레임, 지역 변수로 실행 상태를 보여 줍니다.
성능 문제는 wall-clock 시간, 처리량, CPU, 할당, 대기 가운데 어떤 지표가 나쁜지 구분합니다.
마이크로벤치마크는 JIT 워밍업과 최적화 제거를 고려해야 하므로 JMH 같은 도구를 사용합니다.
이 절에서는 한 가설씩 수정하고 같은 조건으로 다시 측정합니다.
기능 오류와 성능 저하는 모두 관찰 가능한 계약에서 출발합니다. “느리다” 대신 입력 규모, 동시 사용자 수, p95 지연과 같은 조건을 적고, “가끔 틀린다” 대신 재현 입력과 기대 출력을 남깁니다. 측정 조건이 없으면 개선 전후도 비교할 수 없습니다.
최소 재현 사례가 조사 범위를 줄인다
입력, 환경, 기대 결과, 실제 결과를 고정하고 불필요한 코드와 외부 의존성을 제거합니다.
중단점은 상태가 갈라지는 경계에 둔다
오류가 드러난 마지막 줄보다 잘못된 값이 처음 만들어지는 분기와 대입을 찾습니다.
호출 스택을 거꾸로 따라갑니다.
MeasuredFix.java는 지금 설명한 규칙을 실제 값의 변화로 보여 줍니다.
import java.util.List;
public class MeasuredFix {
static int find(List<Integer> sorted, int target) {
int low = 0;
int high = sorted.size() - 1;
while (low <= high) {
int middle = low + (high - low) / 2;
int value = sorted.get(middle);
if (value == target) return middle;
if (value < target) low = middle + 1;
else high = middle - 1;
}
return -1;
}
public static void main(String[] args) {
List<Integer> values = List.of(2, 4, 6, 8, 10);
for (int target : List.of(2, 10, 7)) {
System.out.println(target + " -> " + find(values, target));
}
}
}첫 원소와 마지막 원소는 low <= high 경계 양쪽을 검증하고, 없는 7은 탐색 구간이 완전히 비워질 때 -1로 끝나는지를 확인합니다. 가운데 있는 값 하나만 시험했다면 경계 오류를 놓칠 수 있습니다.
2 -> 0
10 -> 4
7 -> -1첫 원소, 마지막 원소, 없는 중간값을 실행해 경계와 종료 조건을 함께 확인합니다.
입력, 환경, 기대 결과, 실제 결과를 고정하고 불필요한 코드와 외부 의존성을 제거합니다.
오류가 드러난 마지막 줄보다 잘못된 값이 처음 만들어지는 분기와 대입을 찾습니다.
응답 지연인지 전체 처리량인지에 따라 개선 방향이 다릅니다.
성능 지표를 사용자 증상과 연결한다
응답 지연인지 전체 처리량인지에 따라 개선 방향이 다릅니다.
평균만 보지 않고 백분위와 부하 조건을 기록합니다.
디버거와 시간 측정은 서로 다른 질문에 답합니다. 아래 도구를 쓰기 전에 먼저 어떤 가설을 확인할지 정합니다.
// 조건부 중단점 후보: value == target 또는 low > high
// Watch: low, high, middle, valuelong started = System.nanoTime();
int result = find(values, target);
long elapsed = System.nanoTime() - started;// 한 번의 nanoTime 측정은 JIT 워밍업·잡음을 통제하지 못함
// 마이크로벤치마크는 JMH 사용
// 실제 병목은 JFR/프로파일러로 먼저 확인프로파일로 뜨거운 경로를 확인한다
CPU 샘플, 할당 프로파일, 락 대기에서 실제 비용이 큰 경로를 찾고 읽기 쉬운 코드부터 함부로 복잡하게 만들지 않습니다.
재현 입력과 기대 결과를 먼저 고정한다
중단점을 찍기 전에 어떤 입력에서 무엇이 잘못됐는지 최소 사례를 만듭니다.
수정 뒤 같은 입력으로 결과를 비교합니다.
프로파일 결과와 코드 변경을 연결한다
한 번에 한 병목 가설만 수정하고 같은 조건으로 다시 측정합니다.
여러 최적화를 섞으면 어떤 변경이 효과를 냈는지 알 수 없습니다.
입력을 빈 목록, 원소 하나인 목록, 중복 값이 있는 목록으로 바꾸고 기대 인덱스를 먼저 적어 보세요. 이어서 정렬되지 않은 List.of(10, 2, 8)을 넣으면 메서드의 숨은 전제 때문에 결과를 신뢰할 수 없습니다. 알고리즘 버그와 호출 계약 위반을 구분하려면 sorted라는 전제도 재현 기록에 포함해야 합니다.
System.nanoTime 한 번으로 두 구현을 비교하면 클래스 로딩, JIT 워밍업, GC, OS 스케줄링 잡음이 결과를 지배할 수 있습니다. 작은 코드의 처리량을 비교해야 할 때는 JMH를 사용하고, 실제 요청 지연 문제라면 먼저 JFR과 애플리케이션 지표로 병목 위치를 좁힙니다.
최적화가 가독성을 크게 해치면 되돌릴 기준도 필요합니다. 같은 부하에서 목표 지표가 의미 있게 개선됐는지, 다른 백분위나 메모리 사용이 악화되지 않았는지 확인한 뒤 변경을 유지합니다.
디버깅과 최적화는 재현, 관찰, 한 가지 가설, 최소 변경, 재측정이라는 같은 과학적 순서를 따릅니다.