본문으로 건너뛰기

APM — Java 애플리케이션 성능 모니터링

프로덕션 환경에서 "왜 느린가?"라는 질문에 답하기 위해 APM(Application Performance Monitoring)이 필요합니다. APM은 단순 로그나 서버 메트릭을 넘어, 개별 HTTP 요청이 어떤 서비스를 거쳐 얼마나 걸렸는지, 어디서 병목이 발생했는지를 추적합니다. 이 챕터에서는 Elastic APM과 Pinpoint를 중심으로 Spring Boot 애플리케이션의 성능을 모니터링하는 방법을 실습합니다.

APM이란 무엇인가

APM은 애플리케이션의 성능과 안정성을 실시간으로 측정하는 도구입니다. 핵심 기능은 세 가지입니다.

분산 트레이싱(Distributed Tracing) 은 사용자의 한 번 요청이 여러 마이크로서비스를 통과할 때 전체 흐름을 하나의 트레이스(Trace)로 연결해서 보여줍니다. "API Gateway → User Service → Order Service → DB" 경로에서 어느 구간에 200ms가 소요됐는지 즉시 파악할 수 있습니다.

슬로우 쿼리 탐지(Slow Query Detection) 는 DB 쿼리, 외부 API 호출, 캐시 미스 등 응답 시간이 임계값을 초과하는 작업을 자동으로 감지해 알려줍니다.

에러 추적(Error Tracking) 은 예외 발생 시 전체 스택 트레이스와 함께 "어떤 사용자가, 어떤 요청을 보냈을 때 발생했는지"를 맥락(context)과 함께 기록합니다.

사용자 요청

├── Span 1: Nginx (2ms)
├── Span 2: Spring Boot Controller (15ms)
│ ├── Span 3: Service Layer (12ms)
│ │ ├── Span 4: JPA Query (8ms) ← 슬로우 쿼리
│ │ └── Span 5: Redis Cache (1ms)
│ └── Span 6: External HTTP Call (3ms)
└── Total: 33ms

Elastic APM 구성

Docker Compose 구성 (APM Server + Elasticsearch + Kibana)

# docker-compose.yml (APM 포함)
version: '3.8'

services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- ELASTIC_PASSWORD=changeme123!
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- apm
healthcheck:
test: ["CMD-SHELL", "curl -s -u elastic:changeme123! http://localhost:9200/_cluster/health | grep -q yellow\\|green"]
interval: 20s
timeout: 10s
retries: 10

kibana:
image: docker.elastic.co/kibana/kibana:8.12.0
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=changeme123!
ports:
- "5601:5601"
networks:
- apm
depends_on:
elasticsearch:
condition: service_healthy

apm-server:
image: docker.elastic.co/apm/apm-server:8.12.0
container_name: apm-server
# 설정 파일 대신 환경변수로 간단히 구성
command: >
apm-server -e
-E apm-server.rum.enabled=true
-E setup.kibana.host=kibana:5601
-E setup.template.settings.index.number_of_replicas=0
-E apm-server.kibana.enabled=true
-E apm-server.kibana.host=kibana:5601
-E output.elasticsearch.hosts=["elasticsearch:9200"]
-E output.elasticsearch.username=elastic
-E output.elasticsearch.password=changeme123!
ports:
- "8200:8200"
networks:
- apm
depends_on:
elasticsearch:
condition: service_healthy

volumes:
es_data:

networks:
apm:
driver: bridge
# 스택 시작
docker-compose up -d

# APM Server 상태 확인
curl http://localhost:8200/

# 응답 예시:
# {"build_date":"2024-01-15T...","build_sha":"...","publish_ready":true,"version":"8.12.0"}

Spring Boot Elastic APM 에이전트 연동

1단계: APM 에이전트 JAR 다운로드

# Maven Central에서 최신 버전 다운로드
wget -O elastic-apm-agent.jar \
https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.49.0/elastic-apm-agent-1.49.0.jar

# 또는 Maven 의존성으로 추가 (에이전트는 -javaagent 방식 사용 권장)

2단계: pom.xml에 Elastic APM API 의존성 추가 (선택 - 커스텀 트레이싱용)

<!-- pom.xml -->
<dependency>
<groupId>co.elastic.apm</groupId>
<artifactId>apm-agent-api</artifactId>
<version>1.49.0</version>
</dependency>

<!-- Spring Boot Actuator (메트릭 노출용) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

3단계: application.properties 설정

# src/main/resources/application.properties

# ─── Elastic APM 설정 ─────────────────────────────────────────────────────
elastic.apm.service_name=my-spring-app
elastic.apm.server_url=http://localhost:8200
elastic.apm.environment=production
elastic.apm.application_packages=com.example.myapp
elastic.apm.log_level=INFO

