프로젝트명: KRX 배치 마스터 (next-krx-lds-bat-mst)
작성 기준일: 2026-04-01
대상 독자: 신규 개발자 / 운영 담당자
- 프로젝트 개요
- 기술 스택
- 전체 시스템 아키텍처
- 레이어 아키텍처
- Spring Security 인증/인가 구조
- Redis를 활용한 세션 관리
- 배치 마스터(Master-Agent) 동작 구조
- 주요 화면 및 기능 설명
- 환경별 구성
- 운영 관리 체크리스트
배치 마스터 시스템은 금융 업무에서 사용되는 배치(일괄처리) 작업들을 중앙에서 관리하는 플랫폼입니다.
쉽게 말하면: "언제, 어떤 배치 작업을, 어떻게 실행할지" 를 관리하는 관제 시스템
| 역할 | 설명 |
|---|---|
| Job 관리 | 배치 작업 등록/수정/삭제/조회 |
| 스케줄 관리 | 작업을 언제 실행할지 일정 관리 (캘린더/Cron 방식) |
| 실행 결과 조회 | 각 작업의 실행 이력 및 성공/실패 결과 확인 |
| 실시간 알림 | 작업 완료/오류 발생 시 실시간 화면 푸시 |
┌─────────────────────────────────────────────────────────────────┐
│ KRX 전체 시스템 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 배치 마스터 │ Kafka │ 실제 배치 작업 처리 시스템 │ │
│ │ (이 시스템) │───────▶│ (Worker 서버들) │ │
│ │ │ │ │ │
│ │ · Job 등록 │ │ · 실제 데이터 처리 │ │
│ │ · 스케줄 설정 │ │ · 파일 처리 │ │
│ │ · 결과 조회 │ │ · DB 배치 처리 │ │
│ └──────────────────┘ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| 분류 | 기술 | 버전 | 용도 |
|---|---|---|---|
| 프레임워크 | Spring Boot | 3.5.11 | 전체 애플리케이션 기반 |
| 언어 | Java | 21 | 메인 개발 언어 |
| 빌드 | Gradle | 9.3.0 | 빌드/의존성 관리 |
| 보안 | Spring Security | 3.5.11 | 인증/인가 처리 |
| 인증 토큰 | JWT (JJWT) | 0.12.6 | 사용자 인증 토큰 |
| 데이터 접근 | MyBatis | 3.0.3 | DB 쿼리 처리 |
| 데이터베이스 | Oracle (AWS RDS) | 23.3.0 | 메인 데이터 저장소 |
| 세션 저장소 | Redis + Spring Session | 3.5.11 | 분산 세션 관리 |
| 스케줄러 | Quartz | 3.5.11 | 배치 작업 일정 관리 |
| 배치 | Spring Batch | 3.5.11 | 배치 작업 실행 관리 |
| 메시징 | Apache Kafka | - | 비동기 작업 메시지 전달 |
| 분산 락 | ShedLock | 4.39.0 | 다중 서버 중복 실행 방지 |
| 캐시 | Caffeine | - | 메모리 캐시 |
| 화면 템플릿 | Thymeleaf | - | 서버사이드 HTML 렌더링 |
| API 문서 | Swagger (SpringDoc) | 2.8.13 | API 명세 자동 생성 |
| XSS 방어 | Lucy XSS Filter | 2.0.1 | 웹 보안 (XSS 공격 차단) |
┌──────────────────────────────────────────────────────────────┐
│ 프레젠테이션 계층 │
│ Thymeleaf (HTML/CSS/JS) │ REST API (JSON) │ Swagger UI │
└─────────────────────────────────────────────────────────────-┘
│
┌──────────────────────────────────────────────────────────────┐
│ 보안 계층 │
│ Spring Security │ JWT │ Redis Session │
└──────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────────────────────────────────────┐
│ 비즈니스 로직 계층 │
│ Spring MVC (Controller/Service) │ AOP (권한체크/실행시간) │
└──────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────────────────────────────────────┐
│ 데이터 접근 계층 │
│ MyBatis (Mapper + XML) │ HikariCP │
└──────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────────────────────────────────────┐
│ 인프라 계층 │
│ Oracle DB │ Redis │ Kafka │ Caffeine Cache │
└──────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ 사용자 브라우저 │
└────────┬────────┘
│ HTTP 요청
▼
┌─────────────────────────┐
│ Spring Boot WAS │
│ (next-krx-lds-bat-mst) │
│ │
│ ┌────────────────────┐ │
│ │ Spring Security │ │◀── 모든 요청의 첫 번째 관문
│ │ (보안 필터 체인) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Controller │ │◀── URL 라우팅
│ │ (REST / Web UI) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Service │ │◀── 비즈니스 로직
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Mapper (MyBatis) │ │◀── DB 쿼리
│ └────────┬───────────┘ │
│ │ │
└───────────┼─────────────┘
│
┌────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌───────────┐ ┌──────────────────┐
│ Oracle DB │ │ Redis │ │ Kafka │
│ (AWS RDS) │ │ (세션) │ │ (메시지 브로커) │
└──────────────┘ └───────────┘ └────────┬─────────┘
│
┌────────▼─────────┐
│ Worker 서버들 │
│ (배치 실행) │
└──────────────────┘
사용자 요청
│
├─ /api/** → [1순위] ApiSecurityConfig → REST Controller → JSON 응답
├─ /common/api/** → [1순위] ApiSecurityConfig → REST Controller → JSON 응답
│
├─ /common/auth/** → [2순위] WebSecurityConfig → Web Controller → HTML 응답
├─ /job/** → [2순위] WebSecurityConfig → Web Controller → HTML 응답
├─ /calendar/** → [2순위] WebSecurityConfig → Web Controller → HTML 응답
├─ /result/** → [2순위] WebSecurityConfig → Web Controller → HTML 응답
│
├─ /apidoc → 인증 없이 접근 (Swagger 문서)
├─ /swagger-ui/** → 인증 없이 접근 (Swagger UI)
└─ /resources/** → 인증 없이 접근 (CSS, JS, 이미지)
ldsframework/
│
├─ common/ ← 전사 공통 (보안, 설정, 유틸)
│ ├─ config/security/ ← Spring Security 핵심
│ ├─ config/jwt/ ← JWT 토큰 관리
│ ├─ config/redis/ ← Redis 세션 설정
│ ├─ config/kafka/ ← Kafka 메시징 설정
│ ├─ config/batch/ ← 스케줄러/배치 설정
│ ├─ aop/ ← 권한 체크 AOP
│ ├─ advice/ ← 응답 래핑, 예외 처리
│ └─ utils/ ← 공통 유틸
│
└─ batmst/ ← 업무 로직 (배치 마스터)
├─ api/job/ ← Job 관리 API
├─ api/calendar/ ← 캘린더 관리 API
├─ api/result/ ← 실행 결과 API
├─ api/scheduler/ ← Quartz 스케줄러 API
└─ web/ ← 화면 라우팅 (UI Controller)
[HTTP 요청]
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Controller 계층 │
│ - @RestController : REST API 응답 (JSON) │
│ - @Controller : 웹 화면 응답 (HTML) │
│ - URL 매핑 처리, 요청 파라미터 수신 │
│ - 비즈니스 로직은 없고 Service에 위임만 함 │
└──────────────────────┬───────────────────────────────────────┘
│ 위임
▼
┌──────────────────────────────────────────────────────────────┐
│ Service 계층 │
│ - 실제 비즈니스 로직 처리 (중복 체크, 유효성 검증 등) │
│ - 트랜잭션 관리 (@Transactional) │
│ - 여러 Mapper를 조합하여 복잡한 로직 처리 │
└──────────────────────┬───────────────────────────────────────┘
│ DB 조회 위임
▼
┌──────────────────────────────────────────────────────────────┐
│ Mapper 계층 (MyBatis) │
│ - DB와의 직접 통신 담당 │
│ - Java 인터페이스 + XML로 쿼리 분리 │
│ - 동적 쿼리(if, choose, foreach 등)는 XML에서 처리 │
└──────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Oracle DB │
└─────────────────┘
모든 REST API 응답은 CommonResponseDto로 자동 래핑됩니다.
{
"statusCode": "200",
"statusMsg": "성공",
"data": {
/* 실제 데이터 */
}
}참고:
@NoWrap어노테이션이 붙은 컨트롤러/메서드는 래핑에서 제외됩니다.
이 시스템은 두 가지 보안 체계를 동시에 운영합니다.
┌─────────────────────────────────────────────┐
│ 모든 HTTP 요청 │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ URL 패턴으로 보안 체계 선택 │
└──────────┬────────────────────┬─────────────┘
│ │
/api/** │ │ 그 외 URL
/common/api/** │ │
▼ ▼
┌───────────────────────┐ ┌────────────────────────┐
│ ApiSecurityConfig │ │ WebSecurityConfig │
│ [Order=1] │ │ [Order=2] │
│ │ │ │
│ · JWT 토큰 기반 인증 │ │ · 세션 기반 인증 │
│ · STATELESS (무상태) │ │ · 동시 로그인 제한(1) │
│ · CSRF 비활성화 │ │ · 세션 ID 고정 방지 │
└───────────────────────┘ └────────────────────────┘
| 항목 | API 보안 (ApiSecurityConfig) | 웹 보안 (WebSecurityConfig) |
|---|---|---|
| 적용 URL | /api/**, /common/api/** |
나머지 모든 URL |
| 인증 방식 | JWT 토큰 (헤더) | 세션 (쿠키) |
| 상태 관리 | Stateless (상태 없음) | 세션 유지 |
| 동시 로그인 | 제한 없음 | 1개만 허용 |
| 처리 순서 | 1순위 | 2순위 |
[클라이언트]
│
│ POST /common/api/auth/login-processing
│ Body: { "id": "user01", "password": "pass" }
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ ApiJsonLoginAuthenticationFilter │
│ 역할: JSON Body에서 아이디/패스워드를 꺼내서 인증 요청 생성 │
└──────────────────────────────┬───────────────────────────────────┘
│ authenticate()
▼
┌──────────────────────────────────────────────────────────────────┐
│ AuthenticationManager (ProviderManager) │
│ 역할: 어떤 Provider에게 검증을 맡길지 결정 │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ CustomUserDetailsService │
│ 역할: DB에서 해당 아이디의 사용자 정보(패스워드, 권한 등) 조회 │
└──────────────────────────────┬───────────────────────────────────┘
│
┌───────────────┴──────────────┐
▼ ▼
[인증 성공] [인증 실패]
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ ApiJsonLogin │ │ ApiJsonLogin │
│ AuthenticationSuccess │ │ AuthenticationFailure │
│ Handler │ │ Handler │
│ │ │ │
│ · Access Token 발급 │ │ · 401 에러 응답 │
│ · Refresh Token 발급 │ │ · 오류 메시지 JSON 반환 │
│ · JSON으로 토큰 응답 │ └──────────────────────────────────┘
└──────────────────────────┘
│
▼
[클라이언트에게 JWT 토큰 반환]
{
"accessToken": "eyJ...", ← 1시간 유효
"refreshToken": "eyJ..." ← 24시간 유효
}
[클라이언트]
│
│ GET /api/read/jobs
│ Header: Authorization: Bearer eyJ...
│
▼
┌────────────────────────────────────────────────────────────────┐
│ ApiJwtAuthenticationFilter (JWT 검증 필터) │
│ │
│ 1단계: 헤더에서 JWT 토큰 추출 │
│ ├─ 토큰 없음 → 다음 필터로 통과 (후에 인가 단계에서 차단) │
│ └─ 토큰 있음 → 2단계로 │
│ │
│ 2단계: 토큰 유효성 검증 │
│ ├─ 만료된 토큰 → 401 응답 (토큰 만료 안내) │
│ ├─ 위조된 토큰 → 401 응답 (잘못된 토큰 안내) │
│ └─ 유효한 토큰 → SecurityContextHolder에 인증정보 저장 │
└────────────────────────────────┬───────────────────────────────┘
│ 인증 성공
▼
┌────────────────────────────────────────────────────────────────┐
│ ApiAuthorizationManager (인가 처리) │
│ │
│ · SecurityContext에서 인증 정보 꺼내기 │
│ · JWT 토큰에서 사용자 권한(ROLE) 확인 │
│ · 요청 URL에 대한 접근 권한 판단 │
│ ├─ 접근 허용 → Controller로 전달 │
│ └─ 접근 거부 → 403 응답 (ApiAccessDeniedHandler) │
└────────────────────────────────┬───────────────────────────────┘
│
▼
[Controller 실행]
[브라우저]
│
│ POST /common/auth/login-processing
│ Form: id=user01&password=pass
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ WebIdPasswordLoginAuthenticationFilter │
│ 역할: form 파라미터(id, password)를 꺼내서 인증 토큰 생성 │
└──────────────────────────────┬───────────────────────────────────┘
│ authenticate()
▼
┌──────────────────────────────────────────────────────────────────┐
│ webAuthenticationManager (ProviderManager) │
│ → WebIdPasswordLoginAuthenticationProvider에게 검증 위임 │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ WebIdPasswordLoginAuthenticationProvider │
│ │
│ 1. CustomUserDetailsService로 DB에서 사용자 조회 │
│ 2. PasswordEncoder로 입력된 패스워드와 DB 패스워드 비교 │
│ 3. 계정 상태 확인 (잠김/만료/비활성화 여부) │
└──────────────────────────────┬───────────────────────────────────┘
│
┌───────────────┴──────────────┐
▼ ▼
[인증 성공] [인증 실패]
│ │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ CompositeSession │ │ WebIdPasswordLogin │
│ AuthenticationStrategy │ │ AuthenticationFailure │
│ (3단계 세션 전략 순서 실행) │ │ Handler │
│ │ │ · 로그인 페이지로 리다이렉트 │
│ 1. 동시 로그인 제한 체크 │ │ · 에러 메시지 전달 │
│ (기존 세션 만료 처리) │ └──────────────────────────────┘
│ │
│ 2. 세션 ID 교체 │
│ (세션 고정 공격 방지) │
│ │
│ 3. 새 세션을 │
│ SessionRegistry에 등록 │
└──────────────────────┬───────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ WebIdPasswordLoginAuthenticationSuccessHandler │
│ │
│ 1. Access Token 생성 (유효기간: 1시간) │
│ 2. Refresh Token 생성 (유효기간: 24시간) │
│ 3. Refresh Token을 DB에 저장 (갱신용) │
│ 4. JWT Token을 세션에 저장 │
│ session.setAttribute("jwtToken", jwtToken) │
│ 5. 기본 이동 URL로 리다이렉트 → /result │
└──────────────────────────────────────────────────────────────────┘
[로그인 이후 화면 요청]
│
│ GET /job
│ Cookie: JSESSIONID=abc123 ← 브라우저가 자동으로 전송
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Session 저장소 (Redis) │
│ · JSESSIONID로 세션 검색 │
│ · 세션에서 SecurityContext 복원 │
│ · 세션에서 jwtToken 복원 │
└────────────────────────────────┬───────────────────────────────┘
│ 세션 유효
▼
┌────────────────────────────────────────────────────────────────┐
│ WebAuthorizationManager (인가 처리) │
│ · 사용자 권한(ROLE) 확인 │
│ · 요청 URL 접근 가능 여부 판단 │
└────────────────────────────────┬───────────────────────────────┘
│ 접근 허용
▼
[Controller 실행]
┌─────────────────────────────────────────────────────────────────────┐
│ Spring Security 컴포넌트 지도 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [설정 클래스] │
│ ┌─────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ ApiSecurityConfig │ │ WebSecurityConfig │ │
│ │ Order=1 │ │ Order=2 │ │
│ │ /api/**, /common/ │ │ 나머지 모든 URL │ │
│ │ api/** │ │ │ │
│ └─────────────────────┘ └─────────────────────────────────────┘ │
│ │
│ [필터 - 인증 처리] │
│ ┌───────────────────────────────────┐ │
│ │ ApiJwtAuthentication │ JWT 토큰 검증 │
│ │ Filter │ (API 요청마다 실행) │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ ApiJsonLoginAuthentication │ API 로그인 처리 │
│ │ Filter │ (POST /common/api/auth/...) │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ WebIdPasswordLoginAuthentication │ 웹 로그인 처리 │
│ │ Filter │ (POST /common/auth/...) │
│ └───────────────────────────────────┘ │
│ │
│ [Provider - 패스워드 검증] │
│ ┌───────────────────────────────────┐ │
│ │ WebIdPasswordLoginAuthentication │ 아이디/패스워드 검증 │
│ │ Provider │ (웹 로그인 전용) │
│ └───────────────────────────────────┘ │
│ │
│ [UserDetails - 사용자 정보] │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ CustomUserDetailsService → CustomUserDetails │ │
│ │ (DB에서 사용자 조회) (id, name, role, password) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [Handler - 성공/실패 처리] │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ API 성공 핸들러 │ │ Web 성공 핸들러 │ │
│ │ · JWT 토큰 발급 │ │ · JWT 발급 + 세션 저장 │ │
│ │ · JSON 응답 │ │ · /result 리다이렉트 │ │
│ └────────────────────┘ └────────────────────────────────────┘ │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ API 실패 핸들러 │ │ Web 실패 핸들러 │ │
│ │ · 401 JSON 응답 │ │ · 로그인 페이지 리다이렉트 │ │
│ └────────────────────┘ └────────────────────────────────────┘ │
│ │
│ [Authorization - 접근 권한 판단] │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ApiAuthorizationManager│ │ WebAuthorizationManager │ │
│ │ · JWT 토큰 유효성 확인 │ │ · 세션/권한 확인 │ │
│ │ · ROLE 권한 체크 │ │ · ROLE 권한 체크 │ │
│ └─────────────────────────┘ └─────────────────────────────────┘ │
│ │
│ [EntryPoint/Handler - 예외 처리] │
│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │
│ │ ApiAuthentication │ │ WebAuthentication │ │
│ │ EntryPoint (401) │ │ EntryPoint (401) │ │
│ │ · 미인증 JSON 응답 │ │ · 로그인 페이지 리다이렉트 │ │
│ └──────────────────────┘ └────────────────────────────────────┘ │
│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │
│ │ ApiAccessDenied │ │ WebAccessDenied │ │
│ │ Handler (403) │ │ Handler (403) │ │
│ │ · 권한없음 JSON 응답 │ │ · 403 에러 페이지 │ │
│ └──────────────────────┘ └────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ JWT 토큰 구조 │
├───────────────────────────────────────────────────────────────────┤
│ │
│ Access Token (유효기간: 1시간) │
│ ┌──────────┐ ┌────────────────────────┐ ┌─────────────────┐ │
│ │ Header │ . │ Payload │ . │ Signature │ │
│ │ alg, typ │ │ id, role, exp, iat ... │ │ (비밀키로 서명) │ │
│ └──────────┘ └────────────────────────┘ └─────────────────┘ │
│ │
│ Refresh Token (유효기간: 24시간) │
│ · Access Token 만료 시 새로운 Access Token 발급에 사용 │
│ · DB에 저장되어 관리됨 │
│ │
└───────────────────────────────────────────────────────────────────┘
[토큰 갱신 흐름]
클라이언트 서버
│ │
│──── API 요청 (만료된 토큰) ────────▶│
│ │
│◀─── 401 (토큰 만료) ───────────────│
│ │
│──── POST /common/api/auth/refresh──▶│
│ Refresh Token 전송 │ · DB에서 Refresh Token 검증
│ │ · 유효하면 새 Access Token 발급
│◀─── 새 Access Token 반환 ──────────│
│ │
│──── 새 토큰으로 API 재요청 ─────────▶│
[단일 서버 환경 (Redis 없음)]
┌──────────────────┐
│ WAS 서버 │
│ 메모리에 세션 저장 │◀── 사용자 로그인
│ Session: {abc} │
└──────────────────┘
→ 서버 1대일 때는 문제없음
[다중 서버 환경 (Redis 없으면 문제 발생)]
┌─────────────┐ ┌─────────────┐
│ WAS 서버1 │ │ WAS 서버2 │
│ Session:{abc}│ │ 세션 없음! │◀── 같은 사용자가 서버2로 연결되면
└─────────────┘ └─────────────┘ 세션을 찾지 못해 로그아웃됨!
[다중 서버 환경 (Redis 사용 - 이 시스템)]
┌─────────────┐ ┌─────────────┐
│ WAS 서버1 │ │ WAS 서버2 │
└──────┬──────┘ └──────┬──────┘
└──────────┬────────┘
▼
┌─────────────┐
│ Redis │◀── 세션을 중앙 저장소에 저장
│ Session:{abc}│ 어느 서버로 연결되든 동일한 세션 사용
└─────────────┘
| 설정 항목 | 값 | 설명 |
|---|---|---|
| 세션 유효 시간 | 6,000초 (약 100분) | 마지막 활동 후 자동 만료 |
| 적용 환경 | real, dr, dev, test | 운영/개발 환경에서만 적용 |
| 로컬 환경 | 미적용 | 로컬 개발 시 JVM 메모리 세션 사용 |
┌──────────────────────────────────────────────────────────────────┐
│ 세션 생명주기 │
└──────────────────────────────────────────────────────────────────┘
[로그인]
브라우저 → 서버
서버: 세션 생성 (JSESSIONID 발급)
서버: JWT 토큰을 세션에 저장
서버 → 브라우저: JSESSIONID 쿠키 전달
↓
[이후 요청들]
브라우저 → 서버 (쿠키에 JSESSIONID 자동 포함)
서버: Redis에서 세션 조회
서버: 세션에서 JWT 토큰 꺼내기
서버: 인증된 사용자로 처리
↓
[동시 로그인 시도]
다른 브라우저에서 같은 계정으로 로그인
→ 기존 세션 만료 처리
→ 기존 브라우저: 다음 요청 시 /common/auth/login?expired 로 이동
↓
[로그아웃]
브라우저 → POST /common/auth/logout-processing
서버: 세션 삭제 (Redis에서 제거)
서버: JSESSIONID 쿠키 삭제
서버 → 브라우저: 로그인 페이지 리다이렉트
↓
[세션 만료 (타임아웃)]
마지막 요청 후 6,000초 경과
Redis에서 자동 삭제
다음 요청 시 → 로그인 페이지 이동
[User A: PC에서 로그인 중]
Session: "session_A" → Redis에 저장됨
SessionRegistry에 "session_A" 등록됨
[User A: 모바일에서 동일 계정으로 로그인 시도]
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ ConcurrentSessionControlAuthenticationStrategy │
│ · SessionRegistry에서 현재 "user_A"의 세션 목록 확인 │
│ · 기존 세션 "session_A"가 있으므로 → 만료 표시 │
│ (session_A.expireNow() 호출) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ SessionFixationProtectionStrategy │
│ · 모바일 로그인용 새 세션 ID 생성 ("session_B") │
│ · 세션 고정 공격 방지 │
└──────────────────────────────────────────────────────────────────┘
│
▼
모바일: 로그인 성공, "session_B" 사용
PC: 다음 요청 시 "session_A"가 만료됨 → /common/auth/login?expired 이동
┌──────────────────────────────────────────────────────────────────────┐
│ 배치 마스터 Job 실행 아키텍처 │
└──────────────────────────────────────────────────────────────────────┘
[운영자/관리자]
│
│ Job 등록 / 스케줄 설정 (화면에서)
▼
┌──────────────────────────────────────────┐
│ 배치 마스터 시스템 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ DB (Oracle) │ │
│ │ · JOB 테이블 (작업 정보) │ │
│ │ · CALENDAR 테이블 (캘린더) │ │
│ │ · RESULT 테이블 (실행 결과) │ │
│ └──────────┬───────────────────────┘ │
│ │ 앱 시작 시 전체 로드 │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ SchedulerService │ │
│ │ registerAllJobsToScheduler() │ │
│ │ · DB에서 모든 Job 조회 │ │
│ │ · 각 Job을 Quartz에 등록 │ │
│ └──────────┬───────────────────────┘ │
│ │ │
│ ┌────────┴─────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Calendar │ │ Cron │ │
│ │ JobHandler │ │ JobHandler │ │
│ │ (특정 날짜) │ │ (반복 실행) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ Quartz Scheduler │ │
│ │ ThreadPool: 10개 │ │
│ │ ShedLock: 중복 실행 방지 │ │
│ └──────────────────┬───────────────┘ │
│ │ 트리거 발동 │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ CalendarJob │ │ CronJob │ │
│ │ Executor │ │ Executor │ │
│ │ execute() │ │ execute() │ │
│ └──────┬───────┘ └────────┬────────┘ │
│ └──────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ SchedulerProducer │ │
│ │ sendJobMessage() │ │
│ │ → Kafka 토픽으로 메시지 발행 │ │
│ └─────────────────────────────────┘ │
└──────────────────────┬───────────────────┘
│
│ Kafka 메시지
▼
┌─────────────────────────────┐
│ Kafka Broker │
│ Topic: execute-cron-job │
│ Topic: execute-calendar-job│
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Worker 서버 (Consumer) │
│ 실제 배치 작업 수행 │
│ (파일 처리, DB 처리 등) │
└──────────────┬──────────────┘
│ 결과 저장
▼
┌───────────┐
│ Oracle │
│ RESULT │
│ 테이블 │
└───────────┘
[개념] 특정 날짜에 실행하는 Job
예) 매월 말일, 공휴일 제외한 영업일, 특정 날짜들
[등록 방식]
1. 캘린더에 실행할 날짜들 등록
2. Job에 해당 캘린더 연결
3. Quartz: CalendarIntervalTrigger 또는 CustomCalendar로 스케줄 등록
[실행 흐름]
DB의 캘린더 날짜 목록
↓
CalendarJobHandler.registerJob()
↓
Quartz Scheduler 등록
↓
해당 날짜 도달 시 CalendarJobExecutor.execute() 호출
↓
Kafka 메시지 발행 (execute-calendar-job 토픽)
[개념] Cron 표현식으로 반복 실행하는 Job
예) 매일 오전 2시, 매주 월요일 오전 9시, 매시간 정각
[Cron 표현식 형식]
┌────────── 초 (0-59)
│ ┌──────── 분 (0-59)
│ │ ┌────── 시 (0-23)
│ │ │ ┌──── 일 (1-31)
│ │ │ │ ┌── 월 (1-12)
│ │ │ │ │ ┌ 요일 (0-7, 0과 7은 일요일)
│ │ │ │ │ │
0 2 * * * * → 매일 오전 2시 0분 0초 실행
[실행 흐름]
DB의 Cron 표현식
↓
CronJobHandler.registerJob()
↓
Quartz Scheduler에 CronTrigger 등록
↓
트리거 발동 시 CronJobExecutor.execute() 호출
↓
Kafka 메시지 발행 (execute-cron-job 토픽)
[문제 상황: ShedLock 없을 때]
서버 A: Quartz Trigger 발동 → Job 실행 시작
서버 B: Quartz Trigger 발동 → Job 실행 시작 ← 같은 Job이 동시에 2번 실행!
[해결: ShedLock 사용]
서버 A: Quartz Trigger 발동
└─ ShedLock: DB에 Lock 획득 시도
├─ 성공 → Job 실행 (최대 10분 Lock 유지)
└─ 실패 → Job 건너뜀
서버 B: Quartz Trigger 발동
└─ ShedLock: DB에 Lock 획득 시도
└─ 실패 (A가 이미 Lock 보유) → Job 건너뜀
결과: 어느 환경이든 Job은 딱 1번만 실행됨
┌────────────────────────────────────────────────────────────────────┐
│ Kafka 메시지 흐름 │
└────────────────────────────────────────────────────────────────────┘
Producer (이 시스템) Consumer (Worker 서버들)
│ │
│ Topic: execute-cron-job │
│─── {"jobId":"J001", "params":{...}} ──▶│
│ │ Job 실행
│ Topic: execute-calendar-job │
│─── {"jobId":"J002", "params":{...}} ──▶│
│ │
│ Topic: file-upload │
│─── {"fileId":"F001", "path":"..."} ───▶│
│ │
│ Topic: push-message │
│◀── {"type":"complete", "msg":"..."} ───│ 작업 완료 알림
│ │
▼ │
SSE (Server-Sent Events) │
→ 브라우저로 실시간 푸시 │
[Kafka 설정]
· 자동 커밋: OFF (수동 커밋으로 메시지 유실 방지)
· 최대 폴링: 10건 (한 번에 최대 10개 메시지 처리)
· 오프셋 초기화: latest (가장 최신 메시지부터)
[SSE 연결 수립]
브라우저 → GET /common/sse/subscribe
서버: SSE 연결 생성, SseEmitter 등록
[이벤트 발생 시]
Worker 서버 → Kafka push-message 토픽에 메시지 발행
↓
PushMessageKafkaConsumer 메시지 수신
↓
SseEmitterService.sendMessageToClient()
↓
브라우저: 실시간으로 알림 수신 (페이지 새로고침 없이)
┌──────────────────────────────────────────────┐
│ 화면 구조 │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ 상단 네비게이션 (topbar) │ │
│ └────────────────────────────────────────┘ │
│ ┌───────────┐ ┌──────────────────────────┐ │
│ │ │ │ │ │
│ │ 사이드바 │ │ 메인 컨텐츠 영역 │ │
│ │ (sidebar) │ │ │ │
│ │ │ │ · Job 목록 │ │
│ │ · 결과 │ │ · 검색 필터 │ │
│ │ · Job │ │ · 데이터 테이블 │ │
│ │ · 캘린더 │ │ · 모달 창 │ │
│ │ │ │ │ │
│ └───────────┘ └──────────────────────────┘ │
│ ┌────────────────────────────────────────┐ │
│ │ footer │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
| 화면 | URL | 주요 기능 |
|---|---|---|
| 로그인 | /common/auth/login |
아이디/패스워드 로그인 |
| 메인 | / |
시스템 메인 화면 |
| 결과 조회 | /result |
배치 실행 결과 목록/상세 |
| Job 관리 | /job |
Job 등록/수정/삭제/조회 |
| 캘린더 관리 | /calendar |
실행 날짜 캘린더 관리 |
[Job 관리 화면 (/job)]
┌────────────────────────────────────────────────────────────────┐
│ 검색 조건 │
│ [Job명] [상태] [스케줄타입] [검색] │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ Job 목록 [+ 등록] │
│ ┌────────┬───────────┬──────────┬────────┬──────────────────┐ │
│ │ Job ID │ Job명 │ 스케줄 │ 상태 │ 작업 │ │
│ ├────────┼───────────┼──────────┼────────┼──────────────────┤ │
│ │ J001 │ 일일정산 │ CRON │ 활성 │ [상세] [수정] [삭제] │
│ │ J002 │ 월말정산 │ CALENDAR │ 활성 │ [상세] [수정] [삭제] │
│ └────────┴───────────┴──────────┴────────┴──────────────────┘ │
└────────────────────────────────────────────────────────────────┘
[Job 등록 모달]
· Job ID, Job명 입력
· 스케줄 타입 선택 (CRON / CALENDAR)
· CRON 선택 시: Cron 표현식 입력 (가이드 모달 제공)
· CALENDAR 선택 시: 캘린더 선택
· 실행 경로, 파라미터 설정
| 환경 | Profile | 용도 | Redis | 포트 |
|---|---|---|---|---|
| 로컬 | local |
개발자 로컬 개발 | 미사용 (JVM 세션) | 80 |
| 개발 | dev |
통합 개발 환경 | 사용 | - |
| 테스트 | test |
QA/테스트 환경 | 사용 | - |
| DR | dr |
재해복구 서버 | 사용 | - |
| 운영 | real |
실제 운영 환경 | 사용 | - |
src/main/resources/
├── application.yml ← 모든 환경 공통 설정
├── application-local.yml ← 로컬 전용 설정
├── application-dev.yml ← 개발 전용 설정
├── application-dr.yml ← DR 전용 설정
├── application-real.yml ← 운영 전용 설정
├── application-test.yml ← 테스트 전용 설정
│
├── config-local/datasource.yml ← 로컬 DB 접속 정보
├── config-dev/datasource.yml ← 개발 DB 접속 정보
├── config-dr/datasource.yml ← DR DB 접속 정보
├── config-real/datasource.yml ← 운영 DB 접속 정보
└── config-test/datasource.yml ← 테스트 DB 접속 정보
# 로컬 환경 실행
./gradlew bootRun --args='--spring.profiles.active=local'
# 개발 환경 실행
java -jar app.jar --spring.profiles.active=dev
# 운영 환경 실행
java -jar app.jar --spring.profiles.active=real| 설정 항목 | 값 | 설명 |
|---|---|---|
| JWT Access Token 유효기간 | 3,600,000ms (1시간) | API 인증 토큰 |
| JWT Refresh Token 유효기간 | 86,400,000ms (24시간) | 토큰 갱신용 |
| Swagger UI 경로 | /apidoc |
API 문서 접근 URL |
| MyBatis 쿼리 타임아웃 | 10초 | DB 쿼리 최대 실행 시간 |
| Kafka 최대 폴링 수 | 10건 | 1회에 처리할 메시지 수 |
| ShedLock 최대 잠금 시간 | 10분 | 분산 락 최대 유지 시간 |
| 설정 항목 | 값 | 설명 |
|---|---|---|
| 최대 커넥션 수 | 60 | 동시 사용 가능한 최대 DB 연결 |
| 최소 유지 커넥션 | 20 | 항상 유지하는 최소 연결 수 |
| 커넥션 대기 시간 | 30초 | 커넥션 획득 최대 대기 |
| 커넥션 최대 수명 | 900초 (15분) | 커넥션 재생성 주기 |
| 유휴 커넥션 만료 | 300초 (5분) | 미사용 커넥션 반환 시간 |
[ ] 1. 전일 배치 작업 실행 결과 확인
→ /result 화면에서 성공/실패 여부 확인
→ 실패한 Job이 있으면 원인 파악 및 재실행
[ ] 2. 스케줄러 동작 상태 확인
→ Quartz Scheduler 정상 구동 여부
→ 등록된 Job 수와 DB Job 수 일치 여부
[ ] 3. 로그 모니터링
→ 애플리케이션 로그 파일 확인
(./log/krx-nlds-bat-mst.log)
→ ERROR 레벨 로그 발생 여부
[ ] 4. 서버 자원 모니터링
→ /actuator/health 엔드포인트 확인
→ DB 커넥션 풀 사용량
→ Kafka Consumer lag 확인
개발/테스트 환경에서
/apidoc접근 시 전체 API 목록 확인 가능
| 분류 | 주요 API 경로 | 설명 |
|---|---|---|
| 인증 | POST /common/api/auth/login-processing |
API 로그인 |
| 인증 | POST /common/api/auth/refresh |
토큰 갱신 |
| Job | POST /api/create/job |
Job 등록 |
| Job | GET /api/read/jobs |
Job 목록 조회 |
| Job | POST /api/update/job |
Job 수정 |
| Job | POST /api/delete/job |
Job 삭제 |
| 캘린더 | POST /api/create/calendar |
캘린더 등록 |
| 캘린더 | GET /api/read/calendars |
캘린더 목록 |
| 결과 | GET /api/read/results |
실행 결과 조회 |
| 스케줄러 | POST /api/scheduler/register |
스케줄 수동 등록 |
⚠️ 운영 환경에서 반드시 확인할 사항
1. PERMIT_ALL_LIST에 "/**" 가 있으면 안 됨 (개발 편의용 설정)
→ ApiSecurityConfig.PERMIT_ALL_LIST 확인
→ WebSecurityConfig.PERMIT_ALL_LIST 확인
2. Swagger UI는 운영 환경에서 접근 제한 필요
→ /apidoc, /swagger-ui/**, /v3/api-docs/** 경로 차단 검토
3. JWT 비밀키는 환경별로 다르게 설정해야 함
→ application-*.yml의 jwt.secret 값 확인
4. HTTPS 적용 시 WebSecurityConfig의 HSTS 설정 활성화 필요
→ http.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)))
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 로그인 후 세션 유지 안 됨 | Redis 연결 문제 | Redis 서버 상태 및 접속 정보 확인 |
| JWT 토큰 만료 오류 반복 | Access Token 유효시간 | Refresh Token으로 갱신 API 호출 |
| Job이 두 번 실행됨 | ShedLock DB 테이블 문제 | ShedLock 테이블 상태 확인 |
| 특정 Job이 실행 안 됨 | Quartz 스케줄 미등록 | 스케줄러 재시작 또는 수동 등록 API 호출 |
| 화면은 열리는데 API 오류 | 권한 설정 문제 | 사용자 ROLE 및 URL 권한 테이블 확인 |
| Kafka 메시지 처리 지연 | Consumer lag 증가 | Consumer 서버 상태 및 토픽 파티션 확인 |
| 클래스명 | 파일 경로 |
|---|---|
| ApiSecurityConfig | ldsframework/common/config/security/ApiSecurityConfig.java |
| WebSecurityConfig | ldsframework/common/config/security/WebSecurityConfig.java |
| ApiJwtAuthenticationFilter | ldsframework/common/config/security/filter/ApiJwtAuthenticationFilter.java |
| JwtTokenProvider | ldsframework/common/config/jwt/JwtTokenProvider.java |
| RedisConfig | ldsframework/common/config/redis/RedisConfig.java |
| SchedulerConfig | ldsframework/common/config/batch/SchedulerConfig.java |
| KafkaConfig | ldsframework/common/config/kafka/KafkaConfig.java |
| CustomUserDetails | ldsframework/common/config/security/principal/CustomUserDetails.java |
| 규칙 | 내용 |
|---|---|
| REST API URL 패턴 | /api/{action}/{resource} (예: /api/create/job) |
| 응답 형식 | CommonResponseDto 자동 래핑 (예외: @NoWrap) |
| 권한 체크 | @RequirePermission(type = PermissionType.READ) AOP |
| 쿼리 위치 | 단순 쿼리: @Mapper 어노테이션 / 동적 쿼리: XML 파일 |
| 검증 그룹 | ValidGroups.Insert, ValidGroups.Update, ValidGroups.Delete |
| 공통 예외 | CustomRuntimeException 사용 |
╔══════════════════════════════════════════════════════════════════════════════════════════════════╗
║ 배치 작업 등록 → 스케줄러 등록 → Agent 실행 지시 → 배치 실행 전체 흐름 ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════╝
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PHASE 1 웹 화면을 통한 배치 작업(Job) 등록
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[운영자 브라우저] [jobCreateModal.js] [Spring Security]
│ │ │
│ ① /job 화면 접속 │ │
│──────────────────────────────────────────────────────────────────▶│
│ │ │ WebSecurityConfig
│ ② 작업 목록 화면 렌더링 │ │ 세션 인증 통과
│◀──────────────────────────────────────────────────────────────────│
│ │
│ ③ [작업 등록] 버튼 클릭 │
│──────────────────────────────────▶│
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 2 Quartz Scheduler 등록 (POST /api/scheduler/register 호출 또는 애플리케이션 재기동 시 자동 실행) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[운영자 / 시스템] │ │ ① POST /api/scheduler/register │ ▼ [SchedulerController] │ ▼ [SchedulerService.registerAllJobsToScheduler()] │ ├──────────────────────────────────────────────────────────────────────────┐ │ CAL 타입 처리 │ CRON 타입 처리 ▼ ▼ [CalendarJobHandler.registerJob()] [CronJobHandler.registerJob()] │ │ │ ② jobMapper.selectExecutableJobs() ③ jobMapper.selectJobs(전체) │ → CAL 타입 Job 전체 조회 → 전체 Job 조회 │ │ │ ④ 등록 조건 4가지 동시 검사 ⑤ 등록 조건 2가지 검사 │ ┌─────────────────────────────┐ ┌──────────────────────────┐ │ │ isCalendarJob() │ │ isCronJob() │ │ │ jobSchdlTpNm == "CAL" │ │ jobSchdlTpNm == "CRON" │ │ ├─────────────────────────────┤ ├──────────────────────────┤ │ │ isUsableJob() │ │ isUsableJob() │ │ │ 오늘 >= strtDd │ │ 오늘 >= strtDd │ │ │ 오늘 <= endDd │ │ 오늘 <= endDd │ │ ├─────────────────────────────┤ └──────────────────────────┘ │ │ !isFollowJob() │ 모두 통과 시만 등록 │ │ 후행 Job이 아닌 경우 │ │ │ (대표 그룹 Job만 등록) │ │ ├─────────────────────────────┤ │ │ isRunTodayJob() │ │ │ 캘린더 SQL 실행 │ │ │ → 오늘 날짜가 결과에 포함 │ │ │ 되는지 DB 조회로 판별 │ │ └─────────────────────────────┘ │ 모두 통과 시만 등록 │ │ ⑥ JobDetail 생성 ⑥ JobDetail 생성 │ JobBuilder.newJob(CalendarJobExecutor.class) JobBuilder.newJob(CronJobExecutor.class) │ .withIdentity("[등록일시][실행일시][Job ID]", grpNm) .withIdentity("[등록일시][CRON][Job ID]", grpNm) │ .usingJobData("jobId", ...) .usingJobData("jobId", ...) │ .usingJobData("jobExecutePath", ...) .usingJobData("jobExecutePath", ...) │ .usingJobData("follwJobId", ...) .usingJobData("follwJobId", ...) │ .usingJobData("svrNm", ...) .usingJobData("svrNm", ...) │ │ ⑦ Trigger 생성 ⑦ Trigger 생성 │ TriggerBuilder TriggerBuilder │ .startAt(DateBuilder.todayAt(HH, mm, ss)) .withSchedule( │ → 오늘 특정 시각 1회 실행 CronScheduleBuilder.cronSchedule("0 2 * * *")) │ → 반복 일정으로 실행 │ │ ⑧ scheduler.scheduleJob(jobDetail, trigger) ⑧ scheduler.scheduleJob(jobDetail, trigger) │ → Quartz에 등록 완료 → Quartz에 등록 완료 │ → 다음 실행 시각(Date) 반환 → 다음 실행 시각(Date) 반환 │ │ ⑨ 오늘 실행 예정이면 ⑨ 오늘 실행 예정이면 │ schedulerMapper.insertJobExecutionInfo(...) schedulerMapper.insertJobExecutionInfo(...) │ → EXECUTION_INFO 테이블에 실행 예정 정보 저장 → EXECUTION_INFO 테이블에 실행 예정 정보 저장 │ └──────────────────────────────────────────────────────────────────────────┘ │ [Quartz Scheduler] 등록된 Job 대기 중 (Thread Pool: 10개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 3 스케줄 트리거 발동 → Agent에 실행 지시 (Kafka 메시지 발행) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Quartz Scheduler - 지정된 시각 도달] │ │ ① Trigger 발동 (지정 시각 또는 Cron 조건 충족) │ · ShedLock: DB Lock 획득 시도 │ ├─ 성공 → 계속 진행 (다른 서버에서는 Lock 획득 실패 → 실행 건너뜀) │ └─ 실패 → 이 서버에서 실행 중단 (중복 실행 방지) │ ├──────────────────────────────────────────────┐ │ CAL 타입 │ CRON 타입 ▼ ▼ [CalendarJobExecutor.execute()] [CronJobExecutor.execute()] │ │ │ ② context.getJobDetail() │ ② context.getJobDetail() │ → 등록 시 저장했던 데이터 꺼내기 │ → 등록 시 저장했던 데이터 꺼내기 │ │ │ ③ message 조립 │ ③ message 조립 │ { │ { │ key : jobDetail.key, │ key : jobDetail.key, │ jobId : "J001", │ jobId : "J001", │ jobExecutePath : "/batch/run/daily.sh", │ jobExecutePath : "/batch/run/daily.sh", │ follwJobId : "J002", ← 후행 Job ID │ follwJobId : "J002", │ svrNm : "REAL01" ← 실행 대상 서버 │ svrNm : "REAL01" │ } │ } │ │ └──────────────────────┬───────────────────────┘ │ ▼ [SchedulerProducer.sendJobMessage(topic, message)] │ │ ④ new ProducerRecord<>(topic, message) │ │ ⑤ kafkaTemplate.send(record) ← jsonKafkaTemplate 사용 (HashMap → JSON 직렬화) │ ├──────────────────────────────────────────────┐ │ CAL 타입 │ CRON 타입 ▼ ▼ Topic: "execute-calendar-job" Topic: "execute-cron-job" │ │ └──────────────────┬───────────────────────────┘ │ ▼ [Apache Kafka Broker] 메시지 저장 및 전달 대기 │ │ ⑥ 전송 성공/실패 콜백 │ whenComplete((result, ex) -> { │ 성공: log "offset=N" │ 실패: log "전송 실패" │ })
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 4 Agent(Worker 서버)가 메시지 수신 → 배치 실행 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Apache Kafka Broker] │ │ ① Consumer가 토픽 폴링 (최대 10건씩, 수동 커밋) ▼ [Agent / Worker 서버 - Kafka Consumer] │ │ ② 메시지 역직렬화 (JSON → HashMap) │ { │ jobId : "J001", │ jobExecutePath : "/batch/run/daily.sh", │ follwJobId : "J002", │ svrNm : "REAL01" │ } │ │ ③ svrNm 기반으로 실행 대상 서버 식별 │ │ ④ jobExecutePath 기반으로 배치 프로그램 실행 │ ├──────────────────────────────────────────────────────────────────────┐ │ 선행 Job 성공 시 │ 실행 완료 후 ▼ ▼ [follwJobId 확인] [Oracle DB - RESULT 테이블] follwJobId != null 실행 결과 저장 → 후행 Job이 존재함 · jobId → 후행 Job을 이어서 실행 지시 · 실행 시작/종료 시각 (동일 토픽으로 후행 Job 메시지 발행) · 성공/실패 여부 · 오류 메시지 (실패 시) │ │ ⑤ (선택) Kafka push-message 토픽 │ → PushMessageKafkaConsumer │ → SseEmitterService │ → 브라우저 실시간 알림
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 전체 흐름 요약 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
운영자 │ 화면에서 Job 입력 → [저장] │ ▼ JWT 인증 → JobController → JobService → Oracle DB INSERT │ Job 데이터 저장 │ 스케줄러 등록 명령 (수동 or 재기동) │ │ ▼ ▼ Oracle DB에서 Job 목록 조회 SchedulerService │ │ 조건 검사 ├─ CAL Job → CalendarJobHandler (타입 / 유효기간 / 후행여부 / 오늘실행여부) │ → Quartz에 "오늘 HH:MM" 1회 트리거 등록 │ └─ CRON Job → CronJobHandler → Quartz에 "Cron 표현식" 반복 트리거 등록 │ 지정 시각 / Cron 조건 충족 │ ▼ ShedLock → DB Lock 획득 (중복 방지) │ ▼ CalendarJobExecutor / CronJobExecutor JobDetail 데이터 꺼내기 (jobId, jobExecutePath, svrNm, follwJobId) │ ▼ SchedulerProducer Kafka 메시지 발행 │ ┌──────────────────┴──────────────────┐ ▼ ▼ execute-calendar-job execute-cron-job └──────────────────┬──────────────────┘ │ ▼ Kafka Broker │ ▼ Agent (Worker 서버) 메시지 수신 → 배치 실행 후행 Job 있으면 연쇄 실행 │ ▼ 결과 DB 저장 + 실시간 알림 (SSE)
이 문서는 next-krx-lds-bat-mst 프로젝트의 아키텍처 기준으로 작성되었습니다.