서버 로그 관리와 모니터링: 서버가 아프면 누가 알려주는가

📖 12min read

“서버 죽었어요” — 고객 전화로 알게 되는 최악의 상황

Redis로 속도를 올리고, Nginx로 요청을 정리한 뒤, 서비스는 한동안 평화로웠다. 나는 그 안정감에 취해 있었다. "이제 좀 안정적이네"라며 퇴근 후에는 서버 걱정 없이 잠을 잤다.

그러던 어느 금요일 밤, 퇴근 후 맥주를 마시고 있는데 전화가 왔다.

“OO씨, 서비스 접속이 안 돼요. 언제부터 안 됐는지도 모르겠는데, 지금 고객들한테 민원 들어오고 있어요.”

가슴이 철렁했다. 급하게 노트북을 열고 서버에 접속해 보니, Spring Boot 프로세스가 죽어 있었다. 메모리 부족(OOM)으로 서버가 조용히 꺼진 것이다. 조용히. 아무런 알림도 없이.

로그를 뒤져보니 서버는 3시간 전에 이미 죽어 있었다. 3시간 동안 고객들은 빈 화면을 보고 있었고, 나는 맥주를 마시고 있었다.

“서버가 죽었는데 나한테 알려주는 사람이 아무도 없다.”

1인 개발자에게는 이것이 가장 무서운 문제다. 서버가 터져도 알 방법이 없다. 고객 전화가 올 때까지 모른다. 나는 그날 밤, 서버 복구를 마치고 다짐했다.

‘서비스에 건강 검진 시스템을 달자.’

알림이 없는 서버는, 불이 나도 소방서에 연락이 안 되는 건물이다.

1단계: 로그(Log) — 서버의 블랙박스

서버에 무슨 일이 있었는지 알려면, 기록이 있어야 한다. 이것이 ‘로그(Log)’다.

자동차에 블랙박스가 있듯, 서버에는 로그가 있다. 로그를 제대로 남겨두지 않으면, 서버가 왜 죽었는지 사후 분석조차 할 수 없다.

Spring Boot 로그 기본 설정

Spring Boot는 기본적으로 콘솔에 로그를 출력한다. 하지만 콘솔은 서버가 재시작되면 사라진다. 파일로 남겨야 한다.

# application.yml
logging:
  file:
    name: /var/log/myapp/application.log
  level:
    root: INFO
    com.mycompany: DEBUG
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
설정 의미
file.name 로그를 파일로 저장하는 경로
level.root: INFO 기본적으로 INFO 이상만 기록
level.com.mycompany: DEBUG 내 코드는 DEBUG까지 기록
max-file-size: 10MB 파일이 10MB를 넘으면 새 파일로 분리
max-history: 30 30일치만 보관, 오래된 건 자동 삭제

로그 레벨에는 계층이 있다. DEBUG → INFO → WARN → ERROR. 운영 서버에서는 INFO 이상만 남기고, 문제가 생겼을 때 일시적으로 DEBUG를 켜는 것이 일반적이다.

어떤 로그를 남겨야 하는가

초보 시절의 나는 System.out.println("여기 옴1") 같은 로그를 남기고 있었다. 실무에서는 이렇게 남겨야 한다.

// 나쁜 예
System.out.println("에러 발생");

// 좋은 예
log.error("[결제 실패] orderId={}, userId={}, reason={}",
    orderId, userId, e.getMessage(), e);

좋은 로그의 조건은 이렇다.

  • ‘언제’ 발생했는지 (타임스탬프 — 프레임워크가 자동으로 찍어줌)
  • ‘어디서’ 발생했는지 (클래스명, 메서드명)
  • ‘무엇이’ 발생했는지 (에러 메시지, 스택 트레이스)
  • ‘관련 데이터는’ 무엇인지 (주문번호, 유저ID 등)

새벽 3시에 알림을 받고 로그를 열었을 때, "에러 발생"만 적혀 있으면 아무것도 할 수 없다. 로그는 미래의 나에게 보내는 수사 보고서다.

로그는 탐정의 수사 보고서다. 상세할수록 범인(버그)을 빨리 잡는다.

2단계: Nginx 로그 — 누가 들어왔고, 무엇이 실패했는가

Spring Boot 로그만으로는 전체 그림이 보이지 않는다. 사용자가 어떤 URL로 접속했는지, 응답 시간은 얼마나 걸렸는지, 404 에러가 얼마나 발생하는지를 알려면 Nginx 로그도 관리해야 한다.

Nginx는 기본적으로 두 가지 로그를 남긴다.

로그 용도 경로
access.log 모든 접속 기록 (누가, 언제, 어디로) /var/log/nginx/access.log
error.log 에러 발생 기록 (502, 504 등) /var/log/nginx/error.log
# 실시간 접속 로그 확인
tail -f /var/log/nginx/access.log

# 결과 예시:
# 192.168.1.1 - [25/Feb/2025:10:30:01] "GET /api/projects" 200 0.003s
# 192.168.1.2 - [25/Feb/2025:10:30:05] "GET /api/users" 502 0.000s

위 로그에서 502 응답이 보인다면? 이전 글에서 배웠듯, 502는 Nginx는 살아있지만 뒤쪽 Spring Boot가 죽었다는 뜻이다. Nginx 로그가 문제의 시작점을 알려주고, Spring Boot 로그에서 원인을 찾는 것이다.


3단계: 서버 상태 감시 — 죽기 전에 알려줘

로그는 ‘이미 일어난 일’의 기록이다. 하지만 진짜 필요한 건 ‘죽기 전에 미리 알려주는 것’이다.