# 트레이스 샘플링 비율 (1.0 = 100%, 0.1 = 10%)
elastic.apm.transaction_sample_rate=1.0

# 슬로우 쿼리 임계값 (ms)
elastic.apm.span_min_duration=5ms

# 스택트레이스 최소 지속 시간
elastic.apm.stack_trace_limit=50

# ─── Spring Boot Actuator ──────────────────────────────────────────────────
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always

4단계: JVM 옵션으로 에이전트 연결

# 직접 실행 시
java -javaagent:/path/to/elastic-apm-agent.jar \
-Delastic.apm.service_name=my-spring-app \
-Delastic.apm.server_url=http://localhost:8200 \
-Delastic.apm.environment=production \
-Delastic.apm.application_packages=com.example.myapp \
-jar my-spring-app.jar

# Docker 환경
# docker-compose.yml 서비스에 추가:
# environment:
# - JAVA_TOOL_OPTIONS=-javaagent:/app/elastic-apm-agent.jar
# volumes:
# - ./elastic-apm-agent.jar:/app/elastic-apm-agent.jar

5단계: 커스텀 트레이싱 (특정 메서드 계측)

// src/main/java/com/example/myapp/service/OrderService.java
package com.example.myapp.service;

import co.elastic.apm.api.ElasticApm;
import co.elastic.apm.api.Span;
import co.elastic.apm.api.Transaction;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

private final OrderRepository orderRepository;
private final PaymentClient paymentClient;

public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}

public Order processOrder(Long userId, OrderRequest request) {
// 현재 트랜잭션에 커스텀 레이블 추가
Transaction transaction = ElasticApm.currentTransaction();
transaction.setLabel("user_id", userId);
transaction.setLabel("product_count", request.getItems().size());

// 결제 서비스 호출을 별도 Span으로 추적
Span paymentSpan = transaction.startSpan("external", "payment", "charge");
try {
paymentSpan.setName("Payment Service - charge");
PaymentResult result = paymentClient.charge(request.getAmount());
return orderRepository.save(new Order(userId, request, result));
} catch (Exception e) {
// APM에 에러 기록
paymentSpan.captureException(e);
throw e;
} finally {
paymentSpan.end();
}
}
}

Kibana APM UI 활용

APM Server가 데이터를 수집하면 Kibana → APM 메뉴에서 다음을 확인할 수 있습니다.

서비스 맵(Service Map): 마이크로서비스 간 의존 관계를 자동으로 시각화합니다. 각 서비스 노드에 응답 시간과 처리량이 표시됩니다.

트랜잭션(Transactions): HTTP 엔드포인트별 평균 응답 시간, 처리량(req/min), 에러율을 보여줍니다. 가장 느린 트랜잭션 목록에서 병목 지점을 즉시 발견할 수 있습니다.

트레이스(Traces): 개별 요청의 전체 흐름을 워터폴 차트로 보여줍니다. 각 Span의 소요 시간과 순서를 한눈에 파악할 수 있습니다.

에러(Errors): 스택 트레이스, 발생 빈도, 영향받은 사용자 수를 그룹화하여 보여줍니다.

Pinpoint APM 소개 및 구성

Pinpoint는 네이버에서 개발한 오픈소스 APM으로, 분산 트레이싱에 특화되어 있습니다. 바이트코드 인스트루멘테이션(bytecode instrumentation) 방식으로 코드 수정 없이 자동 계측합니다.

# docker-compose.pinpoint.yml
version: '3.8'

services:
# HBase (Pinpoint 데이터 저장소)
hbase:
image: dajobe/hbase:latest
container_name: pinpoint-hbase
ports:
- "16000:16000"
- "16010:16010"
- "16020:16020"
- "16030:16030"
- "2181:2181"
networks:
- pinpoint

# Pinpoint Collector
pinpoint-collector:
image: pinpointdocker/pinpoint-collector:2.5.3
container_name: pinpoint-collector
environment:
- HBASE_HOST=hbase
- CLUSTER_ENABLE=false
ports:
- "9991:9991/udp" # Agent → Collector (span)
- "9992:9992/udp" # Agent → Collector (stat)
- "9993:9993/udp" # Agent → Collector (tcp)
- "9994:9994"
- "9995:9995/udp"
networks:
- pinpoint
depends_on:
- hbase

# Pinpoint Web UI
pinpoint-web:
image: pinpointdocker/pinpoint-web:2.5.3
container_name: pinpoint-web
environment:
- HBASE_HOST=hbase
- CLUSTER_ENABLE=false
ports:
- "8080:8080"
networks:
- pinpoint
depends_on:
- hbase

networks:
pinpoint:
driver: bridge

Pinpoint Java 에이전트 연결:

# 에이전트 다운로드
wget https://github.com/pinpoint-apm/pinpoint/releases/download/v2.5.3/pinpoint-agent-2.5.3.tar.gz
tar xzf pinpoint-agent-2.5.3.tar.gz

