plantpulse-server (웹 콘솔 + IIoT 엔진)
역할
플랫폼의 얼굴이자 두뇌에 해당하는 핵심 모듈입니다. 운영자가 보는 웹 콘솔, 외부 시스템이 호출하는 REST API, 그리고 산업 데이터를 실제로 처리하는 IIoT 엔진이 모두 이 한 모듈 안에서 동작합니다.
| 항목 | 값 |
|---|---|
| 모듈명 | plantpulse-server |
| 컨테이너 | plantpulse-server-web (자기 이미지, 자기 컨테이너) |
| 컨테이너 안 경로 | /opt/kopens/plantpulse-platform/plantpulse-server/ |
| 사용자 접속 | 80 / 443 — plantpulse-proxy 가 받아 이 컨테이너로 넘깁니다. 관리 콘솔도 같은 주소입니다 |
| 호스트 포트 | 없음 — 프록시 뒤에서만 동작합니다 |
| 메모리 한계 | DOCKER_SERVER_MEMORY 기본 14g / 힙 DOCKER_SERVER_HEAP 기본 8g |
| 런타임 | Apache Tomcat 10.1 + Java 21 |
| 처리량 | 초당 40,000건 메시지 (36스레드 파이프라인) |
사용자 트래픽은 항상 plantpulse-proxy(80/443)를 거칩니다. 그래서 백엔드가 옮겨가거나 이름이 바뀌어도 사용자·설비 설정을 건드릴 필요가 없습니다. 컨테이너 상태는 bin/status.sh, 로그는 bin/logs.sh plantpulse-server-web 으로 봅니다.
책임 영역
디렉토리 구조
plantpulse-server/
├── config/ # 서버 관리 설정 (외부화 — 배포해도 유지)
│ ├── plantpulse-engine.properties # IIoT 엔진 튜닝
│ ├── plantpulse-storage.properties # DB 연결
│ ├── plantpulse-mq.properties # 메시징 연결
│ └── log4j2.xml # 로깅 설정 (자가 시딩 — 최초 기동 시 생성)
├── bin/
│ ├── start.sh
│ ├── stop.sh
│ └── log-viewer.sh
├── logs/
│ └── system.log
├── path/ # 경로 매핑
└── server/ # 내장 Tomcat
├── conf/
│ ├── server.xml # 커넥터 / 포트
│ ├── context.xml # 컨텍스트 / 데이터소스
│ ├── catalina.properties
│ ├── logging.properties
│ ├── tomcat-users.xml
│ ├── web.xml
│ └── Catalina/localhost/
│ └── ROOT.xml # 컨텍스트 설정 (sessionCookieName 등)
└── webapps/
├── ROOT.war # 웹 애플리케이션 (WAR 통배포)
└── ROOT/ # Tomcat 이 unpack 한 디렉토리 (수정 금지)
배포 방식: 웹앱은
server/webapps/ROOT.war교체 방식으로 배포합니다 (WAR 통배포, 전환 진행 중). 기존app/plantpulse-server-webexploded 디렉토리 방식은 폐기 예정이며,server.xml의 ContextdocBase오버라이드는 제거되었습니다.ROOT/내부 파일을 직접 수정하면 다음 배포 때 사라지므로, 설정 변경은 항상config/에서 합니다.
주요 설정 파일
설정 파일은 모듈의 config/ 디렉토리에 있으며, 서버는 -Dpp.conf.dir → ${catalina.base}/../config → 클래스패스(fallback) 순으로 탐색합니다. 외부 설정이 로드되면 기동 로그에 Properties loaded from external conf: 가 출력됩니다.
application.properties 는 제거되었습니다 (2026.06). 테마 / 홈페이지 등 콘솔 동작 설정은 콘솔의 시스템 > 설정 관리 (
mm_config테이블)에서 관리하고,alarm.duplicate.check.minutes는plantpulse-engine.properties로 이관되었습니다.
plantpulse-engine.properties
IIoT 엔진 파이프라인 튜닝.
| 항목 | 기본값 | 설명 |
|---|---|---|
engine.pipeline.threads | 36 | 병렬 처리 스레드 수 |
engine.pipeline.ratelimit | 40000 | 초당 최대 메시지 |
engine.pipeline.batch.size | 1000 | 배치 묶음 크기 |
engine.pipeline.queue.size | 1200000 | 큐 용량 |
engine.dedup.enabled | true | 중복 제거 활성화 |
plantpulse-storage.properties
데이터 저장소 연결.
# Cassandra
cassandra.host=${PP_CASSANDRA_HOST}
cassandra.port=${PP_CASSANDRA_PORT}
cassandra.keyspace=${PP_KEYSPACE}
cassandra.user=${PP_CASSANDRA_USER}
cassandra.password=${PP_CASSANDRA_PASSWORD}
# PostgreSQL (Spring DataSource)
postgres.url=jdbc:postgresql://${PP_POSTGRES_HOST}:${PP_POSTGRES_PORT}/${PP_DB_NAME}
postgres.user=${PP_PG_USER}
postgres.password=${PP_PG_PASSWORD}
# Valkey
redis.host=${PP_REDIS_HOST}
redis.port=${PP_REDIS_PORT}
redis.password=${PP_REDIS_PASSWORD}
이 앱은 설정을 렌더링하지 않습니다. 컨테이너로 갈라진 뒤 웹 서버는 데이터레이크의 설정 렌더러 없이 뜨며, 값을 세 곳에서 읽습니다 — ① 운영자가 둔 외부 파일 → ② WAR 에 내장된 기본값 → ③ compose 가 넘긴 환경변수. 셋 중 환경변수가 내장 기본값을 덮습니다.
그래서 앱이 보는 값의 정의처는
compose/docker-compose.yml하나뿐입니다. 데이터레이크 쪽에만 있고 compose 에 없는 이름은 이 앱에 닿지 않습니다 — 데이터레이크에서만 식별자를 바꾼 사이트에서 앱 다섯이 옛 이름을 계속 쓴 사고가 그것입니다(2026-08-29).웹앱 내부(
server/webapps/ROOT/)의 파일은 수정하지 마세요 — 배포 시 사라집니다.
server/conf/server.xml
Tomcat 커넥터 설정. 포트 변경 / TLS / 압축 / 스레드 풀.
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="500"
acceptCount="200"
URIEncoding="UTF-8"
compression="on"
compressibleMimeType="text/html,text/css,application/json,application/javascript" />
<Connector port="443" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
SSLEnabled="true"
maxThreads="500"
sslEnabledProtocols="TLSv1.3,TLSv1.2">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/var/security/plantpulse/server.jks"
certificateKeystorePassword="${PP_TLS_KEYSTORE_PASSWORD}" />
</SSLHostConfig>
</Connector>
운영 명령
호스트에서 — 컨테이너 단위
컨테이너를 다시 띄우는 것이 곧 이 모듈의 재시작입니다. 인프라와 다른 앱은 영향을 받지 않습니다.
cd /opt/kopens/plantpulse-platform-docker
# 서버만 재시작
docker compose -f compose/docker-compose.yml restart plantpulse-server-web
# 상태 · 로그
bin/status.sh
bin/logs.sh plantpulse-server-web -n 200
컨테이너 안으로 들어가기
cd /opt/kopens/plantpulse-platform-docker/bin
./shell.sh plantpulse-server-web
restart-server.sh 로는 재시작되지 않습니다그 스크립트는 각 앱의 자기 컨테이너 안에서 그 모듈의 런처를 부르는 얇은 위임입니다. 데이터레이크 컨테이너에는 웹 서버 실행 파일이 없으므로 «모듈이 없다» 는 오류로 끝납니다. 위의 컨테이너 단위 재시작을 사용해 주세요.
핫 리로드 (재시작 없이 설정 반영)
일부 설정은 콘솔의 시스템 > 설정 관리 메뉴에서 핫 리로드가 가능합니다. config/ 의 프로퍼티 파일이나 server.xml 등 부팅 시점 설정은 재시작이 필요합니다.
로그
| 로그 | 경로 | 내용 |
|---|---|---|
| 메인 로그 | logs/system.log | 애플리케이션 로직, 에러 |
| Tomcat catalina | server/logs/catalina.out | Tomcat 표준 출력 |
| 액세스 로그 | server/logs/localhost_access_log.YYYY-MM-DD.txt | HTTP 요청 로그 |
| 엔진 로그 | logs/engine.log | IIoT 엔진 처리 상세 |
| 슬로우 쿼리 | logs/slow.log | 200ms 이상 쿼리 |
로그 레벨 변경 — 로깅 설정(log4j2.xml)은 모듈 config/ 로 외부화되어 있습니다 (자가 시딩: 최초 기동 시 WAR 기본값이 config/log4j2.xml 로 복사됨, 적용 시 기동 로그에 Logging reconfigured from external conf: 출력):
# 웹 서버 컨테이너 안에서
cd /opt/kopens/plantpulse-platform-docker/bin
./shell.sh plantpulse-server-web
vi /opt/kopens/plantpulse-platform/plantpulse-server/config/log4j2.xml
# <Logger name="plantpulse" level="DEBUG"/> 등 수정 후 exit
# 호스트에서 그 컨테이너만 재시작
cd /opt/kopens/plantpulse-platform-docker
docker compose -f compose/docker-compose.yml restart plantpulse-server-web
외부 파일이므로 WAR 재배포에도 영구 보존됩니다. 웹앱 내부(
server/webapps/ROOT/WEB-INF/classes/log4j2.xml)는 수정하지 마세요 — 배포 시 사라집니다.
성능 튜닝
JVM 힙
# server/bin/setenv.sh (없으면 생성)
export CATALINA_OPTS="-Xms16g -Xmx32g \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/plantpulse/heap/ \
-Xlog:gc*:file=/var/log/plantpulse/gc.log:time,uptime:filecount=10,filesize=100M"
Tomcat 스레드 풀
server.xml 의 Connector 에서 maxThreads / acceptCount 조정. 대규모 동시 접속 환경에서는 다음을 권장합니다.
| 동시 접속 | maxThreads | acceptCount |
|---|---|---|
| ~ 100 | 200 | 100 |
| ~ 500 | 500 | 200 |
| 500+ | 1000 | 500 |
IIoT 엔진 처리량
plantpulse-engine.properties 의 engine.pipeline.threads 를 호스트 코어 수의 70~80% 로 설정. 메시지 적체 시 engine.pipeline.queue.capacity 도 함께 상향.
자세한 튜닝은 성능 튜닝 페이지를 참고해 주세요.
업로드물은 어디에 담기나 — RustFS
설비 문서 · 설비 이미지 · 프로필 이미지는 오브젝트 스토어(RustFS) 에 담깁니다. 데이터베이스에는 본체 대신 s3://<버킷>/<키> 가 들어갑니다.
| 항목 | 값 |
|---|---|
| 버킷 | PP_S3_UPLOAD_BUCKET (기본 upload) — 데이터레이크가 쓰는 plantpulse 버킷과 다릅니다 |
| 접속 | PP_S3_HOST · PP_S3_PORT · PP_S3_USER · PP_S3_PASSWORD |
| 싣는 곳 | plantpulse-server-web 에만 실립니다. 워커에는 가지 않습니다 |
버킷을 가른 이유는 한쪽의 보존 규칙이 다른 쪽에 걸리지 않게 하려는 것입니다.
2026.09 는 부팅할 때 버킷을 만들어 주지 않습니다. 없는 버킷을 가리키면 업로드가 실패합니다. 클린 설치는 이관 과정에서 버킷이 만들어지지만, 값을 손으로 바꿔 다른 버킷을 가리킨다면 그 버킷을 먼저 만들어 두십시오.
접속 값이 비어 있으면 오브젝트 스토어를 만들지 못하고 업로드 경로가 뜨지 않습니다. 컨테이너 환경은 생성 시점에 고정되므로, 값을 더했다면 docker restart 가 아니라 컨테이너를 다시 만들어야 반영됩니다.
업로드 한도 — 한 줄로 적을 수 없습니다
한도가 두 층에 있고, 실제로 먼저 걸리는 것은 거의 언제나 아래층(엔드포인트)입니다.
엔드포인트별 가드 — 먼저 걸린다
| 올리는 것 | 엔드포인트 | 한도 |
|---|---|---|
| 설비 문서 | POST /asset/tree/document/upload | 50 MiB |
| 설비 문서 (API v5) | POST /api/v5/assets/{asset_id}/documents | 50 MiB |
| 설비 이미지 | POST /asset/image/upload | 10 MiB |
| 캔버스 SVG | POST /canvas/svgupload | 10 MiB |
설비 이미지와 캔버스 SVG 는 10 MiB 에서 거부됩니다. 문서만 50 MiB 입니다.
한도를 넘기면 보이는 것
네 엔드포인트가 같은 검증 함수를 쓰고, 메시지는 바이트 숫자만 나옵니다.
file_size: 12582912 > max 10485760
12 MiB 짜리 이미지를 올렸을 때의 예입니다 — 10485760 이 10 MiB, 52428800 이 50 MiB 입니다.
메시지에 단위가 없어 운영자가 바이트를 암산해야 하고, 어떤 파일이 걸렸는지도 나오지 않습니다. 여러 파일을 한 번에 올려 거부되면 크기를 직접 재어 찾아야 합니다.
컨테이너 한도 — 그 위의 천장
| 무엇 | 값 | 어디서 |
|---|---|---|
| 파일 하나 | 50 MiB | web.xml 의 <max-file-size> |
| 멀티파트 요청 전체 | 52 MiB | web.xml 의 <max-request-size> |
| 폼 파라미터(파일 파트 제외) | 2 MiB | Tomcat 기본값 — 두 커넥터 모두 maxPostSize 를 선언하지 않습니다 |
maxPostSize 는 Tomcat 이 요청 파라미터로 바꾸는 바이트의 한도입니다. x-www-form-urlencoded 는 본문 전체가, multipart/form-data 는 파일이 아닌 파트만 셉니다. JSON 같은 raw 본문은 해당이 없습니다.
실측(2026-09-18): 프록시 경유로 3 MiB 멀티파트를 보내면 본문이 전부 전송돼 애플리케이션 인증 계층까지 닿습니다(크기 때문이 아닌 401 로 끝납니다). 전송 경로에 2 MiB 벽은 없고, nginx 는 client_max_body_size 200m 입니다.
2026-09-18 이전에는 443 커넥터만 maxPostSize="-1" 이라 같은 서버가 포트에 따라 다른 한도를 강제했습니다. 지금은 80 · 443 이 같습니다.
헬스 체크
# 표준 헬스체크 (익명, readiness — 엔진이 전 단계 기동을 완료(RUNNING)했을 때만 UP. 배포 자동검증 / 모니터링용)
curl -fsS http://127.0.0.1/api/health
# 준비 완료: 200 {"status":"UP","service":"plantpulse-server-web","ts":1765500000000,"checks":{"engine":"RUNNING"}}
# 기동 중: 503 {"status":"STARTING",...} / 기동 실패·중지: 503 {"status":"DEGRADED",...}
# 경로는 /api/health 다 — «/api/v5/health» 는 없다. 인증도 필요 없다(익명 허용 경로).
# 콘솔 ping
curl -fsS http://127.0.0.1/api/v5/ping
# 공개 상태 페이지의 데이터 (익명 — 24시간 가용성·p99·엔드포인트별 상태)
curl -fsS http://127.0.0.1/api/v5/public/status
# WebSocket 핸드셰이크 확인
curl -i -N \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://127.0.0.1:8000/ws
자주 발생하는 문제
| 증상 | 원인 | 조치 |
|---|---|---|
OutOfMemoryError | 힙 부족 | setenv.sh 에서 -Xmx 상향 + GC 로그 분석 |
Too many open files | 파일 디스크립터 한계 | /etc/security/limits.conf 의 nofile 65535+ |
| 콘솔 응답 느림 | DB 슬로우 쿼리 | PostgreSQL slow query 로그, Valkey 캐시 적중률 확인 |
| 로그인 무한 루프 | 세션 / 쿠키 문제 | 브라우저 캐시 삭제, server/conf/Catalina/localhost/ROOT.xml 의 sessionCookieName 확인 |
| 502 / 504 (리버스 프록시) | 백엔드 응답 시간 초과 | nginx proxy_read_timeout 상향 |
| WebSocket 끊김 | 방화벽 idle timeout | 방화벽 timeout > 60s, tomcat.websocket.session.timeout 조정 |
상세 진단은 문제 해결 페이지를 참고해 주세요.
보안 체크리스트
- 기본 admin 비밀번호 변경 (
admin / admin123!) -
tomcat-users.xml의 manager 계정 제거 또는 강한 비밀번호 -
server.xml의shutdown port (7000)외부 접근 차단 - HTTPS 인증서 적용 (
plantpulse-certs가 구운pp-security볼륨의 자재 또는 외부 CA) - X-Frame-Options / CSP / HSTS 헤더 설정 확인
- API Bearer Token 운영 vault 에 저장
-
config/프로퍼티 파일의 디버그 옵션 비활성화 및 권한 제한 (chmod 600)
자세한 보안 설정은 보안 설정 페이지를 참고해 주세요.