역할
lh-smah-batch는 DB에 등록된 PostgreSQL 함수(프로시저)를 주기에 맞춰 대신 호출해 주는 실행기임. 자기 업무 로직이 거의 없음. 전체 그림은 공개노트/개발/시스템 구성.
cmn.tb_proc_m(프로시저기본)에서 실행 대상 목록을 읽어SELECT {프로시저명}(...)형태로 호출함- 통계 화면의 원천 데이터(단지별 가입자수, IoT 제어수, 이벤트 발생수 등)가 여기서 만들어짐
- 외부에 노출된 API 없음. 아무도 배치를 호출하지 않고 자체 스케줄(
@Scheduled)로만 돎 - 예외로 COTIS 잡 1개만 LH 에너지통합플랫폼을 직접 HTTP 호출해 단지·건물·세대 기준정보를 받아 적재함
SCW와의 구분
같은 cmn.tb_proc_m 테이블을 공개노트/개발/모듈/SCW 스케줄러 서버도 읽음. 담당이 컬럼으로 갈림.
| 구분 | 배치 | SCW |
|---|---|---|
| 집는 행 | cron_schdl_yn = 'N' | cron_xprssn_vl이 있는 행 |
| 주기 출처 | 소스에 하드코딩된 cron 7개 | DB의 cron_xprssn_vl |
| 실행 대상 | DB 프로시저(집계·삭제) | 자바 서비스 로직(날씨, 단말기 수집 등) |
actv_trg_svr_id | 쿼리에서 미사용 | Active WAS 판정에 사용 |
기술 스택
| 항목 | 값 |
|---|---|
| 프레임워크 | Spring Boot 2.4.7, Java 8, Gradle, jar 패키징(lh-smah-batch-{프로필}.jar) |
| 배치 | spring-boot-starter-batch + Spring @Scheduled |
| 영속성 | MyBatis + HikariCP, PostgreSQL smahdb |
| 포트 | 8880 (컨트롤러 0개지만 내장 톰캣이 뜸) |
| 외부 호출 | Apache HttpClient, Squid 프록시 경유(주소가 소스에 하드코딩됨) |
| 로그 | 현재 파일 /app/logs/was/tomcat-batch/logs/batch.log, 롤링본·에러 파일은 /app/logs/tomcat-batch/ |
| Jenkins 잡 | 없음. APW·SCW·OAW만 있음 → 수동 빌드·배포로 추정 |
- 다른 WAS와 달리 war가 아니라 jar임. 톰캣에 올리지 않고
java -jar로 뜸. 공개노트/개발/배포와 빌드 - 프로필은 빌드 시점에 고정됨(
src/main/resources-{프로필}). 실행할 때-Dspring.profiles.active=prod를 줘도 안 바뀜. 어느 프로필인지는 jar 파일명으로 판별할 것 - 8880 포트가 이미 점유되어 있으면 배치 전체가 기동 실패함
- 로그 패턴에
[traceId=]가 있으나 배치는 MDC에 값을 넣지 않아 항상 빈 값임
잡 7개와 스케줄
cron은 전부 BatchScheduler에 하드코딩되어 있고 DB로 못 바꿈.
| 잡 | cron | 하는 일 | 실제 실행 시점 |
|---|---|---|---|
job1 | 매분 5초 | pod_cd='m'·pod_hr='1' 프로시저 | 매분 |
jobMin | 5분마다 10초 | pod_hr='5' 프로시저 | 분이 5의 배수일 때 |
jobMin15 | 15분마다 15초 | pod_hr='15' 프로시저 | 분이 5의 배수일 때 |
jobHour | 5분마다 25초 | pod_cd='H' 시간별 집계 | run_scd분(비면 5분) |
jobDay | 5분마다 30초 | pod_cd='D' 일별 집계·삭제 | run_scd시 10분(비면 00:10) |
jobMon | 5분마다 35초 | pod_cd='M' 월별 집계 | run_scd일 00:15(비면 1일) |
jobCotis | 매일 03:20:10 | 에너지통합플랫폼 기준정보 동기화 | 매일 1회 |
- 시간/일/월 잡은 cron 자체가 5분마다 돌고, 실제 실행 여부를 코드 안에서 걸러 냄. 그래서
run_scd는 5의 배수만 유효함 - 초를 5·10·15·25·30·35로 어긋나게 둬 동시 기동을 피함
pod_cd='Y'(연별)는 조회 쿼리만 있고 잡이 없음. 등록해도 영원히 안 돎- 잡 7개는 각자 전용 스레드풀(크기 1)에서 독립적으로 돎. 서로 막지 않음
실행 이력 보는 법
장애 진단은 cmn.tb_proc_run_h(프로시져실행이력)부터 봄. 자바가 프로시저 반환값을 버리기 때문에 성공·실패는 이 테이블에만 남음.
rsl_cd | 뜻 |
|---|---|
0 | 실행 중. 시작 로그만 남고 종료 로그가 없음 = 비정상 종료 의심 |
1 | 성공 |
2 및 그 외 | 오류. 원인은 er_msg |
조회 키는 proc_nm + proc_trg_tm_vl(월 YYYYMM / 일 YYYYMMDD / 시간 YYYYMMDDHH0000 / 분 YYYYMMDDHHMI00)임.
자바 로그에는 오류가 없는데 통계가 비어 있는 경우가 흔함. 프로시저 내부에 예외 처리가 있어 업무 오류를
rsl_cd='2'로 기록하고 정상 반환하기 때문임. 반드시er_msg를 볼 것.
Spring Batch 메타 테이블(batch.batch_*)도 병행해서 쌓임. 하루 약 2,600건 생기고 sp_table_delete가 30일 지난 행을 지움(1회 삭제 상한 있음). 배치가 며칠 멈췄다 재개되면 밀린 행이 한 번에 안 지워짐.
로그 검색 키워드
- 잡 동작:
<< 1분배치 >>,<< 5분 배치 >>,<< 15분 배치 >>,<< HourlySP >>,<< DailySP >>,<< MonthlySP >> - 대상 개수:
SP개수:—0이 계속 찍히면tb_proc_m의us_yn='N'이거나cron_schdl_yn이Y로 잘못 들어간 것임 - COTIS:
total:(한 줄만 나옴) - 실패:
batch_error.log
COTIS — 기준정보 동기화
매일 03:20에 LH 에너지통합플랫폼(eep.lh.or.kr)에서 단지(sbd) → 건물/동(bld) → 세대(hsh) → 단지공급정보(ssi) → 그룹-단지관계(gsr) 순으로 받아 cmn.tb_*_m에 upsert함.
- 신규 단지·동·세대가 앱/관리자에 안 보이면 이 잡부터 볼 것. 03:20 이후 로그 확인
- 갱신자는 전부
updr_id = 'batch'로 남음. “누가 바꿨나” 문의는 이걸로 식별함 - 대상별 예외 처리가 없음. 앞 순서(
sbd)에서 죽으면 뒤는 아예 안 돎 - 에너지통합플랫폼이 200이 아닌 응답을 주면 잡이 실패하고
tb_proc_run_h에 cotis 행 자체가 안 남음 → “이력이 없다 = 외부 호출 실패”로 읽으면 됨 - 이력은 항상 성공(
rsl_cd='1')으로 고정 기록됨. 성공 여부 판단에 쓰지 말 것.er_msg자리에 대상별 건수 문자열이 들어감 - 세대 테이블의 홈넷식별번호·검침일 등 일부 컬럼은 COTIS가 건드리지 않음(항상
null로 INSERT) - 2023-12에 “한 번에 너무 많이 INSERT해서 터진” 장애가 있어 현재는 200건씩 나눠 넣음. 세대 수가 많은 단지가 추가되면 재발 가능 영역임
배치가 멈추면 무엇이 비는가
| 안 도는 잡 | 증상 |
|---|---|
jobHour | 시간별 App·WEB 가입자·접속자 통계, 시간별 IoT 제어 통계가 빔. 투표 마감 처리·결과 집계가 안 됨 |
jobDay | 일별 통계 전부 공백(단지·동 세대수, App 이용, WEB 가입, DR 실적, IoT 제어, 관리비·임대료 등록 현황, 전출입, 이벤트 발생수). 추가로 보관주기 데이터 삭제·탈퇴회원 삭제가 멈춰 DB 용량이 계속 증가함 |
jobMon | 월별 집계 |
jobCotis | 신규 단지·동·세대가 DB에 안 들어와 앱·관리자에서 안 보이거나 옛날 값으로 남음 |
밀린 회차를 따라잡지 않음(catch-up 없음). 03:00~06:00 정지했다 살아나면 그날 00:10 일배치는 영영 안 돎. 수동으로 SELECT cmn.{프로시저명}('0','{YYYYMMDD}','M');를 실행해 메꿔야 함(각 SQL 파일 주석의 < example > 참고).
주요 프로시저
프로시저 목록·SQL 로직·집계 규칙은 공개노트/개발/모듈/통계 집계 DB 함수에 정리돼 있음. 여기서는 배치 관점만 다룸.
운영 DB의 실제 등록 내역은 아래로 확인할 것.
SELECT * FROM cmn.tb_proc_m WHERE cron_schdl_yn='N' AND us_yn='Y' ORDER BY pod_cd, run_sqn;- 시간(H) 4개(App·WEB 가입자·접속자, IoT 제어, 전자투표), 일(D) 13개(각종 일별 집계 + 보관주기 삭제·탈퇴회원 삭제·공공데이터 감시)
sp_tb_elc_vt_rsl_l은 SQL 파일명이sp_elc_vt_rsl_l.sql로 불일치함cmn.sf_sp_st_log/sf_sp_ed_log는 각 프로시저가 내부에서 부르는 이력 기록용 공통 함수임
tb_proc_m 주요 컬럼 (배치 관점)
| 컬럼 | 배치에서의 의미 |
|---|---|
proc_nm | 그대로 SQL에 치환되어 호출됨 |
cron_schdl_yn | N이어야 배치가 집음. Y면 SCW 담당 |
pod_cd / pod_hr | 주기코드(m/H/D/M) / m일 때 1·5·15로 세분 |
run_scd | 실행 시점. H=분, D=시, M=일. 5의 배수만 유효 |
run_sqn | 실행 순번. 앞 순번이 실패하면 뒤가 전부 안 돎 |
us_yn | Y만 실행 |
actv_trg_svr_id | 배치 쿼리에서 미사용. 아래 알려진 이슈 참고 |
알려진 이슈
- 프로시저 루프에 예외 처리가 없음. 하나가 실패하면 그 뒤
run_sqn의 프로시저가 그 회차에 전부 스킵됨 actv_trg_svr_id필터가 쿼리에 없음. 배치를 2대 이상 띄우면 같은 프로시저를 중복 실행함. 집계 프로시저는 대부분DELETE 후 INSERT라 PK 충돌 또는 집계 중복·누락이 날 수 있음- 중복 실행 방지가 스레드풀 크기 1에만 의존함. 잡 파라미터에 실행 시각을 넣어 Spring Batch의 중복 방지가 무력함. 같은 서버 안에서는 막히지만 막힌 사실이 로그에 안 남고
batch.batch_job_execution에 FAILED만 쌓임 - 재시도·스킵·자동 재기동 없음. 실패한 회차는 그냥 버려짐
- COTIS에 HTTP 타임아웃이 없음. 상대 서버가 응답을 안 주면 스레드가 무한 대기하고, 스레드풀이 1이라 다음 날 기동도 거부됨
- COTIS 트랜잭션이 통짜임. 중간에 실패하면 앞서 성공한 upsert도 롤백됨
- COTIS 건수 집계 누락.
rsl_row_cnt에gsr건수가 빠져 있어 로그의total:과 값이 다름 - jasypt 암복호화 키가 같은 설정 파일에 평문임([REDACTED] 처리 대상). 알고리즘도 구식이라 암호화 실효가 거의 없음. 공개노트/개발/모듈/비밀번호와 전화번호 암호화
- local 프로필 설정에 운영 DB 정보와 구형 평문 비밀번호 형식이 주석으로 잔존함
- 프록시 주소와 cron이 소스에 하드코딩됨. 바뀌면 재빌드가 필요함
@Profile가드가 주석 처리되어 있음. 개발 PC에서 local로 띄워도 스케줄러가 전부 돌아 개발 DB 프로시저가 실제로 실행됨- dev/prod 콘솔 로그가 실제로는 안 나감. appender 정의만 있고 연결이 빠져 있음
- 로그 경로가 두 곳으로 갈림. 현재 파일과 롤링·에러 파일 디렉터리가 달라 둘 다 봐야 함
- prod 커넥션풀 설정이 전부 주석 처리되어 Hikari 기본값(pool 10, 타임아웃 30초)으로 돎. dev(6)보다 큼
run_sqn중복(sp_tb_evn_occ_dly_y·sp_tb_mngr_h_dly_y둘 다 D/5/10)으로 실행 순서가 비결정적임- 테스트 코드 사실상 없음(컨텍스트 로딩 1건)
확인 필요
확인 필요
- 운영
cmn.tb_proc_m의 실제 등록 내역. 이 페이지의 프로시저 목록은 SQL 파일 주석의 등록 예시 기준임.pod_cd='m'(1·5·15분) 항목이 실제로 있는지 확인 필요. 없다면job1·jobMin·jobMin15는 헛돌면서 메타 테이블 행만 쌓고 있는 셈임actv_trg_svr_id필터 누락이 의도인지 버그인지. 운영에 배치가 몇 대 떠 있는지를 먼저 확인해야 영향도 판단이 가능함- COTIS의 정확한 의미와 호출 URL 끝의
1이 무엇인지(추정: 최근 1일치 변경분). 조회 범위가 1일이 맞다면 배치가 하루 이상 멈췄을 때 그 기간 변경분이 영구 누락됨. 에너지통합플랫폼(SQI·헤리트) 연동규격서 확인 필요- COTIS 호출에 인증이 정말 없는지. 코드상 토큰·API Key 헤더가 없음. IP 화이트리스트로만 통제되는 구조인지 확인 필요
- SQL 인젝션 표면. 프로시저명이 문자열 치환으로 SQL에 들어감.
tb_proc_m.proc_nm을 수정할 수 있는 관리자 화면이 있다면 임의 SQL 실행 경로가 됨. 화면 존재 여부와 권한 확인 필요- Spring Batch 메타 테이블 스키마 접근 방식. 설정에 스키마 접두어가 없는데 실물은
batch스키마에 있음. DB 계정의search_path확인 필요- 배치 기동·종료 스크립트와 배포 경로. Jenkins 빌드 대상에 배치가 없어 수동 빌드·수동 배포일 가능성이 높음. 어느 서버에서 어떤 명령으로 뜨는지 확인 필요
- 통계 테이블(
*_dly_y,*_hly_y)을 소비하는 화면 특정. 서버 레포에는 이 테이블을 읽는 코드가 없고 SMW/SMC 또는 LMW/LMC 소스를 봐야 함. 그래야 “배치가 멈추면 어느 화면이 비는지”를 화면 단위로 말할 수 있음sp_tb_evn_occ_dly_y·sp_tb_mngr_h_dly_y의run_sqn중복 정리 필요 여부tb_proc_run_h.run_ty_cd='P'(COTIS 전용)의 정의. 코드·SQL 어디에도 없음google-auth-library-oauth2-http의존성이 실제로 필요한지. 소스에서 사용처를 못 찾음
관련
공개노트/개발/모듈/통계 집계 DB 함수 공개노트/개발/시스템 구성 공개노트/개발/모듈/SCW 스케줄러 서버 공개노트/개발/배포와 빌드 공개노트/개발/모듈/APW 연계 서버 기본매뉴얼/운영/tomcat과 배치 기동 기본매뉴얼/운영/WAS 로그 확인 기본매뉴얼/운영/장애 대응 절차