# pinpoint-root.config 수정
# profiler.collector.ip=localhost
# profiler.sampling.rate=1 (1/1 = 100% 샘플링)

# 에이전트 적용
java -javaagent:pinpoint-agent-2.5.3/pinpoint-bootstrap-2.5.3.jar \
-Dpinpoint.agentId=my-spring-app-001 \
-Dpinpoint.applicationName=MY-SPRING-APP \
-Dpinpoint.config=pinpoint-agent-2.5.3/profiles/local/pinpoint-root.config \
-jar my-spring-app.jar

Spring Boot Actuator + Micrometer + Prometheus 연동

Actuator와 Micrometer를 활용해 Prometheus가 수집할 수 있는 메트릭을 노출합니다.

<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.properties
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.metrics.enabled=true
management.metrics.export.prometheus.enabled=true
# 모든 요청에 URI 태그 추가 (카디널리티 주의)
management.metrics.web.server.request.autotime.enabled=true
management.metrics.tags.application=${spring.application.name}
// src/main/java/com/example/myapp/config/MetricsConfig.java
package com.example.myapp.config;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MetricsConfig {

// 비즈니스 메트릭 예시: 주문 처리 건수
@Bean
public Counter orderCounter(MeterRegistry registry) {
return Counter.builder("orders.processed.total")
.description("Total number of orders processed")
.tag("status", "success")
.register(registry);
}

// 결제 처리 시간 측정
@Bean
public Timer paymentTimer(MeterRegistry registry) {
return Timer.builder("payment.processing.duration")
.description("Time taken to process payment")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
}
}
// 서비스에서 메트릭 사용
@Service
public class OrderService {

private final Counter orderCounter;
private final Timer paymentTimer;

public OrderService(Counter orderCounter, Timer paymentTimer) {
this.orderCounter = orderCounter;
this.paymentTimer = paymentTimer;
}

public Order processOrder(OrderRequest request) {
return paymentTimer.record(() -> {
Order order = doProcess(request);
orderCounter.increment();
return order;
});
}
}

메트릭 확인:

# Prometheus 포맷으로 메트릭 노출 확인
curl http://localhost:8080/actuator/prometheus | grep orders_processed

# 출력 예시:
# orders_processed_total{application="my-spring-app",status="success"} 142.0

슬로우 쿼리 탐지 설정

# application.properties

# Spring Data JPA 슬로우 쿼리 로깅
spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=100

# P6Spy 통합 (전체 SQL + 실행 시간 로깅)
# pom.xml에 p6spy 추가 필요:
# <dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.9.1</version></dependency>
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/mydb

# Elastic APM 슬로우 쿼리 임계값
elastic.apm.span_min_duration=5ms
// src/main/resources/spy.properties (P6Spy 설정)
// appender=com.p6spy.engine.spy.appender.Slf4JLogger
// logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
// customLogMessageFormat=%(executionTime) ms | %(sql)
// filter=true
// exclude=show,select 1

에러 추적과 스택 트레이스 분석

// src/main/java/com/example/myapp/exception/GlobalExceptionHandler.java
package com.example.myapp.exception;

import co.elastic.apm.api.ElasticApm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException e) {
// APM에 예외 기록 (스택트레이스 포함)
ElasticApm.currentTransaction().captureException(e);

// 구조화된 로그 출력 (ELK에서 파싱 가능)
log.error("Unhandled exception: type={}, message={}, trace_id={}",
e.getClass().getSimpleName(),
e.getMessage(),
ElasticApm.currentTransaction().ensureParentId(),
e);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"error", "Internal Server Error",
"message", e.getMessage()
));
}
}

Kibana APM → Errors 탭에서 에러를 클릭하면 다음 정보를 확인할 수 있습니다:

  • 전체 자바 스택 트레이스
  • 에러 발생 시점의 HTTP 요청 정보 (URL, 헤더, 바디)
  • 관련 트레이스 ID로 분산 트레이싱 연결
  • 시간대별 에러 발생 추이 그래프

모니터링 비교: Elastic APM vs Pinpoint

항목Elastic APMPinpoint
언어 지원Java, Node.js, Python, Go 등 다양Java/PHP 중심
설치 난이도쉬움 (ELK와 통합)보통 (HBase 필요)
인프라 요구사항Elasticsearch 필요HBase + ZooKeeper
서비스 맵기본 제공상세 콜 그래프
비용오픈소스 + 유료 클라우드완전 오픈소스
실시간성수 초수 초
자동 계측 범위넓음매우 넓음 (200+ 플러그인)

프로덕션 환경에서는 Elastic APM을 ELK Stack과 함께 사용하거나, 네이버 기술 스택에 익숙하다면 Pinpoint를 선택하는 것이 일반적입니다.