본문으로 건너뛰기

Spring Boot 운영 가이드 (PostgreSQL 17 · MariaDB 11.4)

OpenJDK 21 + Spring Boot 3.5 + Gradle 8 + systemd + 내장 Tomcat 환경에서 PostgreSQL 17 또는 MariaDB 11.4를 사용하는 운영 매뉴얼.

💡 요약 정리

  • 런타임: OpenJDK 21 + Spring Boot 3.5.0 + Gradle 8
  • DB: PostgreSQL 17 또는 MariaDB 11.4 선택
  • 프로세스 매니저: systemd / 리버스 프록시: nginx 1.24
  • 앱 listen: 127.0.0.1:8080 (외부 직접 접근 차단)
  • DEV A(2GB)에서는 빌드 OOM 가능 → DEV B(4GB) 이상 권장

1. 환경 매니페스트

항목
OSUbuntu 24.04 LTS
언어 / 런타임OpenJDK 21
프레임워크Spring Boot 3.5.0 (내장 Tomcat)
빌드 도구Gradle 8 (wrapper)
프로세스 매니저systemd
앱 listen127.0.0.1:8080
리버스 프록시nginx 1.24 (:80/:443, Let's Encrypt 자동)
앱 디렉토리/opt/[프로젝트]
환경변수/etc/[프로젝트]/env
로그/var/log/[프로젝트]
서비스 단위[프로젝트].service
주 코드/opt/[프로젝트]/src/main/java/com/cafe24/[프로젝트]/
빌드 산출물/opt/[프로젝트]/build/libs/*.jar

⚠️ 메모리 주의: Spring Boot는 JVM 기본 사용량이 큽니다. **DEV A (2GB)**에서는 빌드 OOM 가능. DEV B (4GB) 이상 권장.

DB 매니페스트

항목PostgreSQL 17MariaDB 11.4
인증peer auth (sudo psql)unix_socket auth (sudo mariadb)
포트54323306
서비스명postgresql@17-mainmariadb
JDBC URLjdbc:postgresql://127.0.0.1:5432/[DB명]jdbc:mariadb://127.0.0.1:3306/[DB명]
Driverorg.postgresql:postgresqlorg.mariadb.jdbc:mariadb-java-client

2. 서버 접속

2-1. SSH (root)

ssh root@[아이디].mycafe24.com
PROJECT_NAME=myapp; export PROJECT_NAME

2-2. SFTP / rsync

# 빌드 산출물(JAR)만 업로드 권장
rsync -avz ./build/libs/$PROJECT_NAME-*.jar root@[아이디].mycafe24.com:/opt/$PROJECT_NAME/build/libs/

# 소스 통째 업로드 시 (build/, .gradle/ 제외 필수)
rsync -avz --exclude='build/' --exclude='.gradle/' --exclude='.idea/' \
  ./ root@[아이디].mycafe24.com:/opt/$PROJECT_NAME/
ssh root@... "chown -R appuser:appuser /opt/$PROJECT_NAME"

2-3. appuser로 작업 (디버깅)

sudo -u appuser bash
cd /opt/$PROJECT_NAME
./gradlew bootRun --no-daemon         # 빌드+실행 동시 (수동 디버깅)
jcmd $(pgrep -f $PROJECT_NAME) GC.heap_info

3. 환경 확인

ls -la /opt/$PROJECT_NAME
ls -la /opt/$PROJECT_NAME/build/libs/
sudo cat /etc/$PROJECT_NAME/env

sudo systemctl is-active $PROJECT_NAME
sudo journalctl -u $PROJECT_NAME -n 100      # JVM 시작 로그 (30~60초 소요)
ss -tlnp | grep 8080

curl -sf http://127.0.0.1:8080/actuator/health

# JVM 진단
ps aux | grep java
sudo -u appuser jcmd <PID> GC.heap_info       # 힙 사용량
sudo -u appuser jstat -gc <PID>                # GC 통계

4. DB 직접 접속

4-1. PostgreSQL 17

sudo -u postgres psql
\l
\du
\c [DB명]
\dt
\q

4-1'. MariaDB 11.4

sudo mariadb
SHOW DATABASES;
USE [DB명];
SHOW TABLES;
EXIT;

4-2. 앱 사용자로 접속

# PG
psql "postgresql://[DB사용자]:[비밀번호]@127.0.0.1:5432/[DB명]"

# MariaDB
mariadb -u [DB사용자] -p [DB명]

4-3. 샘플 DDL

JPA/Hibernate가 자동 생성. 수동 점검:

PG:

SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';

MariaDB:

SHOW TABLES;

5. 코드 배포 워크플로우

표준 시퀀스

# 1. 빌드 (서버에서) — 메모리 주의
cd /opt/$PROJECT_NAME
sudo -u appuser ./gradlew clean build -x test

# 2. 재시작 (JAR 자동 갱신)
sudo systemctl restart $PROJECT_NAME

# 3. 검증 (시작에 30~60초 소요)
sleep 30
curl -sf http://127.0.0.1:8080/actuator/health
sudo journalctl -u $PROJECT_NAME -n 50

5-A. ⚠️ 배포 시 위험 회피 (★ 가장 중요)

자체 코드를 통째로 업로드하면 자동 구성된 DB·보안·systemd 설정이 사라져 서비스가 깨질 수 있습니다.

위험 5종 매트릭스

#위험증상Spring Boot 안전 패턴
1DB 환경변수 참조 깨짐HTTP 200은 떠도 모든 DB 쿼리 실패 (Connection refused / Auth failed)application.properties에서 ${DB_URL}, ${DB_USER}, ${DB_PASSWORD} 사용
2외부 IP에 listen외부에서 :8080 직접 접속 가능 → nginx 우회 → fail2ban·rate limit 무력화server.address=127.0.0.1 명시
3엔트리 포인트 변경systemd가 시작 시 JAR 못 찾음 ("could not find")build/libs/*.jar 위치/이름 유지 (systemd unit 참조)
4의존성 동기화 누락ClassNotFoundException / NoClassDefFoundError./gradlew clean build -x test 실행
5systemd unit 덮어씀env 변수 주입 안 됨 → DB 연결 실패[프로젝트].service 절대 업로드 대상 포함 금지

안전한 application.properties

# /opt/[프로젝트]/src/main/resources/application.properties
spring.application.name=[프로젝트]

# Server
server.address=127.0.0.1
server.port=8080

# DataSource (env에서 주입)
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}

# PG 사용 시
spring.datasource.driver-class-name=org.postgresql.Driver
# MariaDB 사용 시
# spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# MariaDB: org.hibernate.dialect.MariaDBDialect

# Actuator
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=when-authorized

env 파일 예시

# /etc/[프로젝트]/env
DB_URL=jdbc:postgresql://127.0.0.1:5432/[DB명]
# MariaDB: jdbc:mariadb://127.0.0.1:3306/[DB명]
DB_USER=[DB사용자]
DB_PASSWORD=[비밀번호]
JAVA_OPTS=-Xms256m -Xmx768m -XX:+UseG1GC
SPRING_PROFILES_ACTIVE=prod

절대 업로드 금지 파일

다음 파일은 절대 업로드 대상에 포함하지 마십시오:
  • /etc/systemd/system/[프로젝트].service
  • /etc/[프로젝트]/env

3가지 배포 방법 (안전 순서)

방법설명적합
방법 1 — JAR만 업로드로컬 빌드 후 build/libs/*.jar만 (가장 안전, 메모리 부담 X)일상 배포, DEV A 권장
방법 2 — 소스 통째 업로드 + 서버 빌드build/, .gradle/ 제외 필수, 빌드 시 1.5GB+ 메모리 필요큰 변경, DEV B 이상 권장
방법 3 — 외부 설정 분리DB 등은 env 파일만, 코드는 자유권장 베이스

⚠️ DEV A(2GB)에서 서버 빌드 시 OOM 가능성 매우 높음로컬 빌드 후 JAR 업로드 강력 권장 (공통 운영 가이드 §8 swap 참고).

배포 후 4가지 체크

sudo systemctl is-active $PROJECT_NAME                # 1. active
ss -tlnp | grep 8080 | grep '127.0.0.1'               # 2. 127.0.0.1 listen
curl -sf http://127.0.0.1:8080/actuator/health        # 3. 헬스 OK
curl -sI https://[도메인]/                            # 4. 외부 응답

5-B. Spring Boot 배포 7단계

# 1. 로컬 빌드 후 JAR만 업로드 (가장 안전, 메모리 부담 X)
./gradlew clean build -x test
rsync -avz ./build/libs/$PROJECT_NAME-*.jar root@[아이디].mycafe24.com:/opt/$PROJECT_NAME/build/libs/

# 또는 서버에서 빌드 (DEV B 이상 권장)
ssh root@... "cd /opt/$PROJECT_NAME && sudo -u appuser ./gradlew clean build -x test"

# 2. 권한
ssh root@... "chown -R appuser:appuser /opt/$PROJECT_NAME"

# 3. (의존성 변경 시) ./gradlew clean build

# 4. 재시작
ssh root@... "systemctl restart $PROJECT_NAME"

# 5. 시작 로그 확인 (30~60초)
ssh root@... "journalctl -u $PROJECT_NAME -f"

# 6. 헬스 체크
curl -sf http://127.0.0.1:8080/actuator/health

# 7. 외부
curl -sI https://[도메인]/

자주 만나는 실수

실수결과회복
빌드 중 OOM (DEV A)Exception: Java heap spaceswap 추가 또는 DEV B 이상으로 변경, 또는 로컬 빌드 후 JAR만 업로드
application.properties에 평문 비밀번호보안 위반${DB_PASSWORD} 환경변수 참조로 변경, env 파일 갱신
JAR 파일명 변경systemd가 시작 실패systemd unit의 ExecStart와 일치 확인
server.address 미설정외부 노출 위험server.address=127.0.0.1 추가

5-C. 환영 페이지 끄고 내 코드 띄우기 (필독)

설치 직후 도메인으로 접속하면 "서버가 정상 동작 중입니다" 환영 페이지가 표시됩니다. 이는 Nginx가 /var/www/cafe24-welcome/index.html을 우선 서빙하기 때문입니다. 본인 코드의 / 라우트가 보이게 하려면 이 환영 파일 1개만 정리하면 됩니다.

5-C-1. 환영 페이지 끄기

방법명령
방법 A — 삭제sudo rm /var/www/cafe24-welcome/index.html
방법 B — 백업 후 비활성 (원복 가능)sudo mv /var/www/cafe24-welcome/index.html /var/www/cafe24-welcome/index.html.bak
현재 상태 확인ls /var/www/cafe24-welcome/

5-C-2. 확인

도메인 재접속 또는 curl -sI https://[도메인]/ 결과로 본인 앱이 응답하는지 확인합니다. 캐시가 남으면 시크릿 창 또는 강제 새로고침을 권장합니다.

Tip: 환영 파일을 백업해두면 트러블슈팅 시 "Nginx는 살아있나" 확인용으로 다시 활성화할 수 있습니다.


6. DB 연동 실전 코드

Entity

// src/main/java/com/cafe24/[프로젝트]/Item.java
package com.cafe24.[프로젝트];

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "items")
public class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(columnDefinition = "TEXT")        // PG: jsonb로 변경 가능 / Maria: JSON
    private String payload;

    @Column(name = "created_at")
    private LocalDateTime createdAt = LocalDateTime.now();

    // getters/setters
}

Repository

// src/main/java/com/cafe24/[프로젝트]/ItemRepository.java
package com.cafe24.[프로젝트];

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface ItemRepository extends JpaRepository<Item, Long> {
    List<Item> findTop100ByOrderByCreatedAtDesc();
}

Controller (CRUD)

@RestController
@RequestMapping("/api/items")
public class ItemController {
    private final ItemRepository repo;
    public ItemController(ItemRepository repo) { this.repo = repo; }

    @GetMapping
    public List<Item> list() {
        return repo.findTop100ByOrderByCreatedAtDesc();
    }

    @PostMapping
    public Item create(@RequestBody Item body) {
        return repo.save(body);
    }
}

7. 정적 파일 / 미디어

Spring Boot는 src/main/resources/static/을 자동 서빙. 더 큰 정적 파일은 nginx에 위임:

location /static/ {
    alias /opt/[프로젝트]/static/;
    expires 7d;
}

8. 로그 / 모니터링

sudo journalctl -u $PROJECT_NAME -f
sudo journalctl -u $PROJECT_NAME -p err

sudo tail -f /var/log/nginx/[프로젝트]_access.log
sudo tail -f /var/log/nginx/[프로젝트]_error.log

Actuator로 메트릭

curl http://127.0.0.1:8080/actuator/health
curl http://127.0.0.1:8080/actuator/info
curl http://127.0.0.1:8080/actuator/metrics                # 메트릭 목록
curl http://127.0.0.1:8080/actuator/metrics/jvm.memory.used

Actuator 외부 노출 시 nginx에서 /actuator/ 차단 또는 Basic Auth 권장.


9. HTTPS / 도메인

(공통)

sudo certbot --nginx -d example.com -d www.example.com
sudo systemctl status certbot.timer

10. 트러블슈팅 + FAQ

트러블슈팅 매트릭스

증상원인해결
빌드 중 OOMDEV A 메모리 부족swap 추가 또는 DEV B+ / 로컬 빌드 후 JAR 업로드
HTTP 502JVM 다운 또는 시작 중systemctl status $PROJECT_NAME (시작 30~60초 소요)
Connection refused (DB)DB 미기동systemctl status postgresql@17-main 또는 mariadb
ClassNotFoundException의존성 미빌드./gradlew clean build
Failed to determine driver classdriver 미명시application.propertiesspring.datasource.driver-class-name
Tomcat thread pool 고갈트래픽 과다server.tomcat.threads.max 증가 + DEV C/D 검토
Actuator 외부 접근 가능nginx 노출/actuator/ 차단 또는 Basic Auth

FAQ

Q. 빌드 OOM 발생

A. 두 가지 옵션:

  1. 로컬 빌드 후 JAR만 업로드 (DEV A 유지 가능)
  2. 서버에서 빌드하려면 DEV B 이상 또는 swap 추가:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Q. JVM 메모리 옵션 어디서 설정

A. /etc/[프로젝트]/envJAVA_OPTS:

JAVA_OPTS=-Xms256m -Xmx768m -XX:+UseG1GC

변경 후 sudo systemctl restart $PROJECT_NAME.

Q. profile 분리 (dev/prod)

A. SPRING_PROFILES_ACTIVE=prod (env), 파일은 application-prod.properties 추가.

Q. application.properties 변경했는데 반영 안 돼요

A. JAR 안에 패키징된 properties는 빌드 시 고정. 외부 override:

# /etc/[프로젝트]/env에 직접
SERVER_PORT=8080
SPRING_DATASOURCE_URL=jdbc:postgresql://...

또는 --spring.config.location=file:/etc/[프로젝트]/application.properties.


11. 내장 Tomcat 운영 (Spring Boot 전용)

다른 SET에는 없는 Spring Boot 전용 섹션입니다.

11-1. application.properties Tomcat 옵션

# 스레드 풀
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=10

# 커넥션
server.tomcat.connection-timeout=20000
server.tomcat.max-connections=10000
server.tomcat.accept-count=100

# 압축
server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,application/json
server.compression.min-response-size=1024

# 정적 파일 캐시
spring.web.resources.cache.cachecontrol.max-age=7d

11-2. 부하별 권장 튜닝

트래픽threads.maxmax-connections권장 요금제
가벼움 (~100 req/min)505000DEV A·B
중간 (~1000 req/min)20010000DEV B·C
큰 부하 (~5000 req/min)40020000DEV C·D

JVM 메모리 (Xmx)도 비례 조정 필요.

11-3. WebSocket

내장 Tomcat이 자동 지원. 별도 설정 없이 @MessageMapping 사용 가능.

11-4. 정적 리소스 처리

src/main/resources/static/ → 자동 서빙. nginx에 위임 시 빠름.

11-5. Connector 모드 (NIO / APR)

기본 NIO. APR 사용 시 tomcat-native 라이브러리 추가 필요 (대부분 NIO 충분).

11-6. HTTP + HTTPS 동시 listen

server.port=8443
# 추가 HTTP 포트는 Connector 빈으로 설정 (Spring Boot 코드)

11-7. Actuator로 Tomcat 메트릭

curl http://127.0.0.1:8080/actuator/metrics/tomcat.threads.busy
curl http://127.0.0.1:8080/actuator/metrics/tomcat.global.request

11-8. Tomcat 트러블슈팅

증상메트릭조치
응답 느림tomcat.threads.busy 가 max 근접threads.max 증가, 슬로우 쿼리 점검
Connection 실패tomcat.global.received 급감max-connections 또는 OS ulimit -n 점검

11-9. Tomcat 버전 / 의존성

Spring Boot 3.5에 내장된 Tomcat은 10.x. 직접 버전 변경은 권장하지 않음.


12. 추가 운영 / 커스터마이징

공통 운영·커스터마이징 가이드 참조.

FAQ · 트러블슈팅.

추가 문의 사항이 있으시면 1:1 문의게시판을 통해 문의해 주세요.