성능 최적화 8편 - APM으로 병목 구간 추적하기
지금까지는 쿼리, 캐싱, 락, 커넥션 풀까지 각 층위에서의 최적화 방법을 다뤘습니다. 하지만 실제 운영 환경에서는 "어디가 느린지"를 알아내는 것 자체가 먼저 해결해야 할 문제인 경우가 많습니다. 이번 글에서는 APM(Application Performance Monitoring) 도구를 활용해 실제 병목 구간을 추적하는 방법을 정리합니다.
1. 로그만으로는 한계가 있는 이유
특정 API가 느리다는 문의가 들어왔을 때, 로그만으로 원인을 찾으려면 다음과 같은 어려움이 있습니다.
- 하나의 요청이 컨트롤러 → 서비스 → 여러 레포지토리 → 외부 API 호출까지 이어지는데, 각 구간의 소요 시간을 일일이 로그로 남기지 않으면 어느 구간이 병목인지 알 수 없습니다.
- 요청이 동시에 여러 건 몰리면, 로그만으로는 어떤 로그가 어떤 요청에 속하는지 추적하기 어렵습니다.
- DB 쿼리, 외부 API 호출, GC(Garbage Collection) 등 애플리케이션 코드 바깥의 요인은 로그로 잘 드러나지 않습니다.
APM은 이런 정보를 요청 단위로 자동 수집해서, 트랜잭션 하나가 어디에서 시간을 얼마나 썼는지 시각적으로 보여줍니다.
2. APM이 보여주는 것 - 트랜잭션 분석
APM 도구(Pinpoint, Datadog APM, Scouter 등)를 붙이면, 하나의 요청이 처리되는 동안의 흐름을 아래와 같은 형태로 확인할 수 있습니다.
GET /api/orders/123 총 850ms
├─ OrderController.getOrder() 5ms
├─ OrderService.getOrderDetail() 820ms
│ ├─ OrderRepository.findById() 15ms
│ ├─ PaymentClient.getPaymentInfo() 780ms <- 병목 구간
│ └─ CouponService.getAppliedCoupon() 20ms
└─ Response 직렬화 5ms
전체 850ms 중 780ms가 외부 결제 API 호출에서 소요되고 있다는 것이 한눈에 드러납니다. 이 구간을 모르고 DB 쿼리만 튜닝했다면, 정작 병목은 그대로 남아있었을 것입니다.
3. Spring Boot에 APM 연동하기 (Pinpoint 예시)
Pinpoint는 Java Agent 방식으로 동작해서, 별도의 코드 수정 없이 애플리케이션 실행 시 옵션만 추가하면 됩니다.
java -javaagent:/pinpoint-agent/pinpoint-bootstrap-2.5.3.jar \
-Dpinpoint.agentId=order-service-01 \
-Dpinpoint.applicationName=order-service \
-jar order-service.jar
이렇게 실행하면 Pinpoint Agent가 애플리케이션의 메서드 호출, DB 쿼리, 외부 HTTP 호출을 자동으로 계측해서 수집 서버로 전송합니다.
4. 커스텀 구간 계측하기
자동 계측만으로 부족하다면, 특정 메서드나 로직 구간을 직접 계측 대상으로 지정할 수 있습니다.
@Trace
public OrderDetailDto getOrderDetail(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("주문을 찾을 수 없습니다."));
PaymentInfo paymentInfo = paymentClient.getPaymentInfo(order.getPaymentId());
CouponInfo couponInfo = couponService.getAppliedCoupon(order.getCouponId());
return OrderDetailDto.of(order, paymentInfo, couponInfo);
}
@Trace 같은 어노테이션(도구마다 이름은 다릅니다)을 붙이면, 해당 메서드의 실행 시간이 트랜잭션 분석 화면에 별도 구간으로 표시되어 세밀한 구간까지 추적할 수 있습니다.
5. 느린 트랜잭션만 골라서 보기
트래픽이 많은 서비스에서는 모든 요청을 다 들여다볼 수 없습니다. 대부분의 APM 도구는 응답 시간 임계값을 기준으로 느린 트랜잭션만 필터링해서 보여주는 기능을 제공합니다.
필터 조건: 응답시간 >= 1000ms
결과: 지난 1시간 동안 1초 이상 걸린 요청 47건
이렇게 느린 요청들만 모아서 보면, 특정 API나 특정 외부 연동에서 반복적으로 지연이 발생하는 패턴을 빠르게 파악할 수 있습니다.
6. 알림(Alert) 설정으로 선제 대응하기
병목을 사후에 발견하는 것보다, 임계치를 넘는 순간 알림을 받는 것이 운영에는 더 중요합니다.
alert:
rule: response_time_p99 > 2000ms
duration: 5m
channel: slack
응답 시간의 p99(상위 1% 느린 요청 기준)가 일정 시간 이상 임계값을 넘으면 Slack 등으로 알림을 보내도록 설정해두면, 장애로 이어지기 전에 대응할 수 있습니다.
7. APM 도입 시 고려할 점
- 오버헤드 : Agent 방식은 애플리케이션 성능에 약간의 오버헤드를 줍니다. 계측 범위를 필요한 만큼만 설정하는 것이 좋습니다.
- 데이터 보관 비용 : 트랜잭션 데이터가 쌓이면 저장 공간이 상당히 커질 수 있어, 보관 기간 정책을 함께 설정해야 합니다.
- 민감 정보 마스킹 : 요청/응답 본문을 수집하는 경우, 개인정보나 인증 토큰이 그대로 저장되지 않도록 마스킹 처리가 필요합니다.
8. 정리
- 로그만으로는 요청 하나가 어느 구간에서 시간을 쓰는지 파악하기 어렵고, APM은 이를 트랜잭션 단위로 시각화해준다
- 자동 계측으로 부족한 부분은 커스텀 어노테이션으로 세밀하게 추적할 수 있다
- 느린 트랜잭션 필터링과 알림 설정을 함께 활용하면, 병목을 사후 분석이 아니라 사전에 발견할 수 있다
지금까지 다룬 쿼리 최적화, 캐싱, 락, 커넥션 풀은 모두 "어디를 고쳐야 하는지 알고 있을 때" 적용하는 기법들이었습니다. APM은 그 "어디"를 찾아내는 도구라는 점에서, 성능 최적화 작업의 가장 앞단에 위치한다고 볼 수 있습니다.
'성능 최적화' 카테고리의 다른 글
| 성능 최적화 STEP 10 - JMeter로 부하 테스트하고 개선 효과 검증하기 (0) | 2026.07.08 |
|---|---|
| 성능 최적화 STEP 9 - @Async와 CompletableFuture로 비동기 처리하기 (0) | 2026.07.08 |
| 성능 최적화 STEP 7 - HikariCP 커넥션 풀 튜닝으로 DB 병목 줄이기 (0) | 2026.07.07 |
| 성능 최적화 STEP 6 - 분산 락(Distributed Lock)으로 동시성 문제 해결하기 (0) | 2026.07.06 |
| 성능 최적화 STEP 5 - 인덱스 설계와 튜닝으로 쿼리 속도 개선하기 (0) | 2026.07.06 |