
Spring Boot 애플리케이션을 서버에 배포하려면 단순히 JAR 파일을 실행하는 것만으로는 부족하다. 서버 접속부터 파일 관리, 프로세스와 서비스 운영, 네트워크, 로그 확인, 롤백까지 전체 흐름을 이해해야 한다.
이번 글에서는 리눅스 서버에 애플리케이션을 배포할 때 필요한 기본 개념과 명령어를 정리해 본다.
일반적인 서버 배포는 다음과 같은 순서로 진행된다.
1. 서버에 SSH로 접속
2. Java, Node.js 같은 런타임 설치
3. 애플리케이션 파일 또는 Docker 이미지 전달
4. 환경 변수와 설정 파일 구성
5. 데이터베이스 마이그레이션
6. 애플리케이션 실행
7. Nginx 리버스 프록시 설정
8. HTTPS 인증서 설정
9. 로그와 상태 확인
10. 문제가 생기면 이전 버전으로 롤백
예를 들어 Spring Boot 애플리케이션이라면 파일을 다음과 같이 구성할 수 있다.
/opt/myapp/
├── app.jar
├── application-prod.yml
└── logs/
애플리케이션 프로세스는 systemd가 관리하고, Nginx가 외부 요청을 애플리케이션에 전달하는 형태다.
사용자 브라우저
│
│ https://api.example.com
▼
DNS
도메인 이름을 서버 IP 주소로 변환
│
▼
서버 방화벽
443 포트 요청 허용 여부 확인
│
▼
Nginx
HTTPS 처리 및 요청 전달
│
│ http://127.0.0.1:8080
▼
Spring Boot 애플리케이션
│
▼
데이터베이스 · Redis · 외부 API
각 요소의 역할을 간단히 정리하면 다음과 같다.
배포 과정에서 자주 사용하는 명령어부터 살펴보자.
pwd # 현재 위치 확인
ls -al # 파일 목록 확인
cd /var/www/myapp # 디렉터리 이동
mkdir app # 디렉터리 생성
cp source target # 파일 복사
mv old new # 파일 이동 또는 이름 변경
rm file # 파일 삭제
cat app.log # 파일 전체 내용 출력
less app.log # 긴 파일을 페이지 단위로 확인
tail -f app.log # 실시간 로그 확인
grep "ERROR" app.log # 문자열 검색
find . -name "*.log" # 파일 검색
특히 cd, ls, cp, mv, less, tail, grep, find는 배포와 장애 대응 과정에서 자주 사용한다.
rm -rf는 삭제한 내용을 되돌리기 어려우므로 매우 조심해서 사용해야 한다.
리눅스 터미널은 항상 어떤 디렉터리를 기준으로 동작한다.
pwd는 print working directory의 약자로, 현재 작업 중인 디렉터리를 출력한다.
루트 디렉터리인 /부터 시작하는 전체 경로다. 현재 어느 디렉터리에 있든 항상 같은 파일을 가리킨다.
현재 위치를 기준으로 나타내는 경로다.
. : 현재 디렉터리
.. : 상위 디렉터리
~ : 현재 사용자의 홈 디렉터리
/ : 파일 시스템의 최상위 디렉터리
리눅스에서 자주 접하는 디렉터리는 다음과 같다.
| 경로 | 용도 |
|---|---|
/home | 일반 사용자의 홈 디렉터리 |
/root | root 사용자의 홈 디렉터리 |
/etc | 시스템과 서비스의 설정 파일 |
/var/log | 시스템과 일부 서비스의 로그 |
/opt | 별도로 설치한 애플리케이션 |
/usr/bin | 일반적인 실행 명령어 |
/tmp | 임시 파일 |
/run | 실행 중인 서비스의 임시 상태 정보 |
/var/lib | 서비스가 사용하는 영속 데이터 |
/var/www는 정적 웹 파일을 둘 때 많이 사용한다. 독립적인 백엔드 애플리케이션은/opt나/srv에 두기도 한다.
ls: 파일 목록 확인ls -l: 권한, 소유자, 그룹, 크기, 수정 시간 등 상세 정보 표시ls -a: .으로 시작하는 숨김 파일까지 표시ls -alh: 숨김 파일과 상세 정보를 함께 보여 주고 파일 크기를 KB, MB처럼 읽기 쉽게 표시a: 숨김 파일 포함l: 상세 정보 표시h: 사람이 읽기 쉬운 단위로 파일 크기 표시mkdir: 디렉터리 생성mkdir releases
여러 단계의 디렉터리를 한 번에 만들 때는 -p를 사용한다.
mkdir -p /opt/myapp/releases
-p가 없으면 중간 디렉터리가 존재하지 않을 때 오류가 발생한다.
cp: 복사cp app.jar app-backup.jar
디렉터리 전체를 복사하려면 -r 옵션이 필요하다.
cp -r config config-backup
기존 파일과 같은 이름으로 복사하면 덮어쓸 수 있으므로 운영 서버에서는 대상 경로를 먼저 확인해야 한다.
mv: 이동 또는 이름 변경파일 이름을 변경할 수 있다.
mv app-new.jar app.jar
파일을 다른 디렉터리로 이동할 때도 사용한다.
mv 역시 대상 위치에 같은 이름의 파일이 있으면 덮어쓸 수 있다. 운영 환경에서는 파일 이름에 버전을 포함하는 방식을 많이 사용한다.
app-1.0.0.jar
app-1.0.1.jar
이전 버전 파일이 남아 있으므로 문제가 생겼을 때 롤백하기 쉬워진다.
rm: 삭제디렉터리와 내부 파일을 재귀적으로 강제 삭제하는 다음 명령은 매우 위험하다.
rm -rf 디렉터리
-r: 내부 디렉터리와 파일까지 재귀적으로 처리-f: 별도의 확인 없이 강제로 처리리눅스 터미널에서 삭제한 파일은 일반적으로 휴지통으로 가지 않는다. 특히 관리자 권한과 함께 잘못 사용하면 시스템 전체를 손상시킬 수 있다.
삭제 전에는 대상 경로를 확인하는 습관을 들이는 것이 좋다.
pwd
ls -al 삭제하려는경로
운영 배포에서는 오래된 파일을 즉시 삭제하기보다 별도의 백업 디렉터리로 옮긴 뒤 보존 기간에 따라 정리하는 편이 안전하다.
cat: 파일 전체 내용을 한 번에 출력cat application.yml
짧은 설정 파일을 확인하기에는 좋지만 로그처럼 큰 파일에 사용하면 화면이 빠르게 지나간다.
또한 비밀번호나 API 키가 포함된 파일을 화면에 출력하면 터미널 기록이나 화면 공유를 통해 노출될 수 있으므로 주의해야 한다.
less: 긴 파일을 페이지 단위로 확인less application.log
less 내부에서 자주 사용하는 키는 다음과 같다.
/ERROR: ERROR 검색n: 다음 검색 결과N: 이전 검색 결과q: 종료head와 tail: 파일의 앞부분과 마지막 부분 확인# 파일 앞부분 확인
head application.log
head -n 50 application.log
# 파일 마지막 부분 확인
tail application.log
tail -n 100 application.log
# 실시간으로 추가되는 로그 확인
tail -f application.log
tail -f를 종료할 때는 Ctrl + C를 누른다.
grep: 파일 내용에서 특정 문자열 검색# ERROR 검색
grep "ERROR" application.log
# 대소문자를 구분하지 않고 검색
grep -i "error" application.log
# 줄 번호 포함
grep -n "ERROR" application.log
# 디렉터리 내부를 재귀적으로 검색
grep -R "database.password" /opt/myapp/config
프로세스를 확인할 때 파이프(|)와 함께 자주 사용하기도 한다.
ps aux | grep java
파이프는 왼쪽 명령의 출력을 오른쪽 명령의 입력으로 전달한다. 위 명령에서는 ps aux의 전체 출력 가운데 java가 포함된 줄만 선택한다.
단, grep java 명령 자체도 결과에 나타날 수 있다. 다음처럼 작성하면 그 문제를 피할 수 있다.
ps aux | grep '[j]ava'
find: 이름이나 조건으로 실제 파일 찾기# 특정 경로에서 로그 파일 검색
find /opt/myapp -name "*.log"
# 현재 디렉터리부터 검색
find . -name "application*.yml"
# 파일만 검색
find . -type f -name "*.jar"
# 디렉터리만 검색
find . -type d -name "logs"
grep과 find의 차이find: 파일과 디렉터리 자체를 찾는다.grep: 파일 내용에서 문자열을 찾는다.디스크에 저장된 실행 가능한 파일이다.
프로그램이 실제로 실행 중인 상태다. 프로세스마다 PID라는 고유한 번호가 있다.
ps aux
출력 예시는 다음과 같다.
appuser 2145 2.3 8.1 ... java -jar /opt/myapp/app.jar
여기서 2145가 PID다.
운영체제가 지속적으로 관리하는 백그라운드 프로그램이다. 애플리케이션을 서비스로 등록하면 시작, 종료, 재시작, 부팅 시 자동 실행 등을 일관된 방식으로 관리할 수 있다.
ps: 현재 실행 중인 프로세스 확인ps aux
# 특정 프로세스 검색
ps aux | grep '[j]ava'
# PID를 더 간단하게 검색
pgrep -af java
pgrep 옵션의 의미는 다음과 같다.
-a: 전체 명령어 표시-f: 실행 명령 전체를 대상으로 검색top: CPU와 메모리 사용량 실시간 확인top
주로 다음 항목을 확인한다.
q를 누르면 종료된다.
kill: 프로세스에 시그널 보내기kill은 무조건 프로세스를 강제 종료하는 명령이 아니라 프로세스에 시그널을 보내는 명령이다.
kill -15 2145
15, 즉 SIGTERM은 정상 종료를 요청한다. 애플리케이션은 SIGTERM을 받으면 다음 작업을 수행할 기회를 가질 수 있다.
반면 다음 명령은 SIGKILL을 보낸다.
kill -9 2145
SIGKILL은 애플리케이션이 정리할 기회를 주지 않고 운영체제가 즉시 종료한다. 따라서 보통 다음 순서를 따른다.
SIGTERM으로 정상 종료 요청SIGKILL 고려서비스로 등록된 애플리케이션이라면 PID를 직접 종료하기보다
sudo systemctl stop myapp을 우선 사용한다. systemd에 정의된 종료 절차와 제한 시간을 적용할 수 있기 때문이다.
systemd는 많은 리눅스 배포판에서 서비스의 시작, 종료, 자동 실행, 재시작, 로그 등을 관리한다.
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
sudo systemctl status myapp
# 서버 부팅 시 자동 시작
sudo systemctl enable myapp
# 자동 시작 해제
sudo systemctl disable myapp
# 서비스 파일 수정 후 systemd가 다시 읽게 하기
sudo systemctl daemon-reload
서비스 설정을 수정한 뒤에는 일반적으로 다음 순서로 확인한다.
서비스 설정 파일 수정
→ systemctl daemon-reload
→ systemctl restart myapp
→ systemctl status myapp
→ 로그 확인
[Unit]
Description=My Spring Boot Application
After=network.target
[Service]
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
EnvironmentFile=/etc/myapp/myapp.env
Restart=on-failure
RestartSec=5
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
[Unit]서비스 자체의 설명과 실행 순서를 정의한다.
Description=My Spring Boot Application
After=network.target
After=network.target은 네트워크 기본 구성이 준비된 뒤 실행 순서를 잡겠다는 의미다. 이것만으로 데이터베이스에 실제로 연결할 수 있는 상태까지 보장되는 것은 아니다.
[Service]애플리케이션의 실제 실행 방법을 정의한다.
User=appuser
Group=appgroup
애플리케이션을 root가 아닌 appuser 권한으로 실행한다.
WorkingDirectory=/opt/myapp
애플리케이션의 작업 디렉터리를 지정한다.
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
실제로 실행할 명령이다. systemd에서는 실행 파일의 절대 경로를 사용하는 것이 명확하다.
EnvironmentFile=/etc/myapp/myapp.env
환경 변수가 담긴 파일을 읽는다.
Restart=on-failure
RestartSec=5
비정상적으로 종료되면 5초 뒤 재시작한다. 설정 오류나 DB 연결 오류로 애플리케이션이 계속 실패하면 무한 재시작처럼 보일 수 있으므로 반드시 로그를 확인해야 한다.
[Install]부팅 과정의 어느 단계에서 서비스를 실행할지 정의한다.
WantedBy=multi-user.target
일반적인 서버 운영 단계에서 서비스가 실행되도록 설정한다.
journalctl: systemd 서비스 로그 확인systemd로 실행한 서비스의 표준 출력과 표준 오류는 보통 journal에 기록된다.
# 전체 로그
journalctl -u myapp
# 실시간 로그
journalctl -u myapp -f
# 최근 100줄
journalctl -u myapp -n 100
# 최근 10분
journalctl -u myapp --since "10 minutes ago"
# 오늘 로그
journalctl -u myapp --since today
# 최신 로그부터 보기
journalctl -u myapp -r
애플리케이션이 시작되지 않는다면 다음 두 명령을 먼저 확인한다.
systemctl status myapp
journalctl -u myapp -n 100
systemctl status는 현재 상태를 요약하고, journalctl은 자세한 실행 기록을 보여 준다.
IP 주소는 네트워크에서 서버를 식별하는 주소다.
127.0.0.1: 자기 자신을 가리키는 루프백 주소포트는 한 서버에서 여러 프로그램을 구분하기 위한 번호다.
22 SSH
80 HTTP
443 HTTPS
3306 MySQL
5432 PostgreSQL
6379 Redis
8080 애플리케이션에서 자주 사용하는 포트
현재 열려 있는 listening 포트는 다음 명령으로 확인한다.
ss -lntp
l: 연결을 기다리는 listening 소켓n: 포트 번호를 숫자로 표시t: TCP 소켓p: 해당 포트를 사용하는 프로세스 표시curl서버에서 HTTP 요청을 직접 보내는 도구다.
# 애플리케이션 내부 접근 확인
curl http://127.0.0.1:8080
# 헬스 체크 API가 있다면
curl http://127.0.0.1:8080/health
# 응답 헤더만 확인
curl -I https://example.com
# 상세 통신 과정 확인
curl -v https://example.com
curl 결과를 이용하면 문제가 애플리케이션, Nginx, DNS, TLS 중 어느 범위에 있는지 좁힐 수 있다.
DNS는 도메인 이름을 IP 주소로 바꾸어 준다.
api.example.com
│ DNS 조회
▼
203.0.113.10
DNS를 변경해도 전 세계에 즉시 반영되지 않을 수 있다. 기존 결과가 일정 시간 캐시에 남아 있을 수 있기 때문이다.
DNS가 잘못 설정되어 있으면 애플리케이션과 Nginx가 모두 정상이어도 사용자는 다른 서버에 접속할 수 있다.
클라이언트와 서버가 요청과 응답을 주고받는 규칙이다.
HTTP 통신을 TLS로 암호화한 것이다.
HTTPS를 사용하려면 도메인에 맞는 TLS 인증서가 필요하다.
사용자 ↔ Nginx: HTTPS
Nginx ↔ 같은 서버의 애플리케이션: HTTP
| 상태 코드 | 일반적인 의미 |
|---|---|
200 | 정상 응답 |
301, 302 | 다른 주소로 이동 |
400 | 요청 형식이 잘못됨 |
401 | 인증이 필요하거나 인증 실패 |
403 | 접근 권한 없음 |
404 | 해당 경로 없음 |
500 | 애플리케이션 내부 오류 |
502 | 프록시가 백엔드 애플리케이션과 정상적으로 연결하지 못함 |
503 | 서비스를 사용할 수 없음 |
504 | 백엔드 응답 시간 초과 |
상태 코드별로 우선 확인할 부분은 다음과 같다.
404: Nginx 경로 또는 애플리케이션 라우팅 확인500: 애플리케이션 로그 확인502: 애플리케이션 실행 여부와 포트 확인504: 느린 쿼리, 외부 API, 타임아웃 확인SSH는 원격 서버에 암호화된 연결로 접속하는 방법이다.
ssh deploy@server-ip
# 개인 키를 지정한다면
ssh -i company-server-key.pem deploy@server-ip
각 요소의 의미는 다음과 같다.
deploy: 서버의 사용자 이름server-ip: 접속할 서버 주소-i: 사용할 개인 키 파일 지정SSH 키 인증은 한 쌍의 키를 사용한다.
개인 키는 Git 저장소에 커밋하거나 이메일·메신저로 공유하면 안 된다. 사람마다 별도의 키를 사용하고, 가능하면 서버에서 root 직접 로그인을 제한하는 편이 안전하다.
앞에서 배운 내용을 실제 Spring Boot 배포 순서로 연결해 보자. 다만 회사마다 인프라와 배포 도구가 다르므로, 운영 서버에서 처음 보는 명령을 바로 실행하기보다 기존 배포 문서와 스크립트를 먼저 확인해야 한다.
배포 전에는 다음 정보를 확인한다.
서버 주소
SSH 사용자
배포 경로
애플리케이션 실행 사용자
서비스 이름
사용 포트
로그 위치
설정 파일 위치
배포 전 백업 방법
DB 마이그레이션 여부
롤백 방법
Spring Boot 예시는 다음과 같다.
./gradlew clean build
결과 파일은 보통 다음 경로에 생성된다.
build/libs/myapp.jar
실무에서는 개발자 PC보다 CI 서버에서 빌드하는 편이 재현성과 보안 측면에서 좋다.
scp build/libs/myapp.jar deploy@server:/opt/myapp/releases/myapp-1.2.3.jar
파일을 버전별로 보관하면 기존 버전을 덮어쓰지 않으므로 롤백하기 쉽다.
ls -l /opt/myapp/releases/
ls -l /etc/myapp/myapp.env
애플리케이션을 실행하는 사용자가 필요한 파일을 읽을 수 있는지 확인한다.
스키마 변경이 있다면 Flyway나 Liquibase 같은 도구를 사용할 수 있다.
DB 마이그레이션은 특히 주의해야 한다. 애플리케이션만 이전 버전으로 되돌려도 DB 구조가 이미 변경되었다면 완전한 롤백이 불가능할 수 있기 때문이다.
가능하면 다음처럼 구버전과 신버전이 함께 호환되는 순서로 변경한다.
1. 기존 코드와 새 코드가 함께 사용할 수 있는 컬럼 추가
2. 새 코드 배포
3. 데이터 이전
4. 충분히 검증한 뒤 오래된 컬럼 제거
sudo systemctl restart myapp
재시작 직후에는 반드시 상태와 로그를 확인한다.
sudo systemctl status myapp
journalctl -u myapp -n 100
curl http://127.0.0.1:8080/health
curl -I https://api.example.com
홈페이지만 열리는지 확인하는 데서 끝내지 말고 핵심 기능도 확인해야 한다.
배포 직후 일정 시간 동안 다음 항목을 확인한다.
롤백은 이전 애플리케이션 파일을 다시 실행하는 것만으로 끝나지 않을 수 있다.
확인해야 할 항목은 다음과 같다.
롤백 방법은 장애가 발생한 뒤 만드는 것이 아니라 배포 전에 준비해야 한다.
단순 재시작 방식에서는 기존 애플리케이션이 종료되고 새 애플리케이션이 완전히 시작될 때까지 요청을 처리할 수 없다.
기존 애플리케이션 종료
↓
새 애플리케이션 시작
↓
시작 완료까지 서비스 중단
무중단 배포에서는 보통 두 개 이상의 애플리케이션 인스턴스를 둔다.
┌─ 애플리케이션 A
사용자 ─ Nginx
└─ 애플리케이션 B
배포 순서는 다음과 같다.
이를 구현하는 방법에는 다음과 같은 것들이 있다.
처음부터 무중단 배포를 구현하기보다, 먼저 수동 배포와 롤백 원리를 정확히 이해하는 것이 좋다.
리눅스 배포에서 중요한 것은 명령어를 외우는 것만이 아니다. 요청이 DNS, 방화벽, Nginx, 애플리케이션을 거쳐 데이터베이스까지 어떻게 이동하는지 이해해야 문제가 생겼을 때 원인을 빠르게 좁힐 수 있다.
처음에는 다음 세 가지 흐름부터 익혀 두면 좋다.
systemctl status와 journalctl로 애플리케이션 상태와 로그 확인하기ss와 curl로 포트와 요청 경로 확인하기이 기본 흐름을 이해한 뒤 CI/CD, Docker, 무중단 배포로 범위를 넓혀 가면 배포 과정이 훨씬 선명하게 보일 것이다.