일반적으로 Java 어플리케이션은 시스템이 부팅된 후 콘솔로 접근해서 직접 실행하는 관행이있습니다.
Java 어플리케이션은 보안문제로 root로 실행하지 않는 것이 관례이고 시스템이 모두 부팅 된 후에 일반 user 계정으로 실행하는 것이 대부분이지만, 필요에 따라 부팅후에 바로 어플리케이션이 실행되어야 하는 경우도 있습니다.
테스트 환경
- System : 라즈베리파이 4B (Ubuntu Bookworm)
- Display : 라즈베리파이 7인치 터치 디스플레이
- API : Spring Boot 3.4.5 (Java 21)
- UI : Nginx, React 19.1.0, Vite 6.3.5
- DB : MariaDB 10.11.11
1. Spring Boot 실행
디렉토리 구조는 아래와 같습니다.
/
├── usr/
│ ├── local/
│ │ ├── apiserver/
│ │ ├── arcade/
│ │ │ ├── app.sh # Spring Boot 실행 스크립트
│ │ │ ├── app.pid # Spring Boot 프로세스ID가 들어있는 파일 (자동 생성)
│ │ │ ├── ArcadeKioskApi.jar # Spring Boot jar
│ │ └── log/
│ │ ├── ArcadeKioskApi.log # Application log
│ │ └── ...
│ └── ...
└── ...
Spring boot 실행 계정은 basscraft 입니다.
실행 스크립트를 작성합니다.
$ vi /usr/local/apiserver/arcade/app.sh
#!/bin/bash
APP_NAME="ArcadeKioskApi.jar"
PID_FILE="app.pid"
LOG_FILE="/usr/local/apiserver/arcade/log/ArcadeKiosk.log"
JAVA_OPTS="-Xms512m -Xmx1024m"
PROFILE="dev"
start_app() {
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "이미 실행 중입니다. PID: $(cat $PID_FILE)"
exit 1
fi
echo "애플리케이션 시작 중..."
nohup java $JAVA_OPTS -jar $APP_NAME --spring.profiles.active=$PROFILE >> "$LOG_FILE" 2>&1 &
echo $! > $PID_FILE
echo "시작 완료. PID: $(cat $PID_FILE)"
}
stop_app() {
if [ ! -f "$PID_FILE" ]; then
echo "PID 파일이 없습니다. 앱이 실행 중이지 않을 수 있습니다."
exit 1
fi
PID=$(cat $PID_FILE)
if kill -0 $PID 2>/dev/null; then
echo "애플리케이션 종료 중... PID: $PID"
kill $PID
rm -f $PID_FILE
echo "종료 완료."
else
echo "PID $PID 프로세스가 존재하지 않습니다."
rm -f $PID_FILE
fi
}
case "$1" in
start)
start_app
;;
stop)
stop_app
;;
*)
echo "사용법: $0 {start|stop}"
exit 1
;;
esac
스크립트에 실행 권한을 부여합니다.
$ chmod +x /usr/local/apiserver/arcade/app.sh
$ ls -al
total 67616
drwxr-xr-x 3 basscraft basscraft 4096 Jun 3 13:48 .
drwxr-xr-x 3 basscraft basscraft 4096 Jun 2 11:25 ..
-rw-r--r-- 1 basscraft basscraft 5 Jun 3 13:48 app.pid
-rwxr-xr-x 1 basscraft basscraft 1201 Jun 3 13:40 app.sh
-rw-r--r-- 1 basscraft basscraft 69205366 Jun 3 13:48 ArcadeKioskApi.jar
drwxr-xr-x 2 basscraft basscraft 4096 Jun 3 12:29 log
$
app.sh start
app.sh stop
명령으로 정상적으로 실행, 종료, 로그가 잘 남는다는 전제하에 작업을 진행합니다.
유닛 파일 작성
$ sudo vi /etc/systemd/system/arcade-api.service
[Unit]
Description=Arcade API Spring Boot App
After=network.target
[Service]
Type=forking # nohup java ... & 처럼 백그라운드로 실행된는 경우 PIDFile에 프로세스 아이디 저장됨
User=basscraft # 어플리케이션을 실행할 user
ExecStart=/usr/local/apiserver/arcade/app.sh start
ExecStop=/usr/local/apiserver/arcade/app.sh stop
PIDFile=/usr/local/apiserver/arcade/app.pid
WorkingDirectory=/usr/local/apiserver/arcade
Restart=on-failure # 비정상적으로 종료된 경우만 재시작
RestartSec=5s # 재시작 전에 5초 대기
[Install]
WantedBy=multi-user.target # run level 3
systemd 바이러니 재시작 (가급적 사용자제, demon-reload 로 해결 되는 경우가 대부분)
$ sudo systemctl daemon-reexec
유닛파일 로딩
$ sudo systemctl daemon-reload
부팅시 자동 실행 등록
$ sudo systemctl enable arcade-api.service
즉시 실행
$ sudo systemctl start arcade.service
데몬 상태 확인
$ sudo systemctl status arcade-api.service
● arcade-api.service - Arcade API Spring Boot App
Loaded: loaded (/etc/systemd/system/arcade-api.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-06-03 13:48:47 KST; 1h 5min ago
Process: 2419 ExecStart=/usr/local/apiserver/arcade/app.sh start (code=exited, status=0/SUCCESS)
Main PID: 2420 (java)
Tasks: 43 (limit: 3929)
CPU: 2min 8.842s
CGroup: /system.slice/arcade-api.service
└─2420 java -Xms256m -Xmx512m -jar ArcadeKioskApi.jar --spring.profiles.active=dev
Jun 03 13:48:47 raspberrypi systemd[1]: Starting arcade-api.service - Arcade API Spring Boot App...
Jun 03 13:48:47 raspberrypi app.sh[2419]: 애플리케이션 시작 중...
Jun 03 13:48:47 raspberrypi app.sh[2419]: 시작 완료. PID: 2420
Jun 03 13:48:47 raspberrypi systemd[1]: Started arcade-api.service - Arcade API Spring Boot App.
$
Java 프로세스 확인
$ ps -ef | grep java
basscra+ 2420 1 3 13:48 ? 00:02:08 java -Xms256m -Xmx512m -jar ArcadeKioskApi.jar --spring.profiles.active=dev
basscra+ 2647 2343 0 14:55 pts/1 00:00:00 grep --color=auto java
$
로그 확인
$ cat /usr/local/apiserver/arcade/log/ArcadeKiosk.log
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.5)
[2025-06-03 13:48:52.374] [background-preinit] [INFO ] [o.h.validator.internal.util.Version:21] ### HV000001: Hibernate Validator 8.0.2.Final
[2025-06-03 13:48:52.794] [main] [INFO ] [k.g.a.ArcadeKioskApplication:53] ### Starting ArcadeKioskApplication v0.0.1-SNAPSHOT using Java 21.0.6 with PID 2420 (/usr/local/apiserver/arcade/ArcadeKioskApi.jar started by basscraft in /usr/local/apiserver/arcade)
[2025-06-03 13:48:53.017] [main] [DEBUG] [k.g.a.ArcadeKioskApplication:54] ### Running with Spring Boot v3.4.5, Spring v6.2.6
... 생략 ...
$
여기까지 확인 되었으면 정상.
재부팅 후 데몬 상태, Java 프로세스, 로그 확인 하여 정상인지 확인
가동 중 Spring Boot 앱만 종료, 시작 해야 하는경우는 app.sh 를 직접 실행 하거나 systemctl 을 통해서 등록한 서비스를 중지, 시작 해도 됩니다. (restart는 넣지 않았습니다.)
app.sh 직접 실행
$ cd /usr/local/apiserver/arcade/
$ sh app.sh start
애플리케이션 시작 중...
시작 완료. PID: 1234
$ tail -f log/ArcadeKiosk.log
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.5)
[2025-06-04 12:42:00.881] [background-preinit] [INFO ] [o.h.validator.internal.util.Version:21] ### HV000001: Hibernate Validator 8.0.2.Final
[2025-06-04 12:42:02.036] [main] [INFO ] [k.g.a.ArcadeKioskApplication:53] ### Starting ArcadeKioskApplication v0.0.1-SNAPSHOT using Java 21.0.6 with PID 647 (/usr/local/apiserver/arcade/ArcadeKioskApi.jar started by basscraft in /usr/local/apiserver/arcade)
[2025-06-04 12:42:02.378] [main] [DEBUG] [k.g.a.ArcadeKioskApplication:54] ### Running with Spring Boot v3.4.5, Spring v6.2.6
[2025-06-04 12:42:02.407] [main] [INFO ] [k.g.a.ArcadeKioskApplication:658] ### The following 1 profile is active: "dev"
[2025-06-04 12:42:12.457] [main] [INFO ] [o.s.d.r.c.RepositoryConfigurationDelegate:143] ### Bootstrapping Spring Data JPA repositories in DEFAULT mode.
[2025-06-04 12:42:13.657] [main] [INFO ] [o.s.d.r.c.RepositoryConfigurationDelegate:211] ### Finished Spring Data repository scanning in 1124 ms. Found 3 JPA repository interfaces.
[2025-06-04 12:42:34.259] [main] [INFO ] [o.s.b.w.e.tomcat.TomcatWebServer:111] ### Tomcat initialized with port 8080 (http)
...생략...
systemctl을 이용한 실행
$ sudo systemctl start arcade-api.service
$ sudo systemctl stop arcade-api.service
2. React App 자동 실행
kiosk app의 실행 스크립트 작성
당연히 로컬 브라우져에서 http://localhost[:port] 로 정상 접근이 되어야 합니다.
$ sudo vi /usr/local/share/kiosk/kiosk-start.sh
#!/bin/bash
/usr/bin/chromium-browser --kiosk http://localhost --no-sandbox --disable-gpu --disable-software-rasterizer
cromium 브라우저를 실행하면서 --kiosk 옵션을 주면 주소창, 상태창 모두 없앨 수 있습니다.
참고1. --no-sandbox --disable-gpu --disable-software-rasterizer 옵션을 생략 하는 경우 아래 에러가 발생함
Jun 03 15:56:38 raspberrypi kiosk-start.sh[1720]: close object 1: Invalid argument
Jun 03 15:56:38 raspberrypi kiosk-start.sh[1720]: close object 2: Invalid argument
Jun 03 15:56:38 raspberrypi kiosk-start.sh[1720]: close object 3: Invalid argument
Jun 03 15:56:40 raspberrypi kiosk-start.sh[1720]: close object 1: Invalid argument
Jun 03 15:56:40 raspberrypi kiosk-start.sh[1720]: close object 3: Invalid argument
Jun 03 15:56:40 raspberrypi kiosk-start.sh[1720]: close object 4: Invalid argument
위 에러 관련하여 ChatGPT 권고는 아래와 같습니다.

