클라우드 환경, 특히 AWS EC2 인스턴스에 힘들게 개발한 Java 또는 Spring Boot 애플리케이션(.jar)을 배포하고 나면 개발자들은 한 가지 공통적인 문제에 부딪히곤 합니다. 바로 "어떻게 이 애플리케이션을 안정적으로, 중단 없이 계속 실행시킬 것인가?" 하는 문제입니다. 터미널을 열고 java -jar my-app.jar
명령어를 실행한 뒤, SSH 접속을 끊으면 애플리케이션도 함께 종료됩니다. 서버가 예기치 않게 재부팅이라도 되면 수동으로 다시 접속해서 실행해야 하는 번거로움은 말할 것도 없습니다. OutOfMemoryError와 같은 얘기치 못한 오류로 프로세스가 종료되었을 때, 이를 감지하고 자동으로 재시작하는 기능은 운영 환경의 필수 요건입니다.
이러한 문제를 해결하기 위해 많은 분들이 nohup
명령어와 백그라운드 실행(&
)을 사용하거나, screen
또는 tmux
와 같은 터미널 멀티플렉서를 활용합니다. 물론 이 방법들도 급할 때는 유용하지만, 프로덕션 환경에서 장기적으로 서비스를 운영하기에는 여러 한계점을 가집니다.
nohup
&&
: 프로세스가 예기치 않게 종료되었을 때 자동으로 재시작해주지 않습니다. 단순히 터미널 세션이 끊어져도 프로세스가 유지되는 수준입니다.screen
/tmux
: 가상 터미널 세션을 만들어 작업을 유지하는 방식이지만, 근본적으로 데몬(Daemon)으로 서비스를 관리하는 방식이 아닙니다. 서버 재부팅 시 세션이 사라지므로 자동 실행을 위해서는 추가적인 스크립트 작업이 필요하며, 관리가 복잡해집니다.
그렇다면 진정한 프로덕션 환경에서 권장되는, 가장 안정적이고 표준적인 방법은 무엇일까요? 바로 리눅스의 시스템 서비스 매니저(System Service Manager)를 사용하는 것입니다. 최신 리눅스 배포판(Ubuntu 16.04 이상, CentOS 7 이상 등)의 표준으로 자리 잡은 systemd
를 이용하면, 우리의 Java 애플리케이션을 하나의 독립된 시스템 서비스로 등록하고 관리할 수 있습니다. 이를 통해 우리는 다음과 같은 강력한 기능들을 손쉽게 구현할 수 있습니다.
- 부팅 시 자동 실행: 서버가 켜질 때 애플리케이션이 자동으로 시작됩니다.
- 자동 재시작: 애플리케이션이 오류로 인해 종료되면
systemd
가 이를 감지하고 자동으로 다시 실행시켜 줍니다. - 표준화된 관리:
systemctl
이라는 일관된 명령어로 서비스의 시작, 중지, 재시작, 상태 확인이 가능합니다. - 강력한 로깅: 애플리케이션의 모든 출력(stdout, stderr)이 중앙화된 로그 시스템(Journal)에 기록되어 디버깅 및 모니터링이 매우 용이합니다.
- 리소스 제어 및 의존성 관리: 서비스가 사용하는 CPU, 메모리 등의 리소스를 제한하거나, 데이터베이스(MySQL, PostgreSQL 등)와 같은 다른 서비스가 시작된 후에 우리 애플리케이션을 실행하도록 의존성을 설정할 수 있습니다.
이 글에서는 복잡한 쉘 스크립트(.sh) 작성 없이, systemd
의 서비스 유닛(Unit) 파일을 직접 작성하여 AWS EC2(Ubuntu 20.04/22.04 기준) 환경에서 Spring Boot 애플리케이션을 안정적으로 운영하는 방법을 처음부터 끝까지, 아주 상세하게 다룹니다. 이 가이드를 따라오시면, 더 이상 새벽에 서버가 다운되었다는 알림에 놀라 잠에서 깨는 일은 없을 것입니다.
1. Systemd 서비스의 핵심, 유닛(Unit) 파일 이해하기
systemd
를 다루기 전에, 그 핵심 구성 요소인 '유닛(Unit)' 파일에 대해 먼저 이해해야 합니다. systemd
가 관리하는 모든 리소스는 '유닛'이라는 개념으로 추상화되며, 이 유닛의 설정과 동작 방식을 정의하는 파일이 바로 '유닛 파일'입니다. 유닛 파일은 확장자에 따라 종류가 나뉩니다.
.service
: 가장 흔하게 사용되는 유닛으로, 우리가 다룰 데몬 프로세스(서비스)를 정의합니다..socket
: 특정 소켓을 감시하고, 해당 소켓으로 요청이 들어오면 연결된 서비스를 활성화합니다. (e.g., 특정 포트로 요청이 올 때까지 서비스를 실행하지 않다가 요청이 오면 실행).target
: 다른 유닛들을 그룹화하는 역할을 합니다. 시스템의 특정 상태를 나타내며, 부팅 시 특정 타겟을 활성화함으로써 관련된 모든 유닛을 한 번에 시작/중지할 수 있습니다. (e.g.,multi-user.target
,graphical.target
).timer
: cron과 유사하게 특정 시간에 또는 주기적으로 다른 유닛을 실행시키는 타이머를 정의합니다.
이 글에서는 Java 애플리케이션을 서비스로 등록할 것이므로, .service
유닛 파일을 집중적으로 다룰 것입니다. 이 파일은 보통 /etc/systemd/system/
경로에 생성하며, 크게 세 가지 섹션([Unit]
, [Service]
, [Install]
)으로 구성됩니다.
[Unit]
섹션: 서비스에 대한 전반적인 설명과 다른 유닛과의 의존 관계를 정의합니다. "이 서비스는 무엇이며, 언제 실행되어야 하는가?"에 대한 메타데이터가 담깁니다.[Service]
섹션: 서비스의 실제 동작을 정의하는 가장 중요한 부분입니다. 어떤 사용자로, 어떤 명령어를 실행할지, 실패 시 어떻게 동작할지 등을 상세하게 설정합니다.[Install]
섹션:systemctl enable
명령을 통해 서비스를 활성화할 때, 어떤 타겟에 이 서비스를 연결할지를 정의합니다. 즉, 시스템 부팅 시 어떤 상태에서 이 서비스를 자동으로 시작할지를 결정합니다.
이제 이 구조를 바탕으로 실제 우리만의 서비스 파일을 만들어 보겠습니다.
2. Spring Boot 앱을 위한 Systemd 서비스 파일 작성 (실전편)
이제 본격적으로 우리의 Spring Boot 애플리케이션을 위한 .service
파일을 작성해 보겠습니다. 가독성과 관리 편의성을 위해 서비스 이름은 애플리케이션의 이름과 유사하게 짓는 것이 좋습니다. 예를 들어, 애플리케이션 이름이 'my-awesome-app'이라면 서비스 파일 이름은 my-awesome-app.service
로 명명합니다.
사전 준비 사항
- EC2 인스턴스: Ubuntu 20.04 LTS 또는 22.04 LTS가 설치된 EC2 인스턴스가 준비되어 있어야 합니다.
- Java 설치: 인스턴스에 애플리케이션 실행에 필요한 버전의 Java(OpenJDK)가 설치되어 있어야 합니다. (
sudo apt update && sudo apt install openjdk-17-jdk
) - .jar 파일 업로드: 빌드된 애플리케이션의
.jar
파일이 서버의 특정 경로에 업로드되어 있어야 합니다. 일반적으로 EC2의 기본 사용자인 `ubuntu`의 홈 디렉토리(/home/ubuntu/
)나, 별도의 애플리케이션 디렉토리(/app/
등)에 위치시킵니다. 이 가이드에서는/home/ubuntu/my-app-0.0.1.jar
파일이 있다고 가정하겠습니다.
Step 1: 서비스 파일 생성
터미널에서 root
권한으로 /etc/systemd/system/
디렉토리에 새로운 서비스 파일을 생성합니다. nano
, vim
등 편한 텍스트 편집기를 사용하세요.
sudo nano /etc/systemd/system/my-app.service
Step 2: 서비스 파일 내용 작성
이제 열린 편집기에 아래 내용을 작성합니다. 각 항목에 대한 상세한 설명이 이어지니, 단순히 복사-붙여넣기 하지 마시고 각 지시어의 의미를 꼭 이해하며 진행하시기 바랍니다.
[Unit]
Description=My Awesome Spring Boot Application
Documentation=https://my-docs.example.com
After=network.target mysql.service
Wants=mysql.service
[Service]
# 실행 권한 및 환경 설정
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu
Environment="SPRING_PROFILES_ACTIVE=prod"
Environment="DB_PASSWORD=your_secure_password"
# 실행 명령어 정의
ExecStart=/usr/bin/java -jar -Duser.timezone=Asia/Seoul /home/ubuntu/my-app-0.0.1.jar
# 프로세스 종료 및 재시작 정책
SuccessExitStatus=143
TimeoutStopSec=10
Restart=on-failure
RestartSec=5
# 리소스 제한 (선택 사항)
# LimitNOFILE=65536
# LimitNPROC=4096
[Install]
WantedBy=multi-user.target
위 설정은 매우 견고하며 실무에서 바로 사용 가능한 수준의 구성입니다. 이제 각 라인이 어떤 의미를 가지는지 하나씩 파헤쳐 보겠습니다.
[Unit] 섹션 상세 분석
Description
: 서비스에 대한 간단한 설명입니다.systemctl status
명령을 실행했을 때 표시되는 이름으로, 어떤 서비스인지 사람이 쉽게 식별할 수 있도록 도와줍니다.Documentation
: (선택 사항) 서비스와 관련된 문서가 있다면 URL을 명시할 수 있습니다.After=network.target mysql.service
: 이 서비스의 시작 순서를 정의하는 매우 중요한 지시어입니다.network.target
은 네트워크가 완전히 활성화된 후에 이 서비스를 시작하라는 의미입니다. 웹 애플리케이션이라면 필수적인 설정입니다.mysql.service
는 MySQL 데이터베이스 서비스가 시작된 *이후에* 우리 애플리케이션을 시작하라는 의미입니다. 만약 PostgreSQL을 사용한다면postgresql.service
, Redis를 사용한다면redis-server.service
와 같이 애플리케이션이 의존하는 다른 서비스들을 명시해주어야 합니다.Wants=mysql.service
:After
가 순서만 정의하는 반면,Wants
는 '느슨한 의존성'을 설정합니다.my-app.service
가 시작될 때,systemd
는mysql.service
도 함께 시작하려고 시도합니다. 하지만mysql.service
가 시작에 실패하더라도my-app.service
는 그대로 시작됩니다. 만약 반드시 의존하는 서비스가 성공적으로 시작되어야 한다면Wants
대신Requires=mysql.service
를 사용합니다. 이 경우 MySQL 시작에 실패하면 우리 앱도 시작되지 않습니다.
[Service] 섹션 상세 분석 (가장 중요!)
User=ubuntu
/Group=ubuntu
: 보안상 매우 중요한 설정입니다. 서비스를root
사용자로 실행하는 것은 심각한 보안 위협이 될 수 있습니다. 애플리케이션에 취약점이 있을 경우, 공격자가 서버의 전체 권한을 탈취할 수 있습니다. 따라서 반드시 애플리케이션 실행에 필요한 최소한의 권한을 가진 일반 사용자(여기서는ubuntu
)로 실행하도록 지정해야 합니다.WorkingDirectory=/home/ubuntu
:ExecStart
명령어가 실행될 작업 디렉토리를 지정합니다. 만약 애플리케이션 내부에서 로그 파일 생성 등 상대 경로를 사용하는 코드가 있다면, 이 설정이 기준 경로가 됩니다..jar
파일이 위치한 디렉토리로 설정하는 것이 일반적입니다.Environment=...
: 서비스 프로세스에 주입할 환경 변수를 설정합니다. Spring Boot에서는application.properties
나application.yml
보다 환경 변수가 우선순위가 높습니다. 따라서 프로덕션 환경에서 민감한 정보(DB 비밀번호 등)나 환경별 설정(prod
,dev
프로필)을 코드와 분리하여 관리할 때 매우 유용합니다. 위 예제에서는 Spring Boot의 활성 프로필을prod
로 설정하고, DB 비밀번호를 환경 변수로 전달하고 있습니다. 여러 개를 설정할 수 있습니다.ExecStart=/usr/bin/java -jar ...
: 서비스를 시작할 때 실행될 실제 명령어입니다./usr/bin/java
:java
만 쓰는 것보다 전체 경로를 명시하는 것이 더 안정적입니다.which java
명령어로 경로를 확인할 수 있습니다.-Duser.timezone=Asia/Seoul
: JVM 시스템 프로퍼티를 설정하는 부분입니다. 서버의 기본 시간대와 관계없이 애플리케이션의 시간대를 서울로 고정하여 시간 관련 버그를 예방할 수 있습니다./home/ubuntu/my-app-0.0.1.jar
: 실행할.jar
파일의 절대 경로입니다.
SuccessExitStatus=143
: Spring Boot 애플리케이션은systemctl stop
과 같은SIGTERM
신호를 받으면 정상 종료(graceful shutdown)를 수행하고 종료 코드 143으로 프로세스를 마칩니다.systemd
는 기본적으로 종료 코드 0만 성공으로 간주하기 때문에, 이 설정을 추가하지 않으면 정상적인 종료도 실패로 간주할 수 있습니다. 따라서 Spring Boot 앱을 서비스로 등록할 때는 이 설정을 추가해주는 것이 좋습니다.TimeoutStopSec=10
:systemctl stop
명령 시,SIGTERM
신호를 보내고 나서 애플리케이션이 종료될 때까지 기다리는 시간을 초 단위로 지정합니다. Spring Boot의 정상 종료(graceful shutdown) 과정에서 처리 중이던 요청을 완료하는 데 시간이 필요할 수 있으므로, 적절한 대기 시간을 부여합니다. 이 시간이 지나도 종료되지 않으면systemd
는 강제 종료(SIGKILL
) 신호를 보냅니다.Restart=on-failure
: 자동 재시작의 핵심입니다. 이 지시어는 서비스가 '실패'로 간주되는 상황에서 자동으로 재시작하도록 만듭니다. '실패'란 0 또는SuccessExitStatus
에 지정된 코드 이외의 코드로 종료되거나, 특정 신호에 의해 비정상적으로 종료되는 경우를 말합니다. 즉, OOM(OutOfMemoryError) 등으로 인해 JVM이 죽으면systemd
가 이를 감지하고 다시 실행시켜 줍니다.always
: 실패 여부와 관계없이, 어떤 이유로든 프로세스가 종료되면 무조건 재시작합니다. (정상적인 stop 명령에도 재시작되므로 주의)on-abnormal
,on-watchdog
등 다른 옵션도 있지만,on-failure
가 가장 일반적으로 사용됩니다.
RestartSec=5
: 재시작하기 전에 5초를 대기합니다. 만약 설정 파일 오류 등으로 인해 앱이 시작하자마자 계속 죽는다면, 이 설정이 없다면 1초에도 수십 번씩 재시작을 시도하여 시스템에 엄청난 부하를 줄 수 있습니다. 이를 방지하기 위한 안전장치입니다.
[Install] 섹션 상세 분석
WantedBy=multi-user.target
:systemctl enable my-app.service
명령을 실행했을 때, 이 서비스의 심볼릭 링크를/etc/systemd/system/multi-user.target.wants/
디렉토리 아래에 생성하라는 의미입니다.multi-user.target
은 시스템이 부팅되어 네트워크는 연결되었지만 그래픽 인터페이스는 없는, 일반적인 서버 환경의 상태를 의미합니다. 즉, 이 설정은 "서버가 정상적으로 부팅되면 이 서비스를 자동으로 시작해 주세요"라는 약속과 같습니다.
이렇게 상세한 설정을 통해 우리는 단순한 자동 실행을 넘어, 안정적이고 예측 가능한 방식으로 서비스를 운영할 수 있는 기반을 마련했습니다.
3. 서비스 제어 및 관리: Systemctl 명령어 마스터하기
이제 훌륭한 서비스 파일을 작성했으니, systemctl
명령어를 사용하여 우리의 새로운 서비스를 시스템에 등록하고 생명을 불어넣을 차례입니다. systemctl
은 systemd
를 제어하는 핵심 도구입니다.
Step 1: Systemd 데몬 리로드
새로운 서비스 파일을 생성했거나 기존 파일을 수정한 후에는, systemd
가 변경 사항을 인지할 수 있도록 반드시 데몬을 리로드해야 합니다.
sudo systemctl daemon-reload
Step 2: 서비스 활성화 (부팅 시 자동 시작 설정)
아래 명령은 [Install]
섹션의 WantedBy
설정에 따라 서비스를 부팅 시 자동으로 시작되도록 활성화합니다. 이 명령을 실행하면 위에서 설명한 심볼릭 링크가 생성됩니다.
sudo systemctl enable my-app.service
(출력 예시: Created symlink /etc/systemd/system/multi-user.target.wants/my-app.service → /etc/systemd/system/my-app.service.
)
반대로 자동 시작을 비활성화하고 싶다면 disable
명령을 사용하면 됩니다.
sudo systemctl disable my-app.service
Step 3: 서비스 시작, 중지, 재시작
이제 서비스를 수동으로 제어해 봅시다.
서비스 시작:
sudo systemctl start my-app.service
서비스 중지:
sudo systemctl stop my-app.service
서비스 재시작 (중지 후 다시 시작):
sudo systemctl restart my-app.service
Step 4: 서비스 상태 확인 (가장 중요!)
서비스가 잘 실행되고 있는지, 오류는 없는지 확인하는 것은 운영의 기본입니다. status
명령은 서비스에 대한 모든 정보를 종합적으로 보여주는 매우 유용한 명령어입니다.
sudo systemctl status my-app.service
정상적으로 실행 중이라면 다음과 유사한 출력을 보게 될 것입니다.
● my-app.service - My Awesome Spring Boot Application
Loaded: loaded (/etc/systemd/system/my-app.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2023-10-25 10:30:00 KST; 5min ago
Docs: https://my-docs.example.com
Main PID: 12345 (java)
Tasks: 45 (limit: 4096)
Memory: 512.8M
CPU: 45.123s
CGroup: /system.slice/my-app.service
└─12345 /usr/bin/java -jar -Duser.timezone=Asia/Seoul /home/ubuntu/my-app-0.0.1.jar
Oct 25 10:30:00 ip-172-31-10-20.ap-northeast-2.compute.internal systemd[1]: Started My Awesome Spring Boot Application.
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: . ____ _ __ _ _
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: \\/ ___)| |_)| | | | | || (_| | ) ) ) )
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: ' |____| .__|_| |_|_| |_\__, | / / / /
Oct 25 10:30:05 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: =========|_|==============|___/=/_/_/_/
Oct 25 10:30:06 ip-172-31-10-20.ap-northeast-2.compute.internal java[12345]: :: Spring Boot :: (v3.1.5)
... (애플리케이션 시작 로그) ...
여기서 주목해야 할 부분은 다음과 같습니다.
Loaded
: 서비스 파일이 정상적으로 로드되었고,enabled
(자동 시작 활성화) 상태임을 보여줍니다.Active: active (running)
: 현재 서비스가 활성화되어 실행 중임을 나타내는 가장 중요한 지표입니다. 만약 문제가 있다면inactive (dead)
또는failed
상태로 표시됩니다.Main PID
: 실행 중인 Java 프로세스의 ID입니다.- 로그 부분: 하단에는 애플리케이션의 가장 최근 로그 몇 줄이 표시되어, 시작 시 오류가 있었는지 빠르게 파악할 수 있습니다.
4. 강력한 로깅 시스템 활용: journalctl 로 디버깅하기
systemd
의 가장 강력한 기능 중 하나는 Journal이라는 중앙화된 로깅 시스템입니다. 우리가 서비스로 등록한 애플리케이션의 모든 표준 출력(System.out.println
)과 표준 에러(e.printStackTrace()
)는 별도의 설정 없이 자동으로 Journal에 기록됩니다. 더 이상 nohup.out
파일을 찾아 헤매거나 복잡한 로그 파일 설정을 할 필요가 없습니다.
journalctl
명령어를 사용하면 이 로그를 매우 효율적으로 조회하고 필터링할 수 있습니다.
특정 서비스의 전체 로그 보기:
sudo journalctl -u my-app.service
-u
옵션은 'unit'의 약자로, 특정 유닛의 로그만 필터링합니다.
실시간으로 로그 스트리밍 (tail -f
와 동일):
개발 및 디버깅 시 가장 많이 사용하게 될 명령어입니다. 실시간으로 발생하는 로그를 계속해서 화면에 출력해 줍니다.
sudo journalctl -u my-app.service -f
최근 N개의 로그만 보기:
예를 들어 최근 100줄의 로그만 보고 싶을 때 사용합니다.
sudo journalctl -u my-app.service -n 100
특정 시간 이후의 로그 보기:
장애 분석 시 매우 유용합니다. 어제 발생한 문제를 분석하고 싶다면 다음과 같이 사용할 수 있습니다.
# 어제부터 발생한 로그
sudo journalctl -u my-app.service --since "yesterday"
# 2023년 10월 25일 10시부터 11시까지의 로그
sudo journalctl -u my-app.service --since "2023-10-25 10:00:00" --until "2023-10-25 11:00:00"
이처럼 journalctl
을 활용하면, 문제 발생 시 원인을 추적하고 해결하는 시간을 획기적으로 단축할 수 있습니다.
5. 최종 점검 및 결론
모든 설정을 마쳤다면, 마지막으로 실제 상황을 시뮬레이션하여 우리의 설정이 완벽하게 동작하는지 확인해 봅시다.
- 서비스 실행:
sudo systemctl start my-app.service
로 서비스를 시작하고status
로 정상 실행을 확인합니다. - 프로세스 강제 종료:
status
에서 확인한 PID를 사용하여 프로세스를 강제로 죽여봅니다. (sudo kill -9 [PID]
). - 자동 재시작 확인: 잠시 후(
RestartSec
에 설정한 시간 이후) 다시sudo systemctl status my-app.service
를 실행해 보세요.Active
상태가active (running)
으로 다시 돌아오고,Main PID
가 새로운 값으로 변경되었다면 자동 재시작 기능이 성공적으로 동작하는 것입니다. 로그에도 재시작된 기록이 남아있을 것입니다. - 서버 재부팅:
sudo reboot
명령으로 EC2 인스턴스를 재부팅합니다. - 부팅 후 자동 실행 확인: 재부팅이 완료된 후 다시 서버에 접속하여
systemctl status my-app.service
를 실행합니다. 서비스가 별도의 조작 없이 자동으로 실행되어active (running)
상태라면, 부팅 시 자동 실행 설정까지 완벽하게 완료된 것입니다.
축하합니다! 이제 여러분의 Java/Spring Boot 애플리케이션은 단순한 프로세스가 아닌, 시스템이 직접 관리하고 보호하는 견고한 '서비스'로 거듭났습니다. 처음에는 systemd
의 설정이 다소 복잡하고 생소하게 느껴질 수 있지만, 한번 제대로 구축해두면 비교할 수 없는 안정성과 관리의 편의성을 제공합니다.
nohup
의 시대를 끝내고, systemd
를 통해 여러분의 소중한 서비스를 진정한 프로덕션 수준으로 한 단계 업그레이드하시길 바랍니다. 이제 서버가 다운되어도, 애플리케이션에 예기치 못한 오류가 발생해도, systemd
가 든든한 수호신처럼 여러분의 서비스를 지켜줄 것입니다.
0 개의 댓글:
Post a Comment