Health Check — 서버야 살아있니?

Spring Boot에는 서버의 건강 상태를 확인할 수 있는 내장 기능이 있다. ‘Spring Boot Actuator’라는 라이브러리다.

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics
  endpoint:
    health:
      show-details: always

이 설정만 추가하면 /actuator/health 엔드포인트가 생긴다.

$ curl http://localhost:8080/actuator/health
{
  "status": "UP",
  "components": {
    "db": { "status": "UP" },
    "redis": { "status": "UP" },
    "diskSpace": {
      "status": "UP",
      "details": { "free": "15GB" }
    }
  }
}

DB 연결, Redis 연결, 디스크 공간까지 한눈에 볼 수 있다. 만약 DB가 끊기면 “status”: "DOWN"이 뜬다.

간단한 알림 시스템 — 쉘 스크립트 + cron

거창한 모니터링 도구가 없어도, 쉘 스크립트와 cron만으로 기본적인 감시가 가능하다.

#!/bin/bash
# /home/user/health_check.sh

HEALTH_URL="http://localhost:8080/actuator/health"
WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" $HEALTH_URL)

if [ "$STATUS" != "200" ]; then
    # 서버가 응답하지 않으면 Slack으로 알림 전송
    curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"🚨 서버 응답 없음! HTTP 상태: '$STATUS'"}' \
        $WEBHOOK_URL
fi
# cron에 등록 (1분마다 실행)
$ crontab -e
* * * * * /home/user/health_check.sh
구성 요소 역할
curl 서버에 health check 요청
http_code 응답 코드 확인 (200이면 정상)
Slack Webhook 이상 감지 시 메신저로 알림
cron 1분마다 자동 실행

이 스크립트가 돌고 있었다면, 금요일 밤 서버가 죽은 지 1분 만에 내 핸드폰에 Slack 알림이 왔을 것이다. 3시간이 아니라 1분 만에.

서버의 심장이 멈추면 1분 안에 알려주는 시스템. 이것이 모니터링의 시작이다.

한 걸음 더: 전문 모니터링 도구

위의 쉘 스크립트는 ‘서버가 살아있는지 죽었는지’ 정도만 확인할 수 있다. 더 세밀한 감시가 필요하다면 전문 도구를 고려할 수 있다.

도구 특징 적합한 환경
Prometheus + Grafana CPU, 메모리, 응답 시간 등 지표를 수집하고 대시보드로 시각화 자체 서버가 있는 경우
Uptime Kuma 가볍고 직관적인 서버 상태 모니터링 (Docker로 쉽게 설치) 1인/소규모 팀
Sentry 애플리케이션 에러 추적 특화. 에러 발생 시 코드 위치까지 알려줌 에러 추적 중심
AWS CloudWatch AWS 인프라 모니터링 자동 통합 AWS 사용 시

1인 개발 단계에서는 쉘 스크립트 + Slack 알림만으로도 충분하다. 서비스가 커지면 Prometheus + Grafana 조합을 추천한다. Uptime Kuma는 설치가 매우 쉬워서 빠르게 시작하기 좋다.


실무 조언: 로그와 모니터링의 핵심 원칙

1. 로그는 ‘검색 가능’해야 한다

로그 파일이 1GB짜리 한 덩어리이면 grep으로 찾는 데만 한 세월이다. 날짜별로 파일을 나누고(Rolling), 일정 기간이 지나면 자동 삭제되도록 설정하자. 위의 max-file-sizemax-history 설정이 바로 그것이다.

2. 민감 정보는 로그에 남기지 마라

// 절대 하면 안 되는 로그
log.info("로그인 시도: id={}, password={}", userId, password);

// 올바른 로그
log.info("로그인 시도: id={}", userId);

비밀번호, 주민번호, 카드 번호 같은 민감 정보가 로그에 남으면 개인정보보호법 위반이다.

3. 알림 피로(Alert Fatigue)를 주의하라

모든 WARNING에 알림을 걸면, 하루에 수백 개의 알림이 쏟아진다. 결국 진짜 중요한 알림도 무시하게 된다. 늑대소년이 되는 것이다.

알림은 ‘당장 행동이 필요한 것’에만 걸자. 서버 다운, DB 연결 끊김, 디스크 90% 초과. 이 정도만 알림으로 보내고, 나머지는 대시보드에서 확인하면 충분하다.


마치며: ‘운영’은 기능 개발만큼 중요하다

이제 나의 서비스에는 3겹의 안전망이 갖춰졌다.

  • Redis: 불필요한 DB 호출을 줄여 속도를 높이고
  • Nginx: 요청을 정리하고 외부로부터 내부 구조를 숨기고
  • 로그/모니터링: 문제가 생기면 1분 안에 알려준다

처음에는 ‘기능 개발’만이 개발자의 일이라고 생각했다. 하지만 실무를 겪을수록 깨달았다. 기능을 만드는 것보다, 만든 것을 안정적으로 돌아가게 유지하는 것이 더 어렵고 중요하다는 것을.

서비스는 한번 배포하면 끝이 아니다. 24시간 돌아가야 하고, 사용자가 늘어나도 버텨야 하고, 문제가 생기면 즉시 대응할 수 있어야 한다. 그 ‘즉시’를 가능하게 하는 것이 바로 로그와 모니터링이다.

기능을 만드는 건 ‘출발’이다. 그것을 안정적으로 지키는 것이 ‘운영’이다. 개발자는 두 가지를 모두 할 줄 알아야 한다.

댓글 남기기