# FTPDJ — 대외기관 파일 수신 어댑터 (인수인계 문서) > **대상 소스**: [`src/com/eactive/eai/adapter/ftp/FTPDJ.java`](../src/com/eactive/eai/adapter/ftp/FTPDJ.java) > **관련 가이드**: [`DJERP_1000_AP_BAP(FTP)개발가이드_v0.9_20260603.pptx`](./DJERP_1000_AP_BAP%28FTP%29개발가이드_v0.9_20260603.pptx) — EMS 등록 절차·화면 조작 중심 > **최종 갱신**: 2026-09-09 ## 0. 이 문서에 대하여 FTPDJ를 **넘겨받아 유지보수할 개발자**를 위한 문서입니다. PPT 가이드가 "화면에서 어떻게 등록하는가"를 다룬다면, 이 문서는 **"등록한 값이 코드에서 어떻게 해석되어 동작하는가"** 를 다룹니다. 특히 FTPDJ의 동작은 4장~10장이 그 내용이며 이 문서의 핵심입니다. 값 검증이 없어 **오타가 조용히 다른 동작으로 이어지는 지점**이 몇 군데 있습니다. 6장(`protocol`)과 7장(`io.bandwidth`)은 반드시 읽고 넘어가십시오. --- ## 1. 저장소 · 빌드 · 배포 ### 1.1 Git 저장소 ``` origin https://git.eactive.synology.me:8090/eapim/djbank/bapweb.git ``` | 브랜치 | 용도 | |---|---| | `master` | 주 브랜치 | | `soxor` | 작업 브랜치 (FTPDJ 개발이 진행된 곳) | FTPDJ 관련 주요 커밋: | 커밋 | 일자 | 내용 | |---|---|---| | `adeec70` | 2026-06-05 | `FileFetchRequestHandler` 구현 (HTTP 요청 진입점 신규) | | `7f98f97` | 2026-09-07 | 취약점 patch version (의존 jar 교체) | ### 1.2 빌드 Ant 기반입니다. Maven/Gradle 설정은 없으며 **jar는 `WebContent/WEB-INF/lib`에 직접 커밋**되어 있습니다. ```bash ant war # clean → compile → dist/BAPWeb.war 생성 ant build # 컴파일만 (build/classes) ``` | 항목 | 값 | |---|---| | Java source/target | **17** (`build.xml`) | | 컴파일 classpath | `WebContent/WEB-INF/lib/*.jar` (단 `ezgatormodule2.jar` 제외) + `libs/*.jar` | | 산출물 | `dist/BAPWeb.war` | | WAS | WebLogic, context-root `/BAPWeb` (`WebContent/WEB-INF/weblogic.xml`) | > `WebContent/WEB-INF/oldlib`은 취약점 조치로 교체된 **구버전 jar 보관용**입니다. 클래스패스에도 WAR에도 포함되지 않습니다(`build.xml`의 war 태스크에서 제외 처리). 되돌릴 일이 없다면 삭제해도 무방합니다. --- ## 2. FTPDJ는 무엇인가 EMS(관리자포탈)의 **대외기관 등록 → 송수신구분코드**에서 `FTPDJ`를 선택하면 이 클래스가 전송 어댑터로 동작합니다. DJBank 공용 SFTP 프록시를 경유해 **대외기관의 파일을 내려받는(Download) 전용 어댑터**입니다. | 항목 | 내용 | |---|---| | 클래스 | `com.eactive.eai.adapter.ftp.FTPDJ` | | 상속 / 구현 | `Transfer` 상속, `SocketService` 구현 | | 지원 동작 | **수신(`recvFile`)만 지원** | | 미지원 | `sendFile()` 호출 시 `UnsupportedOperationException` | | 전송 라이브러리 | JSch 기반 (`FTPUtil.listFilesJSCH`, `FTPUtil.retrieve`) | `SocketService`의 나머지 메서드(`connect`, `isActive`, `getCurrentSocket` 등)는 인터페이스 계약을 채우기 위한 빈 구현입니다. **실제 로직은 `recvFile()` 한 곳에서 시작합니다.** 소스를 처음 읽는다면 여기부터 보십시오. --- ## 3. 전체 동작 흐름 ``` [1] HTTP 요청 (action=DOWNLOAD, remoteCode, remoteFile) │ FileFetchRequestHandler ▼ [2] Job_Queue 테이블에 작업 등록 → 즉시 응답 (status=QUEUED, jobId=UUID) │ 스케줄러가 매분 0초 / 30초에 큐를 집어감 ▼ [3] FTPDJ.recvFile(batchDoc) ← 이 문서의 범위 │ ├─ FtpEnv.init() : 접속정보(DB) + 프로퍼티(DB)로 실행 환경 조립 ← 핵심 ├─ listFilesJSCH() : 원격 디렉터리 목록 조회 후 대상 파일 존재 확인 ├─ fileDownProcess() : 다운로드 → RECVREAL 수신 → RECVARCH 이동 → check 파일 생성 └─ callbackProcess() : callback.url 이 있으면 BAT에 완료 통보 후 응답코드 검증 ``` **비동기 구조**입니다. HTTP 요청은 큐에 넣고 바로 `QUEUED`를 반환하며, 실제 전송은 최대 30초 뒤 스케줄러가 시작합니다. 요청자는 `action=STATUS`로 `jobId`를 조회해 진행 상태를 확인합니다. 요청/응답 JSON 규격은 PPT 가이드 4장을 참조하십시오. --- ## 4. 프로퍼티 목록 (`TelegramInfo{업무구분_대외기관}`) FTPDJ가 **실제로 읽는** 프로퍼티는 아래 8개가 전부입니다. | 키 | 기본값 | 용도 | 비고 | |---|---|---|---| | `remote.path` | `""` | 원격지 기준 경로 | `#YYYYMMDD#` 치환 지원. 요청이 절대경로면 **무시됨** (5장) | | `protocol` | `""` → `sftp_passwd` | 전송 프로토콜 | **값 매핑 주의** (6장) | | `proxy.server` | `""` | SFTP 프록시 서버 | `IP:PORT` 형식. 비우면 직접 접속 | | `connection.timeout` | `30` | 접속 타임아웃(초) | 숫자만. 문자 포함 시 `NumberFormatException` | | `read.timeout` | `30` | 읽기 타임아웃(초) | 동일 | | `io.bandwidth` | `0` | 전송 대역 제한 | `0`=무제한. `K`/`M` 표기 지원 (7장) | | `ftp.transfer.type` | `BINARY` | FTP 전송 모드 | `ASCII`가 필요한 경우에만 지정 | | `callback.url` | `""` | 수신 완료 통보 URL | 비우면 콜백 생략 (10장) | 접속 정보(IP·포트·사용자 ID·비밀번호/SSH Key)와 수신 대상 파일명은 **프로퍼티가 아니라 대외기관 연결 정보 및 HTTP 요청**에서 배치 헤더로 전달됩니다. 프로퍼티에서 찾지 마십시오. --- ## 5. `remote.path` 조립 규칙 원격 경로는 **프로퍼티 단독으로 결정되지 않고** 요청으로 들어온 `remoteFile`과 조합됩니다. ```java remotePath = 프로퍼티 remote.path requestPath = remoteFile 의 디렉터리 부분 remoteFile = remoteFile 의 파일명 부분 remotePath = Paths.get(remotePath).resolve(requestPath).normalize() + "/" ``` ### 5.1 요청이 절대경로면 `remote.path`는 무시된다 Java `Path.resolve()`는 인자가 절대경로일 때 **기준 경로를 버리고 인자를 그대로 반환**합니다. PPT 가이드의 *"remote.path : 요청시 full path 이면 무시됨"* 이 이 동작입니다. | `remote.path` | 요청 `remoteFile` | 최종 원격 경로 | 파일명 | |---|---|---|---| | `/data/recv` | `test.txt` | `/data/recv/` | `test.txt` | | `/data/recv` | `sub/test.txt` | `/data/recv/sub/` | `test.txt` | | `/data/recv` | `/home/soxor/sftp/svr/test.txt` | `/home/soxor/sftp/svr/` ← **remote.path 무시** | `test.txt` | | `""` (미등록) | `/home/soxor/sftp/svr/test.txt` | `/home/soxor/sftp/svr/` | `test.txt` | > 요청자가 절대경로를 보내는 방식이면 `remote.path`는 등록하지 않아도 됩니다. 반대로 `remote.path`로 접근 경로를 **통제하려는 의도라면 이 구조로는 불가능합니다** — 요청자가 절대경로를 보내면 어디든 접근됩니다. 경로 제한이 요건이라면 별도 검증 로직을 추가해야 합니다. ### 5.2 날짜 토큰 치환 조합이 끝난 경로에서 `#YYYYMMDD#` 패턴을 찾아 실제 일자로 치환합니다. ``` 정규식: #YYYYMMDD([+-]\d+)?# ``` | 표기 | 의미 | 예 (오늘이 2026-09-09) | |---|---|---| | `#YYYYMMDD#` | 당일 | `20260909` | | `#YYYYMMDD-1#` | 전일 | `20260908` | | `#YYYYMMDD-7#` | 7일 전 | `20260902` | | `#YYYYMMDD+1#` | 익일 | `20260910` | 등록 예: ``` remote.path = /data/recv/#YYYYMMDD# → /data/recv/20260909/ remote.path = /data/recv/#YYYYMMDD-1# → /data/recv/20260908/ ``` 동작상 유의점: 1. 치환이 발생하면 계산된 일자가 **배치 헤더의 기준일자(BaseDate)로 함께 설정**됩니다. 로그·모니터링의 기준일자가 이 값을 따릅니다. 2. 치환 중 예외가 나도 **작업은 중단되지 않고** 어댑터 로그에만 기록됩니다. 이 경우 경로에 `#YYYYMMDD#` 문자열이 그대로 남아 원격 경로 조회 단계에서 실패합니다. 3. 경로 끝의 `/`는 없으면 자동으로 붙습니다. --- ## 6. `protocol` 값 매핑 (주의) **가장 오해하기 쉬운 항목입니다.** 매칭은 열거형 이름 기준이고, **매칭에 실패해도 오류 없이 조용히 기본값으로 처리**됩니다. | 등록값(대소문자 무관) | 해석 | 내부 코드 | 전송 방식 | |---|---|---|---| | `ftp` | FTP | `0` | 평문 FTP | | `sftp_passwd` | SFTP + 비밀번호 | `1` | SFTP | | `sftp_key` | SFTP + SSH Key | `2` | SFTP | | **미등록 / 공백 / 그 외 모든 값** | **SFTP + 비밀번호** | `1` | SFTP | 즉 아래가 모두 **동일하게 `sftp_passwd`로 동작**합니다. ``` protocol = (미등록) protocol = sftp ← 가이드 표기지만 매핑 대상이 아님 protocol = SFTP protocol = sftp_password ← 오타 ``` 지켜야 할 규칙은 두 가지입니다. - **FTP를 쓰려면 반드시 `ftp`로 정확히** 등록해야 합니다. 오타 시 SFTP로 접속을 시도해 원인 파악이 어려운 실패가 납니다. - **SSH Key 인증을 쓰려면 반드시 `sftp_key`로** 등록해야 합니다. 비워두거나 `sftp`로 두면 비밀번호 인증으로 동작합니다. > 오타가 로그에 남지 않으므로 접속 실패 시 프로퍼티 오타를 우선 의심하십시오. **개선 후보**: 매칭 실패 시 경고 로그를 남기거나 예외를 던지도록 `Protocol.get()`을 수정하는 것이 안전합니다. --- ## 7. `io.bandwidth` 단위 표기 문자열을 그대로 파싱하지 않고 **치환 후 정수 변환**합니다. ```java 값.replace("K", "000").replace("M", "000000").replace(",", "") ``` | 등록값 | 실제 적용값 | 설명 | |---|---|---| | `0` 또는 미등록 | `0` | 제한 없음 | | `500K` | `500000` | | | `10M` | `10000000` | | | `1,024` | `1024` | 콤마 제거 | | `500k` | **예외 발생** | 소문자 `k`는 치환되지 않음 → `NumberFormatException` | | `500KB` | **예외 발생** | `B`가 남음 | **대문자 `K` / `M`만 사용하십시오.** 표기가 잘못되면 `FtpEnv.init()` 단계에서 예외가 나 다운로드가 시작되지 않습니다. --- ## 8. 파일 저장 위치 프로퍼티 (`ResponseRcvDirInfo`) 수신 파일이 놓이는 위치는 대외기관 프로퍼티가 아니라 **공통 그룹 `ResponseRcvDirInfo`** 에서 결정됩니다. (EMS 메뉴: 환경정보 → 공통코드관리 → 프라퍼티 관리) | 키 | 용도 | FTPDJ에서의 역할 | |---|---|---| | `rcv.real.directory` | 수신 중 임시 디렉터리 | 다운로드가 **먼저 떨어지는 곳** | | `rcv.arch.directory` | 수신 완료 보관 디렉터리 | 완료 후 **이동되는 최종 위치**. check 파일도 여기 | | `rcv.error.directory` | 오류 파일 디렉터리 | 실패 시 받다 만 파일을 이동 | | `rcv.root.directory` | 빈 완료표시 파일 위치 | 0바이트 파일 생성 | | `rcv.root.ext` | check 파일 확장자 | **미등록 시 `.chk`** | | `retension.day.*` | 폴더별 보존일수 | 값(일) 경과 시 자동 삭제. 공백/비정수면 삭제 안 함 | ### 8.1 경로 조립과 파일 생성 순서 작업 등록 시 수신 디렉터리는 `rcv.real.directory/업무구분코드/대외기관코드` 로 설정됩니다. ``` ① 다운로드 {rcv.real.directory}/{업무구분}/{대외기관}/{파일명} ② 이동 {rcv.arch.directory}/{업무구분}/{대외기관}/{파일명} ③ check 파일 {rcv.arch.directory}/{업무구분}/{대외기관}/{파일명}{rcv.root.ext} ④ 빈 표시 파일 {rcv.root.directory}/{업무구분}/{대외기관}/{파일명} ``` - ①→② 이동은 `REPLACE_EXISTING`이므로 **같은 이름의 기존 파일은 덮어씁니다.** - ③, ④는 생성 전에 기존 파일이 있으면 **먼저 삭제**하고 새로 만듭니다. - 다운로드 중 실패하면 ①의 파일을 `rcv.error.directory`로 옮기고, 이동조차 실패하면 조용히 무시합니다. > **순서를 바꾸지 마십시오.** check 파일(③)은 반드시 본 파일 이동(②) **이후**에 생성됩니다. 수신측(BAT)이 check 파일을 폴링하는 구조이므로, check 파일이 보이는 시점에는 본 파일이 이미 최종 위치에 있어야 합니다. 순서를 뒤집으면 BAT가 미완성 파일을 집어갈 수 있습니다. --- ## 9. check 파일 포맷 `rcv.arch.directory` 아래 `{파일명}.chk`로 생성되며, 기존 EAI(BAT)가 폴링으로 읽어갑니다. ``` %-30s %8s %010d %c %-100s @@ ``` | 위치(1-based) | 길이 | 내용 | |---|---|---| | 1 – 30 | 30 | InterfaceID (거래구분코드 `bizCode`), 좌측정렬 공백 패딩 | | 31 – 38 | 8 | 기준일자 `YYYYMMDD` (생성 시점의 현재일자) | | 39 – 48 | 10 | 파일 건수, 0 패딩 (**항상 `0000000001`**) | | **49** | 1 | **상태값** — 생성 시 `'0'` 고정 | | 50 – 149 | 100 | 파일명, 좌측정렬 공백 패딩 | | 150 – 151 | 2 | 종료 표시 `@@` | 총 **151바이트**입니다. 49번째 바이트의 상태값은 콜백 응답 검증에서 다시 쓰입니다(10장). 상수 `BAT_RET_CODE_IDX = 49`가 이 위치를 가리킵니다. > **BAT와의 인터페이스 규격입니다.** 포맷을 바꾸면 수신측 파싱이 깨집니다. 변경 시 반드시 BAT 담당자와 합의하십시오. --- ## 10. `callback.url` 동작 `callback.url`이 **비어 있으면 콜백 단계 자체를 건너뜁니다.** 등록되어 있으면 다음을 호출합니다. ``` GET {callback.url}?eaibatMsg={check 파일과 동일한 151바이트 문자열} ``` 응답 검증 조건은 세 가지이며, **하나라도 어긋나면 작업 전체가 실패 처리**됩니다. | 검증 | 실패 시 메시지 | |---|---| | HTTP 상태코드 `200` | `Response code is not 200.(코드)` | | 응답 본문 길이 ≥ 49바이트 | `BAT return message is too short.` | | 응답 본문 **49번째 바이트 = `'1'`** | `BAT return code is not '1'.(값)` | 응답 인코딩은 `Content-Type` 헤더의 charset을 따르고, 없으면 UTF-8로 처리합니다. > **알아둘 것:** 파일 수신과 저장이 모두 성공해도 콜백 검증에서 실패하면 작업은 오류로 기록됩니다. --- ## 11. 의존 라이브러리 취약점 조치 내역 | 취약점 번호 | 해당 jar 파일 | 조치 내용 | |:-------------------|:-----------------------------|:------------------------------------| | CVE-2023-46604
CVE-2026-34197 | activemq-broker-5.15.13.jar
activemq-client-5.15.13.jar
activemq-console-5.15.13.jar
activemq-spring-5.15.13.jar | **5.15.13 → 5.19.11** 교체.
이 CVE의 최소 수정 버전은 5.15.16이며,
현재 활성 라인 최신인 5.19.11을 적용했습니다. | | CVE-2023-48795 | jsch-0.1.55.jar | **com.jcraft:jsch 0.1.55
→ com.github.mwiede:jsch 2.28.7** 교체.
Terrapin은 0.2.15에서
strict KEX 지원으로 수정되었습니다. | | CVE-2023-20863 | spring-core-5.3.27.jar 등
spring 계열 14건 | **현재 버전 5.3.27이 바로 이 CVE의 수정 버전입니다.**
영향 범위는 5.3.0–5.3.26 및 6.0.0–6.0.7이고,
수정 릴리스가 5.3.27 / 6.0.8입니다.
\*\* 조치 불필요. 스캔 도구가 영향 범위 상한을
잘못 잡은 것으로 보이며, 판정 근거 재확인을 요청드립니다 | | CVE-2026-22740 | spring-core-5.3.27.jar 등
spring 계열 14건 | 이 결함은 **WebFlux 서버 애플리케이션 전용**입니다
(멀티파트 임시파일 미삭제).
본 애플리케이션은 `spring-webmvc` 기반이며
`spring-webflux` jar 자체가 배포물에 없습니다. |