systemd로 스프링 부트 앱 무중단 운영하기

CHOI HONGSEO·2026년 7월 22일

지난 글(AWS t3.micro에서 스프링 부트가 자꾸 죽던 이유)에서 OOM으로 서버가 죽었던 걸 다뤘다. 스왑을 추가하고 힙을 제한해서 메모리 부족 자체는 완화했지만, 근본적인 문제가 하나 더 있었다 — 그동안 이 앱을 nohup java -jar ... 같은 방식으로 SSH 세션에서 수동으로 띄우고 있었다는 것이다.

수동 실행의 문제점

  1. 인스턴스가 재부팅되면 다시 켜지지 않는다 — 사람이 SSH로 접속해서 다시 실행해줘야 한다

  2. 프로세스가 죽으면 그걸로 끝 — 재시작해주는 사람이 없으면 계속 죽어있는 채로 방치된다

  3. 재배포할 때 실수하기 쉽다 — 실제로 OOM 당시 커널 로그를 뜯어보니 java 프로세스가 두 개 떠 있던 흔적이 있었다. 재배포하면서 이전 프로세스를 안 죽이고 새로 하나 더 띄운 것으로 추정된다. 메모리가 빠듯한 인스턴스에서 프로세스가 중복으로 떠 있으면 OOM을 자초하는 셈이다

이 세 가지를 한 번에 해결하는 방법이 systemd 서비스로 등록하는 것이다.

서비스 파일 작성

/etc/systemd/system/stock-backend.service


[Unit]

Description=Stock Disclosure Crawler Backend

After=network.target mariadb.service

Requires=mariadb.service

[Service]

Type=simple

User=ubuntu

WorkingDirectory=/home/ubuntu/stock/backend

ExecStart=/usr/bin/java -Xmx400m -jar /home/ubuntu/stock/backend/backend-0.0.1-SNAPSHOT.jar

EnvironmentFile=/home/ubuntu/stock/backend/app.env

Restart=always

RestartSec=10

SuccessExitStatus=143

[Install]

WantedBy=multi-user.target

항목별로 짚어보면:

  • After=network.target mariadb.service / Requires=mariadb.service: 네트워크와 MariaDB가 먼저 뜬 다음에 이 서비스를 시작하라는 뜻. DB 연결이 필요한 앱인데 DB보다 먼저 뜨면 초기 연결에 실패할 수 있어서 순서를 강제했다.
  • ExecStart-Xmx400m: 지난 글에서 다룬 힙 제한을 여기서 적용한다.
  • EnvironmentFile: DB 비밀번호, 텔레그램 봇 토큰 같은 값을 소스코드에 하드코딩하지 않고 별도 env 파일로 분리해서 여기서 주입한다. 스프링 부트의 application.properties에는 ${DB_PASSWORD:...} 형태로 환경변수를 우선 읽도록 설정해뒀다.
  • Restart=always / RestartSec=10: 어떤 이유로든 프로세스가 죽으면 10초 뒤에 자동으로 다시 띄운다. 이제 OOM이 다시 발생해도 사람이 손대지 않아도 짧은 다운타임 후 스스로 복구된다.
  • SuccessExitStatus=143: systemctl stop으로 정상 종료시킬 때 SIGTERM(종료 코드 143)을 보내는데, 이걸 "성공적인 종료"로 안 쳐주면 systemd가 이걸 "비정상 종료"로 착각해서 다시 살리려고 시도한다. 그러면 정상적으로 서비스를 내리고 싶어도 계속 재시작되는 웃긴 상황이 생긴다. 이 한 줄로 그걸 방지한다.

설치


sudo cp stock-backend.service /etc/systemd/system/

sudo systemctl daemon-reload

sudo systemctl enable stock-backend    # 부팅 시 자동 시작

sudo systemctl enable mariadb          # DB도 마찬가지로

sudo systemctl start stock-backend

sudo systemctl status stock-backend

enable을 빼먹으면 지금 당장은 잘 돌아가도, 인스턴스를 재부팅하는 순간 다시 예전처럼 수동으로 띄워줘야 하는 상황으로 돌아간다. 이번에 재부팅 뒤에 서버가 안 켜져서 놀랐던 게 바로 이걸 빼먹어서였다.

삽질: jar를 새로 올렸는데 반영이 안 됨

systemd로 전환한 뒤, 재배포할 때 FileZilla로 새 jar를 덮어쓰기만 하고 서비스는 재시작을 안 한 적이 있다. 그랬더니 백그라운드 스케줄러는 멀쩡히 도는데 API 요청만 오면 연결이 끊기는 애매한 장애가 났다. 파일만 덮어쓴다고 실행 중인 프로세스에 반영되는 게 아니라는 걸 몸으로 배웠다 — 이 디버깅 과정은 내용이 꽤 길어서 별도 글(재배포하다 서버를 반쯤 죽인 이야기)로 따로 정리하겠습니다.

교훈만 요약하면, 배포 순서를 이렇게 확정했다:


sudo systemctl stop stock-backend   # 먼저 내리고

# (여기서 FileZilla로 새 jar 업로드)

sudo systemctl start stock-backend  # 그다음 새로 띄우기

결과 확인


sudo systemctl status stock-backend

● stock-backend.service - Stock Disclosure Crawler Backend

Loaded: loaded (/etc/systemd/system/stock-backend.service; enabled; preset: enabled)

Active: active (running) since ...

Main PID: XXXX (java)

enabledactive (running)이 같이 떠 있으면 재부팅에도, 크래시에도 살아남는 구조가 완성된 것이다.

회고

  • "일단 돌아가게만 만들기"와 "죽어도 스스로 복구되게 만들기"는 완전히 다른 레벨의 작업이라는 걸 체감했다.
  • 인프라를 코드/설정 파일로 관리하면(이번엔 .service 파일 하나) 재현 가능하고, 실수를 구조적으로 줄일 수 있다. 사람이 매번 명령어를 외워서 치는 것보다 훨씬 안전하다.
  • 배포 자동화 없이 수동으로 파일 올리고 재시작하는 과정 자체가 실수 포인트라는 걸 알았다. 다음 단계로는 CI/CD 파이프라인을 구축해서 이 수동 과정 자체를 없애는 게 목표다.

다음 글에서는 이렇게 자동 복구 체계를 만들어놓고도 놓칠 수 있는 부분, "장애가 났다는 걸 사람이 어떻게 바로 알 수 있는가"를 CloudWatch 알람으로 해결한 과정을 정리한다.

profile
곧 성공할 개발자입니다.

0개의 댓글