본문으로 건너뛰기

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-web exploded 디렉토리 방식은 폐기 예정이며, server.xml 의 Context docBase 오버라이드는 제거되었습니다. 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.threads36병렬 처리 스레드 수
engine.pipeline.ratelimit40000초당 최대 메시지
engine.pipeline.batch.size1000배치 묶음 크기
engine.pipeline.queue.size1200000큐 용량
engine.dedup.enabledtrue중복 제거 활성화

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 catalinaserver/logs/catalina.outTomcat 표준 출력
액세스 로그server/logs/localhost_access_log.YYYY-MM-DD.txtHTTP 요청 로그
엔진 로그logs/engine.logIIoT 엔진 처리 상세
슬로우 쿼리logs/slow.log200ms 이상 쿼리

로그 레벨 변경 — 로깅 설정(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 조정. 대규모 동시 접속 환경에서는 다음을 권장합니다.

동시 접속maxThreadsacceptCount
~ 100200100
~ 500500200
500+1000500

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/upload50 MiB
설비 문서 (API v5)POST /api/v5/assets/{asset_id}/documents50 MiB
설비 이미지POST /asset/image/upload10 MiB
캔버스 SVGPOST /canvas/svgupload10 MiB
「업로드 한도 50 MiB」로만 안내하면 이미지에서 틀립니다

설비 이미지와 캔버스 SVG 는 10 MiB 에서 거부됩니다. 문서만 50 MiB 입니다.

한도를 넘기면 보이는 것​

네 엔드포인트가 같은 검증 함수를 쓰고, 메시지는 바이트 숫자만 나옵니다.

file_size: 12582912 > max 10485760

12 MiB 짜리 이미지를 올렸을 때의 예입니다 — 10485760 이 10 MiB, 52428800 이 50 MiB 입니다.

단위도 파일 이름도 없습니다

메시지에 단위가 없어 운영자가 바이트를 암산해야 하고, 어떤 파일이 걸렸는지도 나오지 않습니다. 여러 파일을 한 번에 올려 거부되면 크기를 직접 재어 찾아야 합니다.

컨테이너 한도 — 그 위의 천장​

무엇값어디서
파일 하나50 MiBweb.xml 의 <max-file-size>
멀티파트 요청 전체52 MiBweb.xml 의 <max-request-size>
폼 파라미터(파일 파트 제외)2 MiBTomcat 기본값 — 두 커넥터 모두 maxPostSize 를 선언하지 않습니다
2 MiB 는 «파일» 한도가 아닙니다 — 프록시로도 큰 파일이 올라갑니다

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)

자세한 보안 설정은 보안 설정 페이지를 참고해 주세요.

관련 문서​