# ElinkTransactionContext 아키텍처 ## 개요 `ElinkTransactionContext`는 트랜잭션(거래) 범위에서 컨텍스트 변수를 관리하기 위한 ThreadLocal 기반 유틸리티입니다. JEP 함수 등 EAIMessage나 Properties에 직접 접근할 수 없는 클래스에서 트랜잭션 범위 변수에 접근할 수 있도록 합니다. --- ## 핵심 설계 원칙 1. **ThreadLocal이 직접 저장소**: EAIMessage의 Map을 참조하지 않고 ThreadLocal에 직접 저장 2. **Lazy Initialization**: 최초 put 시 Map 자동 생성 (명시적 초기화 불필요) 3. **clear()는 필수**: 스레드 풀 환경에서 메모리 누수 방지 4. **비동기 전달용 필드 분리**: `transactionContextTransfer`는 JMS 전달 전용 --- ## 구성 요소 ### 1. ElinkTransactionContext (저장소) ```java // ElinkTransactionContext.java (elink-online-core) public class ElinkTransactionContext { private static final ThreadLocal> THREAD_LOCAL = new ThreadLocal<>(); // 컨텍스트 정리 (필수) public static void clear(); // 비동기 전달용 public static Map getAll(); // 복사본 반환 public static void restore(Map ctx); // 복원 // 명시적 접근자 (Lazy init - 최초 put 시 Map 생성) public static void setSeedSalt(String seedSalt); public static String getSeedSalt(); public static void setEncryptionKey(String encryptionKey); public static String getEncryptionKey(); } ``` - **위치**: `elink-online-core` - **역할**: ThreadLocal 기반 트랜잭션 변수 저장소 - **특징**: - 명시적 초기화 불필요 (Lazy init) - `clear()`는 필수 (finally 블록에서 호출) ### 2. EAIMessage.transactionContextTransfer (전달용) ```java // EAIMessage.java (elink-online-common) /** * [주의] 비동기 JMS 전달 전용 - 직접 사용 금지! */ private Map transactionContextTransfer; public Map getTransactionContextTransfer(); public void setTransactionContextTransfer(Map ctx); ``` - **위치**: `elink-online-common` - **역할**: 비동기 거래 시 JMS를 통한 컨텍스트 전달 - **특징**: - `Serializable` - ObjectMessage로 직렬화 - **직접 데이터 저장 용도로 사용 금지** - 복원 후 즉시 null로 초기화 --- ## 처리 흐름 ### 동기 (Sync) 처리 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ ApiAdapterController.callApi() │ │ │ │ │ ├─ try { │ │ │ service.callApi() │ │ │ └─ RequestProcessor.hookup() │ │ │ │ │ │ │ ├─ [컨텍스트 사용 가능 - Lazy Init] ───────────────── │ │ │ │ ElinkTransactionContext.setSeedSalt("value") │ │ │ │ └─ 최초 호출 시 Map 자동 생성 │ │ │ │ │ │ │ ├─ FlowRouter.process() │ │ │ │ └─ ElinkESBProcessProxy → Process 호출 │ │ │ │ │ │ │ ├─ [반환 후에도 컨텍스트 사용 가능] ✓ │ │ │ │ │ │ │ └─ 응답 반환 │ │ │ } │ │ └─ finally { │ │ ElinkTransactionContext.clear() ← 최상위 진입점에서 해제 │ │ } │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 비동기 (Async) 처리 - Queue 사용 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ [송신 스레드] │ │ │ │ ApiAdapterController.callApi() │ │ │ │ │ ├─ try { │ │ │ RequestProcessor.hookup() │ │ │ ├─ ElinkTransactionContext.setSeedSalt("value") │ │ │ │ └─ ThreadLocal에 직접 저장 │ │ │ │ │ │ │ └─ FlowRouter.process() │ │ │ ├─ [컨텍스트 복사] ───────────────────────────────────── │ │ │ │ eaiMessage.setTransactionContextTransfer( │ │ │ │ ElinkTransactionContext.getAll() │ │ │ │ ) │ │ │ └─ JMSSender.sendToQueue(eaiMessage, ...) │ │ │ └─ EAIMessage 직렬화 (transactionContextTransfer 포함)│ │ │ } │ │ └─ finally { ElinkTransactionContext.clear() } │ │ │ │ └──────────────────────────┼──────────────────────────────────────────────┘ │ [JMS Queue] │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ [수신 스레드 - Consumer] │ │ │ │ FCQueueReceiver / DefaultQueueConsumer │ │ │ │ │ ├─ EAIMessage 역직렬화 (transactionContextTransfer 복원) │ │ │ │ │ └─ ElinkESBProcessProxy.callElinkESBProcess(BaseFCProcess, ...) │ │ │ │ │ ├─ [조건 충족: ASYNC && !outbound] │ │ │ ├─ ElinkTransactionContext.restore( │ │ │ │ eaiMessage.getTransactionContextTransfer() │ │ │ │ ) │ │ │ └─ eaiMessage.setTransactionContextTransfer(null) ← 즉시 초기화 │ │ │ │ ├─ BaseFCProcess.callService() │ │ │ └─ ElinkESBProcessProxy(..., RESTProcess, ...) │ │ │ └─ [조건 미충족: outbound] → 복원 안함 │ │ │ └─ RESTProcess 호출 │ │ │ └─ ElinkTransactionContext.getSeedSalt() │ │ │ └─ 송신 스레드에서 설정한 값 조회 ✓ │ │ │ │ │ └─ finally { ElinkTransactionContext.clear() } │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 초기화/해제 위치 | 위치 | 역할 | 비고 | |------|------|------| | `ApiAdapterController.callApi()` | 해제 (finally) | 동기 - 최상위 진입점 | | `RequestProcessorSupport.execute()` | 해제 (finally) | 동기 - 안전망 (이중 해제) | | `FlowRouter.process()` | 컨텍스트 복사 | 비동기 송신 전 | | `ElinkESBProcessProxy.callElinkESBProcess()` | 복원/해제 (조건부) | 비동기 수신 시 | ### ElinkESBProcessProxy 복원 조건 ```java boolean shouldManageContext = eaiMessage != null && EAIMessageKeys.ASYNC_SVC.equals(eaiMessage.getSvcTsmtUsgTp()) // ASYNC 거래 && (serviceURI == null || !serviceURI.startsWith("com.eactive.eai.outbound")); // outbound가 아닐 때 if (shouldManageContext) { ElinkTransactionContext.restore(eaiMessage.getTransactionContextTransfer()); eaiMessage.setTransactionContextTransfer(null); // 복원 후 즉시 초기화 } ``` - **ASYNC 거래**: 동기 거래는 Lazy init으로 자동 처리 - **outbound가 아닐 때**: BaseFCProcess 호출 시에만 복원, RESTProcess 호출 시에는 이미 복원된 상태 > **Note**: `clear()`는 멱등(idempotent)하므로 여러 번 호출되어도 안전합니다. --- ## 사용 예시 ### 값 설정 (RequestProcessor, DefaultProcess 등) ```java // SeedSalt 설정 (최초 호출 시 Map 자동 생성) ElinkTransactionContext.setSeedSalt("o123y498172"); // EncryptionKey 설정 ElinkTransactionContext.setEncryptionKey("myKey123"); ``` ### 값 조회 (JEP 함수, Transform 등) ```java // SeedSalt 조회 String seedSalt = ElinkTransactionContext.getSeedSalt(); // EncryptionKey 조회 String encryptionKey = ElinkTransactionContext.getEncryptionKey(); ``` --- ## 변수 추가 방법 새로운 트랜잭션 변수가 필요한 경우 `ElinkTransactionContext`에 접근자 메소드 추가: ```java // ElinkTransactionContext.java // 1. Key 상수 추가 private static final String KEY_NEW_VARIABLE = "NEW_VARIABLE"; // 2. Setter 추가 (private put 헬퍼 사용) public static void setNewVariable(String value) { put(KEY_NEW_VARIABLE, value); } // 3. Getter 추가 (private get 헬퍼 사용) public static String getNewVariable() { return get(KEY_NEW_VARIABLE); } ``` --- ## 주의사항 ### Thread Pool 환경 - `ApiAdapterController.callApi()` finally에서 `clear()` 호출 - `ElinkESBProcessProxy.callElinkESBProcess()` finally에서 조건부 `clear()` 호출 - Thread 재사용 시 이전 트랜잭션 데이터 누수 방지 ### 비동기 Queue 처리 - `EAIMessage.transactionContextTransfer`는 **전달 전용** (직접 접근 금지) - `FlowRouter`에서 송신 전 `getAll()`로 복사 - `ElinkESBProcessProxy`에서 `restore()` 후 즉시 null 초기화 ### transactionContextTransfer 직접 사용 금지 ```java // ❌ 잘못된 사용 eaiMessage.getTransactionContextTransfer().put("key", "value"); // ✓ 올바른 사용 ElinkTransactionContext.setSeedSalt("value"); ``` --- ## 관련 파일 | 파일 | 위치 | 역할 | |------|------|------| | `ElinkTransactionContext.java` | `elink-online-core` | ThreadLocal 컨텍스트 저장소 | | `EAIMessage.java` | `elink-online-common` | transactionContextTransfer (전달용) | | `ApiAdapterController.java` | `eapim-online` | 해제 (동기 최상위 진입점) | | `FlowRouter.java` | `elink-online-common` | 비동기 송신 전 컨텍스트 복사 | | `ElinkESBProcessProxy.java` | `elink-online-common` | 복원/해제 (비동기 - 조건부) | | `RequestProcessorSupport.java` | `elink-online-common` | 해제 (동기 안전망) | --- ## 클래스 다이어그램 ``` ┌─────────────────────────────────────┐ │ elink-online-core │ ├─────────────────────────────────────┤ │ ElinkTransactionContext │ │ ┌───────────────────────────────┐ │ │ │ ThreadLocal>│◄── 직접 저장소 (Lazy Init) │ └───────────────────────────────┘ │ │ + clear() │ │ + getAll() → Map 복사본 │ │ + restore(Map) │ │ + setSeedSalt() / getSeedSalt() │ │ + setEncryptionKey() / ... │ └───────────────┬─────────────────────┘ │ 비동기 전달 시 │ getAll() → 복사 ▼ ┌─────────────────────────────────────┐ │ elink-online-common │ ├─────────────────────────────────────┤ │ EAIMessage │ │ - transactionContextTransfer │◄── JMS 전달 전용 (직접 사용 금지!) │ : Map │ Serializable │ │ │ [송신] FlowRouter에서 설정 │ │ [수신] ElinkESBProcessProxy에서 │ │ restore() 후 null 초기화 │ └─────────────────────────────────────┘ ```