실행 중인 프로그램 하나가 프로세스이고, 커널이 번호(PID)를 붙입니다. 모든 프로세스는 부모(PPID)가 있고, 부모가 죽으면 자식은 PID 1 인 systemd 에게 입양됩니다.
| 변수·명령 | 뜻 |
|---|---|
$ |
현재 셸의 PID |
$! |
직전 백그라운드 PID |
$PPID |
부모 PID |
ps -ef |
전체 프로세스 목록 |
ps -p PID -o ... |
특정 프로세스의 지정 열 |
자바 앱을 띄운 셸이 부모이고 java 가 자식입니다. 셸이 닫히면 자식은 HUP 신호를 받아 함께 죽는 것이 기본 동작입니다.
ps -ef | grep java 가 가장 흔한 조합입니다. grep 자기 자신도 목록에 나오므로 grep -v grep 을 붙이거나 pgrep 을 씁니다.
ps -ef | grep '[j]ava' # [j] 트릭: grep 자신은 제외
pgrep -f 'app.jar' # 명령줄 전체에서 찾아 PID 만
jps -l # JDK 도구: 자바 프로세스만 클래스·jar 이름과 함께jps 는 JDK 에 들어 있습니다. 같은 서버에 자바 앱이 여러 개면 ps 보다 jps -l 이 한눈에 들어옵니다.
kill 은 프로세스에 신호를 보내는 명령입니다. 기본 신호 TERM 은 "정리하고 종료해라" 라는 요청이고, 프로세스가 무시할 수도 있습니다.
| 신호 | 번호 | 뜻 | 자바 앱 반응 |
|---|---|---|---|
| TERM | 15 | 종료 요청 | shutdown hook 실행 |
| KILL | 9 | 즉시 강제 종료 | 정리 없이 사라짐 |
| HUP | 1 | 터미널 끊김 | 기본은 종료 |
| INT | 2 | Ctrl+C | TERM 과 비슷 |
| 0 | - | 생존 확인만 | 신호 안 보냄 |
kill -9 는 마지막 수단입니다. Spring 의 @PreDestroy, DB 커넥션 반납, 처리 중인 요청 마무리가 모두 건너뛰어집니다. 먼저 kill PID 를 보내고 30초쯤 기다린 뒤에도 안 죽을 때만 씁니다.
프로세스가 신호로 죽으면 종료 코드는 128 + 신호 번호 입니다. 로그나 systemctl status 에서 이 숫자를 보면 무슨 일이 있었는지 바로 압니다.
| 종료 코드 | 뜻 |
|---|---|
| 0 | 정상 종료 |
| 1 | 앱이 스스로 에러 종료 |
| 137 | 128+9, KILL 로 죽음 (OOM killer 흔함) |
| 143 | 128+15, TERM 으로 죽음 (정상 정지) |
자바 앱이 TERM 을 받아 정상 정지하면 143 을 돌려줍니다. systemd 는 0 이 아닌 종료를 실패로 보므로 유닛 파일에 SuccessExitStatus=143 을 적어 줍니다.
명령 끝에 & 를 붙이면 백그라운드로 돌고 즉시 프롬프트가 돌아옵니다. 그러나 터미널을 닫으면 HUP 신호로 함께 죽습니다.
nohup java -jar app.jar > app.out 2>&1 & # HUP 무시, 출력은 파일로
jobs # 현재 셸의 백그라운드 작업
disown # 이미 띄운 작업을 셸에서 떼기nohup 은 임시 실행에는 충분하지만 재부팅 시 자동 시작이 없고 재시작도 안 됩니다. 운영 서비스는 반드시 systemd 로 올립니다.
systemd 는 PID 1 로 부팅 직후 뜨는 관리자입니다. 서비스 하나가 유닛 파일 하나이고, 위치는 /etc/systemd/system/이름.service 입니다.
유닛 파일은 세 섹션으로 나뉩니다. [Unit] 은 설명과 순서, [Service] 는 실행 방법, [Install] 은 부팅 시 소속입니다.
[Unit]
Description=MyApp Spring Boot service
After=network-online.target
[Service]
User=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/opt/myapp/conf/env
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/myapp/current/app.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetExecStart 는 절대 경로여야 하고 셸 문법(&&, |, >)을 쓸 수 없습니다. 복잡하면 실행 스크립트를 만들어 그 경로를 적습니다. 04 레슨에서 그 스크립트를 만듭니다.
| 키 | 뜻 | 권장 값 |
|---|---|---|
Type |
시작 방식 | simple |
User Group |
실행 계정 | 전용 계정 |
EnvironmentFile |
환경 변수 파일 | - 붙여 없어도 통과 |
Restart |
재시작 조건 | on-failure |
RestartSec |
재시작 대기 | 5 |
TimeoutStopSec |
정지 대기 후 KILL | 30 |
LimitNOFILE |
열 수 있는 파일 수 | 65536 |
Type=simple 은 ExecStart 프로세스가 곧 서비스라는 뜻으로, 자바 앱에 맞습니다. LimitNOFILE 은 커넥션이 많은 앱에서 "Too many open files" 를 막습니다. 08 장애 대응에서 다시 봅니다.
유닛 파일을 만들거나 고친 뒤에는 반드시 daemon-reload 를 먼저 합니다. 이걸 빼면 systemd 가 옛 내용을 그대로 씁니다.
| 명령 | 뜻 |
|---|---|
systemctl daemon-reload |
유닛 파일 다시 읽기 |
systemctl start myapp |
지금 시작 |
systemctl stop myapp |
지금 정지 |
systemctl restart myapp |
정지 후 시작 |
systemctl status myapp |
상태·PID·최근 로그 |
systemctl enable myapp |
부팅 시 자동 시작 |
systemctl enable --now myapp |
enable + start |
systemctl is-active myapp |
스크립트용 상태 확인 |
start 와 enable 은 다릅니다. start 만 하면 재부팅 뒤 안 뜨고, enable 만 하면 지금은 안 뜹니다. 둘 다 필요하면 enable --now 로 한 번에 합니다.
systemd 로 띄운 서비스의 표준 출력은 저널에 모입니다. 파일을 찾을 필요 없이 journalctl -u 서비스 로 봅니다.
| 명령 | 뜻 |
|---|---|
journalctl -u myapp |
서비스 로그 전체 |
journalctl -u myapp -f |
실시간 따라가기 |
journalctl -u myapp -n 100 |
최근 100줄 |
journalctl -u myapp --since '1 hour ago' |
시간 범위 |
journalctl -u myapp -p err |
에러 이상만 |
02 레슨의 grep 을 그대로 붙일 수 있습니다. journalctl -u myapp --since today | grep -c ERROR 가 오늘 에러 수입니다.
프로세스가 살아 있어도 CPU 나 메모리를 다 쓰고 있으면 장애입니다. 세 명령으로 현재 상태를 봅니다.
top -b -n 1 | head -n 15 # 한 화면만 찍고 종료, CPU·MEM 상위
uptime # 부하 평균 1·5·15분, 코어 수보다 크면 과부하
free -m # 메모리 MB, available 열을 본다top 을 대화형으로 열면 P 는 CPU 순, M 은 메모리 순 정렬, q 로 나옵니다. top -p PID 로 자바 프로세스 하나만 볼 수도 있습니다.
systemctl stop myapp 을 치면 다음 순서로 진행됩니다. 자바 앱이 이 흐름에 맞게 동작하는지가 무중단 배포의 전제입니다.
systemd 가 TERM 을 보냅니다.@PreDestroy 를 호출하고 커넥션 풀을 닫습니다.TimeoutStopSec 안에 안 끝나면 KILL 을 보냅니다.Spring Boot 는 server.shutdown=graceful 로 완전 종료를 켜면 처리 중인 요청을 마친 뒤 종료합니다. 이때 spring.lifecycle.timeout-per-shutdown-phase 가 TimeoutStopSec 보다 짧아야 KILL 을 피합니다.
앱은 root 가 아닌 전용 계정으로 띄웁니다. 계정을 만들고 배포 폴더 소유권을 주는 것이 유닛 파일 작성 전 준비입니다.
sudo useradd -r -s /sbin/nologin myapp # 로그인 불가 시스템 계정
sudo mkdir -p /opt/myapp /var/log/myapp
sudo chown -R myapp:myapp /opt/myapp /var/log/myapp-s /sbin/nologin 은 이 계정으로 ssh 접속을 막습니다. 앱이 뚫려도 공격자가 얻는 권한이 이 계정 범위로 제한됩니다.