이 글에서 얻는 것
- 로그 레벨(DEBUG/INFO/WARN/ERROR)을 적절히 사용할 수 있습니다.
- 구조화된 로깅(Structured Logging)으로 검색 가능한 로그를 작성합니다.
- MDC(Mapped Diagnostic Context)로 요청별 추적을 구현합니다.
- 로그 수집(ELK 스택)의 기본 개념을 이해합니다.
0) 로깅은 “운영 환경에서 눈"이다
왜 로깅이 중요한가?
개발 환경:
- 디버거로 단계별 실행
- 로컬에서 재현 가능
운영 환경:
- 디버거 사용 불가
- 재현 어려움
- 로그만이 유일한 단서!
로그의 역할:
- 문제 원인 파악 (디버깅)
- 사용자 행동 추적
- 성능 분석
- 보안 감사 (Audit)
- 비즈니스 인사이트
1) 로그 레벨
1-1) 로그 레벨 종류
TRACE: 가장 상세한 정보 (거의 사용 안 함)
DEBUG: 디버깅 정보
INFO: 중요한 비즈니스 이벤트
WARN: 경고 (처리는 됐지만 주의 필요)
ERROR: 오류 (처리 실패)
1-2) 레벨별 사용 예시
TRACE (거의 사용 안 함)
log.trace("Entering method calculateTotal() with params: {}", params);
log.trace("Variable x = {}, y = {}", x, y);
DEBUG (개발/테스트)
log.debug("Fetching user from database: userId={}", userId);
log.debug("Query executed: {} (took {}ms)", sql, duration);
log.debug("Cache hit: key={}", key);
INFO (중요 이벤트)
log.info("User logged in: userId={}", userId);
log.info("Order created: orderId={}, amount={}", orderId, amount);
log.info("Email sent to: {}", email);
log.info("Application started in {}ms", startupTime);
WARN (주의 필요)
log.warn("Retry attempt {}/{} for orderId={}", attempt, maxRetries, orderId);
log.warn("Deprecated API called: /api/v1/users");
log.warn("Connection pool usage high: {}/{}", active, max);
log.warn("Fallback to default value: {}", defaultValue);
ERROR (오류)
log.error("Failed to process order: orderId={}", orderId, exception);
log.error("Database connection failed", exception);
log.error("Payment gateway timeout: orderId={}", orderId);
1-3) Spring Boot 로그 레벨 설정
# application.yml
logging:
level:
root: INFO # 기본 레벨
com.myapp: DEBUG # 패키지별 설정
org.springframework.web: DEBUG # Spring Web 로그
org.hibernate.SQL: DEBUG # SQL 로그
org.hibernate.type.descriptor.sql: TRACE # SQL 파라미터
환경별 설정:
# application-dev.yml (개발)
logging:
level:
root: DEBUG
# application-prod.yml (운영)
logging:
level:
root: INFO
com.myapp: WARN # 운영에서는 WARN 이상만
2) 구조화된 로깅 (Structured Logging)
2-1) 왜 구조화가 필요한가?
❌ 비구조화 로그 (검색 어려움)
log.info("User Alice logged in from 192.168.1.1 at 2025-12-16 10:30:00");
log.info("Order 12345 created by user Bob with amount $1000");
검색 시:
"Alice" 검색 → 다른 Alice도 나옴
"amount" 검색 → 정확한 금액 추출 어려움
✅ 구조화 로그 (검색 쉬움)
log.info("User logged in: userId={}, ip={}", userId, ipAddress);
log.info("Order created: orderId={}, userId={}, amount={}", orderId, userId, amount);
JSON 로그 (더 강력):
{
"timestamp": "2025-12-16T10:30:00.000Z",
"level": "INFO",
"message": "User logged in",
"userId": "alice",
"ipAddress": "192.168.1.1",
"requestId": "abc-123"
}
2-2) Logback JSON 설정
의존성:
// build.gradle
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
logback-spring.xml:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- JSON 형식으로 출력 -->
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
2-3) 구조화 로깅 예시
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(CreateOrderRequest request) {
log.info("Creating order: userId={}, items={}, totalAmount={}",
request.getUserId(),
request.getItems().size(),
request.getTotalAmount());
try {
Order order = orderRepository.save(request.toEntity());
log.info("Order created successfully: orderId={}, userId={}, amount={}",
order.getId(),
order.getUserId(),
order.getAmount());
return order;
} catch (Exception e) {
log.error("Failed to create order: userId={}, error={}",
request.getUserId(),
e.getMessage(),
e); // 마지막 파라미터는 예외
throw e;
}
}
}
출력 (JSON):
{
"timestamp": "2025-12-16T10:30:00.000Z",
"level": "INFO",
"logger": "com.myapp.service.OrderService",
"message": "Order created successfully: orderId=12345, userId=alice, amount=1000",
"orderId": 12345,
"userId": "alice",
"amount": 1000
}
3) MDC: 요청별 컨텍스트 추적
3-1) MDC란?
MDC (Mapped Diagnostic Context)
= 현재 스레드에 컨텍스트 정보 저장
용도:
- 요청 ID 추적
- 사용자 ID 추적
- 세션 ID 추적
장점:
- 모든 로그에 자동으로 추가
- 요청별로 로그 필터링 가능
3-2) MDC 사용 예시
Filter에서 MDC 설정:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 요청 ID 생성
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
// 사용자 ID (인증된 경우)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
MDC.put("userId", auth.getName());
}
// IP 주소
MDC.put("ipAddress", request.getRemoteAddr());
filterChain.doFilter(request, response);
} finally {
// 요청 종료 시 MDC 클리어 (메모리 누수 방지)
MDC.clear();
}
}
}
로그 출력:
@Service
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public User findUser(Long id) {
// MDC에서 자동으로 requestId, userId 추가
log.info("Fetching user: id={}", id);
return userRepository.findById(id).orElseThrow();
}
}
Logback 설정 (MDC 포함):
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [requestId=%X{requestId}, userId=%X{userId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
출력:
10:30:00.123 [http-nio-8080-exec-1] INFO c.m.service.UserService [requestId=abc-123, userId=alice] - Fetching user: id=1
3-3) 비동기 작업에서 MDC
@Service
public class AsyncService {
@Async
public CompletableFuture<Void> processAsync(Order order) {
// ❌ 새 스레드에서는 MDC가 비어있음!
// ✅ 해결: MDC를 전달
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return CompletableFuture.runAsync(() -> {
// MDC 복원
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
try {
log.info("Processing order asynchronously: orderId={}", order.getId());
// 처리...
} finally {
MDC.clear();
}
});
}
}
4) 로그 수집과 분석 (ELK 스택)
4-1) ELK 스택이란?
ELK = Elasticsearch + Logstash + Kibana
Elasticsearch: 로그 저장/검색
Logstash: 로그 수집/변환
Kibana: 로그 시각화/대시보드
플로우:
App → (JSON 로그) → Logstash → Elasticsearch → Kibana
4-2) Filebeat로 로그 수집
docker-compose.yml:
version: '3.8'
services:
app:
image: myapp:1.0
volumes:
- ./logs:/app/logs # 로그 파일 공유
filebeat:
image: docker.elastic.co/beats/filebeat:8.11.0
volumes:
- ./logs:/var/log/myapp
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml
depends_on:
- elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
ports:
- "9200:9200"
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
filebeat.yml:
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/myapp/*.log
json.keys_under_root: true # JSON 파싱
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "myapp-logs-%{+yyyy.MM.dd}"
setup.kibana:
host: "kibana:5601"
4-3) Kibana에서 로그 검색
검색 예시:
- level: ERROR
- userId: alice
- requestId: abc-123
- message: *order*
- amount > 1000
- @timestamp: [now-1h TO now]
5) 로깅 베스트 프랙티스
✅ 1. 로그 레벨 적절히 사용
// ✅ 좋은 예
log.debug("Cache lookup: key={}", key); // 디버깅용
log.info("User logged in: userId={}", userId); // 비즈니스 이벤트
log.warn("Retry attempt {}/{}", attempt, max); // 경고
log.error("Payment failed: orderId={}", orderId, e); // 오류
// ❌ 나쁜 예
log.info("x = 1, y = 2"); // DEBUG가 적절
log.error("User logged in"); // ERROR가 아님, INFO
✅ 2. 파라미터 바인딩 사용
// ❌ 나쁜 예 (문자열 연결)
log.info("User " + userId + " logged in"); // 항상 문자열 생성
// ✅ 좋은 예 (파라미터 바인딩)
log.info("User logged in: userId={}", userId); // 로그 레벨이 활성화된 경우만 생성
✅ 3. 예외는 마지막 파라미터로
// ✅ 좋은 예
try {
// ...
} catch (Exception e) {
log.error("Failed to process order: orderId={}", orderId, e);
// 예외는 마지막 파라미터 → 스택 트레이스 출력
}
// ❌ 나쁜 예
catch (Exception e) {
log.error("Error: {}", e.getMessage()); // 스택 트레이스 없음!
}
✅ 4. 민감 정보 로그 안 함
// ❌ 나쁜 예
log.info("Login attempt: email={}, password={}", email, password);
log.info("Credit card: {}", creditCardNumber);
// ✅ 좋은 예
log.info("Login attempt: email={}", maskEmail(email));
log.info("Payment processed: cardLast4={}", cardLast4Digits);
✅ 5. 구조화된 데이터
// ❌ 나쁜 예
log.info("User Alice ordered 3 items for $100");
// ✅ 좋은 예
log.info("Order created: userId={}, itemCount={}, amount={}",
userId, itemCount, amount);
✅ 6. 요청 시작/종료 로깅
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
log.info("API called: POST /orders, userId={}", request.getUserId());
try {
Order order = orderService.createOrder(request);
log.info("API success: POST /orders, orderId={}, duration={}ms",
order.getId(), duration);
return ResponseEntity.ok(order);
} catch (Exception e) {
log.error("API failed: POST /orders, userId={}, error={}",
request.getUserId(), e.getMessage(), e);
throw e;
}
}
}
6) 자주 하는 실수
❌ 실수 1: 너무 많은 로그
// ❌ 나쁜 예
for (User user : users) {
log.info("Processing user: {}", user.getId()); // 100만 건 루프!
}
// ✅ 좋은 예
log.info("Processing {} users", users.size());
for (User user : users) {
log.debug("Processing user: {}", user.getId()); // DEBUG 레벨
}
log.info("Processed {} users in {}ms", users.size(), duration);
❌ 실수 2: 로그에 예외 안 넣음
// ❌ 나쁜 예
catch (Exception e) {
log.error("Error occurred: {}", e.getMessage());
}
// ✅ 좋은 예
catch (Exception e) {
log.error("Error occurred", e); // 스택 트레이스 포함
}
❌ 실수 3: MDC 클리어 안 함
// ❌ 나쁜 예
MDC.put("userId", userId);
// 요청 끝나도 MDC가 남아있음 (메모리 누수)
// ✅ 좋은 예
try {
MDC.put("userId", userId);
// 처리...
} finally {
MDC.clear();
}
연습 (추천)
구조화 로깅 적용
- 프로젝트에 JSON 로깅 설정
- 모든 로그를 key=value 형식으로 변경
MDC 구현
- Filter에서 requestId 추가
- 모든 로그에 requestId 포함 확인
ELK 스택 실습
- Docker Compose로 ELK 구성
- Kibana에서 로그 검색/시각화
요약: 스스로 점검할 것
- 로그 레벨을 적절히 사용할 수 있다
- 구조화된 로그를 작성할 수 있다
- MDC로 요청별 추적을 구현할 수 있다
- ELK 스택의 기본 개념을 이해한다
- 로깅 베스트 프랙티스를 적용할 수 있다
다음 단계
- APM (Application Performance Monitoring):
/learning/deep-dive/deep-dive-apm-basics/ - Prometheus + Grafana:
/learning/deep-dive/deep-dive-prometheus-grafana/ - 분산 추적 (Zipkin):
/learning/deep-dive/deep-dive-distributed-tracing/
💬 댓글