앱 동작에는 문제가 없지만 불필요한 로그를 줄이기 위해서 권고대로 최종 수정했습니다.
#!/bin/bash
#/usr/bin/chromium-browser --kiosk http://localhost --no-sandbox --disable-gpu --disable-software-rasterizer > /dev/null 2>&1
/usr/bin/chromium-browser --kiosk http://localhost \
--no-sandbox --disable-gpu --disable-software-rasterizer \
--disable-crash-reporter \
--disable-logging \
--log-level=3
스크립트에 실행 권한 부여
$ sudo chmod +x /usr/local/share/kiosk/kiosk-start.sh
$ ls -al /usr/local/share/kiosk/kiosk-start.sh
-rwxr-xr-x 1 root root 63 Jun 2 22:10 /usr/local/share/kiosk/kiosk-start.sh
$
웹서버의 경우 80포트로 접근가능해야 하기 때문에 실행 권한이 root 인경우가 많습니다. 파일의 소유자는 root여도 관계 없습니다.
해당 스크립트 실행 시 앱 화면만 있는 상태의 브라우져가 실행 되어야 합니다.
(종료는 ctrl+w 입니다)
유닛파일 생성
$ sudo vi /etc/systemd/system/kiosk.service
[Unit]
Description=Kiosk Mode Launch
#After=graphical.target # 부팅 후 실행 까지 너무 오래 걸려서 multi-user.target 으로 수정
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/share/kiosk/kiosk-start.sh
User=basscraft
Group=basscraft
Environment=DISPLAY=:0
Environment=XDG_RUNTIME_DIR=/run/user/1000 # basscraft 계정의 uid=1000
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus # basscraft 계정의 uid=1000
#Restart=always # 이 옵션이 있는 경우 브라우져가 종료 되도 다시 실행 됨
[Install]
#WantedBy=graphical.target # 부팅 후 실행 까지 너무 오래 걸려서 multi-user.target 으로 수정
WantedBy=multi-user.target
참고 1. 최초 실행 시점을 graphical.target 으로 설정 했으나 부팅 후 너무 오랜 시간이 걸려서 multi-user.target 시점으로 조정
참고 2. 상태 확인시 아래 에러가 발생하여
ERROR:dbus/object_proxy.cc(590)] Failed to call method: org.freedesktop.DBus...
ERROR:dbus: Could not parse server address: Unknown address type
아래 옵션을 추가 하였습니다.
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
1000은 basscraft 계정의 uid로 ($ id -u basscraft) 명령으로 확인 가능합니다.
유닛 파일 로딩
$ sudo systemctl daemon-reload
부팅시 자동 실행 등록
$ sudo systemctl enable kiosk.service
즉시 실행
$ sudo systemctl start kiosk.service
상태 확인
$ sudo systemctl status kiosk.service
● kiosk.service - Kiosk Mode Launch
Loaded: loaded (/etc/systemd/system/kiosk.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-06-03 15:43:59 KST; 24s ago
Main PID: 2652 (kiosk-start.sh)
Tasks: 44 (limit: 3929)
CPU: 3.123s
CGroup: /system.slice/kiosk.service
├─2652 /bin/bash /usr/local/share/kiosk/kiosk-start.sh
├─2671 /usr/lib/chromium/chrome_crashpad_handler --monitor-self --monitor-self-annotation=ptype=crashpad-handler "--database=/home/basscraft/.config/chromium/Crash Reports" "--annotation=chann>
├─2673 /usr/lib/chromium/chrome_crashpad_handler --no-periodic-tasks --monitor-self-annotation=ptype=crashpad-handler "--database=/home/basscraft/.config/chromium/Crash Reports" "--annotation=>
├─2676 "/usr/lib/chromium/chromium --type=zygote --no-zygote-sandbox --no-sandbox --crashpad-handler-pid=0 --enable-crash-reporter=,built on Debian GNU/Linux 12 (bookworm) --change-stack-guard>
├─2677 "/usr/lib/chromium/chromium --type=zygote --no-sandbox --crashpad-handler-pid=0 --enable-crash-reporter=,built on Debian GNU/Linux 12 (bookworm) --change-stack-guard-on-fork=enable"
├─2708 "/usr/lib/chromium/chromium --type=gpu-process --no-sandbox --disable-dev-shm-usage --enable-gpu-rasterization --ozone-platform=wayland --use-angle=gles --crashpad-handler-pid=0 --enabl>
├─2710 "/usr/lib/chromium/chromium --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=gles>
├─2717 "/usr/lib/chromium/chromium --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-sandbox-type=utility --no-sandbox --disable-dev-shm-usage --use-angle=g>
├─2733 "/usr/lib/chromium/chromium --type=renderer --crashpad-handler-pid=0 --enable-crash-reporter=,built on Debian GNU/Linux 12 (bookworm) --change-stack-guard-on-fork=enable --no-sandbox -->
└─2734 "/usr/lib/chromium/chromium --type=renderer --crashpad-handler-pid=0 --enable-crash-reporter=,built on Debian GNU/Linux 12 (bookworm) --change-stack-guard-on-fork=enable --no-sandbox -->
Jun 03 15:43:59 raspberrypi systemd[1]: Started kiosk.service - Kiosk Mode Launch.
재부팅 후에 잘 실행 됩니다.
끝.
추가.
API 서버와 브라우저가 모두
[Unit]
... 생략...
After=network-online.target
... 생략...
[Install]
WantedBy=multi-user.target
으로 설정 되어 있어서 API 서버가 완전히 실행 되기 전에 브라우저가 실행 되고 API를 호출 하지 못해서 에러가 발생할 수 있는 상황,
ChatGPT 도움을 받아서 kiosk.service 실행 수정
$ sudo vi /etc/systemd/system/kiosk.service
[Unit]
Description=Kiosk Mode Launch
After=arcade-api.service network-online.target # arcade-api.service 가 실행 된 후 실행 되도록 종속성 추가
Wants=network-online.target
Requires=arcade-api.service # arcade-api.service 가 실행되지 않으면 실행하지 않음
[Service]
ExecStart=/usr/local/share/kiosk/kiosk-start.sh
User=basscraft
Group=basscraft
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=DISPLAY=:0
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable kiosk.service
Created symlink /etc/systemd/system/multi-user.target.wants/kiosk.service → /etc/systemd/system/kiosk.service.
$ sudo reboot
기대 했던 것과 다르게 여전히 API가 실행 완료 되기 전에 브라우저가 실행되어서 여전히 API 오류를 보임
원인은 API 실행 시
$ nohup java ... &
처럼 백그라운드로 실행하기 때문에 service 입장에서는 API를 실행만 할 뿐 정상적으로 준비 상태인지 알 수가 없음
브라우저를 실행 시켜 주는 kiosk-start.sh에 API 동작을 확인하는 스크립트를 추가
$ sudo vi /usr/local/share/kiosk/kiosk-start.sh
#!/bin/bash
# 1. API 기동 대기
echo "Waiting for arcade-api..."
until curl -sf http://localhost:8080/v1/arcade/health; do
sleep 2
done
/usr/bin/chromium-browser --kiosk http://localhost \
--no-sandbox --disable-gpu --disable-software-rasterizer \
--disable-crash-reporter \
--disable-logging \
--log-level=3
API 서버에 앤드 포인트를 하나 추가 해줌
@RequestMapping("/v1/arcade")
@RequiredArgsConstructor
public class KioskController {
private final KioskService kioskService;
@GetMapping("/health")
public ResponseEntity<?> getHealth() {
return ResponseEntity
.ok()
.body(ApplicationResponse.ok());
}
... 생략 ...
브라우저가 조금 늦게 뜨는 경향은 있지만 오류는 발생하지 않음
향후 키오스크 기기를 라즈베리파이 보다 성능이 좋은 것으로 교체 하게 되면 큰 문제는 없을 듯
참고1.
Requires=arcade-api.service 옵션을 추가 하는 경우 arcade-api.service가 종료 되면 브라우져도 자동 종료 됨
(여기서는 제거 해야 할 듯)
'Java' 카테고리의 다른 글
[Springboot] 실행시 properties 주입 방법 (1) | 2024.01.03 |
---|---|
log4j 취약점(CVE-2021-44228) 점검 방법 - jar파일 스캔 (1) | 2021.12.13 |
주민번호 규칙 (2) | 2019.11.26 |
Spring Polymorphism과 Factory Pattern에 대한 고찰 (1) | 2019.04.19 |
[서블릿 개발하기] #7 Servlet 들여다 보기 (0) | 2019.04.17 |