feat: Dual 이중 버킷 유량제어 패키지 추가 (DJErp→Dual 리네임·이관)

- InflowType 열거형 추가 (ADAPTER/INTERFACE/CLIENT 타입 코드 캡슐화)
- InflowControlDAO: InflowType 기반 범용 메서드 추가, 기존 메서드 위임
- RequestProcessor: private→protected 전환, 클라이언트/어댑터/인터페이스
  유량제어 훅 메서드 5개 추가 (checkClientInflow 등)
- dual 패키지 신규 추가
  · DualCustomBucket: perSecond/threshold 이중 버킷, 차단 원인 구분
  · DualBucket: isAdapterPassDetail 등 차단 원인 반환 인터페이스
  · DualInflowControlManager: 그룹 메트릭 카운팅(TargetMetrics), 리로드 연동
  · DualRequestProcessor: 클라이언트 유량제어 + 상세 오류 메시지 훅 구현
- 단위 테스트 5종 추가 (DualCustomBucket, Manager, Metrics, RequestProcessor, Concurrency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-04-27 09:40:27 +09:00
parent 632550220d
commit a4c9a2f2bd
12 changed files with 1842 additions and 14 deletions
@@ -41,20 +41,36 @@ public class InflowControlDAO extends BaseDAO {
@Autowired
private InflowControlGroupMappingLoader inflowControlGroupMappingLoader;
// -------------------------------------------------------------------------
// Enum 기반 범용 메서드 — type 코드를 직접 노출하지 않음
// -------------------------------------------------------------------------
public List<InflowTargetVO> getInflowList(InflowType type) throws DAOException {
return getInflowList(type.code());
}
public Map<String, InflowTargetVO> getInflowMap(InflowType type, String name) throws DAOException {
return getInflowMap(type.code(), name);
}
// -------------------------------------------------------------------------
// 기존 명명 메서드 — enum 기반 메서드로 위임
// -------------------------------------------------------------------------
public List<InflowTargetVO> getInflowAdapterList() throws DAOException {
return getInflowList("01");
return getInflowList(InflowType.ADAPTER);
}
public List<InflowTargetVO> getInflowInterfaceList() throws DAOException {
return getInflowList("02");
return getInflowList(InflowType.INTERFACE);
}
public Map<String, InflowTargetVO> getInflowTargetByAdater(String name) throws DAOException {
return getInflowMap("01", name);
return getInflowMap(InflowType.ADAPTER, name);
}
public Map<String, InflowTargetVO> getInflowTargetByInterface(String name) throws DAOException {
return getInflowMap("02", name);
return getInflowMap(InflowType.INTERFACE, name);
}
private List<InflowTargetVO> getInflowList(String type) throws DAOException {
@@ -0,0 +1,23 @@
package com.eactive.eai.common.inflow;
/**
* 유량제어 대상 유형.
* {@link InflowControlDAO}의 type 컬럼 값을 enum으로 관리하여
* 호출부에 코드 문자열이 노출되지 않도록 한다.
*/
public enum InflowType {
ADAPTER("01"),
INTERFACE("02"),
CLIENT("03");
private final String code;
InflowType(String code) {
this.code = code;
}
public String code() {
return code;
}
}
@@ -0,0 +1,37 @@
package com.eactive.eai.common.inflow.dual;
import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.InflowTargetVO;
/**
* Bucket 인터페이스 확장.
* 기존 isAdapterPass/isInterfacePass(boolean)에 더해
* 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지 반환하는 메서드 추가.
* 클라이언트별 유량제어 메서드도 포함.
*/
public interface DualBucket extends Bucket {
/**
* 어댑터 유량제어 체크 — 차단 원인 포함.
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
*/
String isAdapterPassDetail(String adapter);
/**
* 인터페이스 유량제어 체크 — 차단 원인 포함.
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
*/
String isInterfacePassDetail(String inter);
/**
* 클라이언트 유량제어 체크 — 차단 원인 포함.
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
*/
String isClientPassDetail(String clientId);
/**
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
* @return 등록된 설정이 없으면 null
*/
InflowTargetVO getClientInflowThreshold(String clientId);
}
@@ -0,0 +1,58 @@
package com.eactive.eai.common.inflow.dual;
import com.eactive.eai.common.inflow.InflowTargetVO;
import io.github.bucket4j.local.LocalBucket;
/**
* 어댑터/인터페이스 유량제어 버킷.
* 기존 CustomBucket(단일 버킷)과 달리 perSecond/threshold를 별도 버킷으로 분리하여
* 어느 한도를 초과했는지 구분 가능 (CustomGroupBucket과 동일한 이중 구조).
*/
public class DualCustomBucket {
public static final String RESULT_PASS = null;
public static final String RESULT_BLOCKED_PER_SECOND = "PER_SECOND";
public static final String RESULT_BLOCKED_THRESHOLD = "THRESHOLD";
private final LocalBucket perSecondBucket;
private final LocalBucket thresholdBucket;
private final InflowTargetVO inflowTargetVo;
public DualCustomBucket(LocalBucket perSecondBucket, LocalBucket thresholdBucket, InflowTargetVO inflowTargetVo) {
this.perSecondBucket = perSecondBucket;
this.thresholdBucket = thresholdBucket;
this.inflowTargetVo = inflowTargetVo;
}
/**
* 두 버킷을 순서대로 소비 시도.
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
*/
public String tryConsume() {
boolean perSecondPass = (perSecondBucket == null) || perSecondBucket.tryConsume(1);
if (!perSecondPass) {
return RESULT_BLOCKED_PER_SECOND;
}
boolean thresholdPass = (thresholdBucket == null) || thresholdBucket.tryConsume(1);
if (!thresholdPass) {
// perSecond 토큰은 이미 소비됨 — Token Bucket 특성상 롤백 불가
// 초당 버킷은 빠르게 리필되므로 실질적 영향 미미
return RESULT_BLOCKED_THRESHOLD;
}
return RESULT_PASS;
}
public long getPerSecondAvailableTokens() {
return (perSecondBucket != null) ? perSecondBucket.getAvailableTokens() : -1;
}
public long getThresholdAvailableTokens() {
return (thresholdBucket != null) ? thresholdBucket.getAvailableTokens() : -1;
}
public LocalBucket getPerSecondBucket() { return perSecondBucket; }
public LocalBucket getThresholdBucket() { return thresholdBucket; }
public InflowTargetVO getInflowTargetVo() { return inflowTargetVo; }
}
@@ -0,0 +1,313 @@
package com.eactive.eai.common.inflow.dual;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.CustomGroupBucket;
import com.eactive.eai.common.inflow.InflowControlDAO;
import com.eactive.eai.common.inflow.InflowControlManager;
import com.eactive.eai.common.inflow.InflowTargetVO;
import com.eactive.eai.common.inflow.InflowType;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.local.LocalBucket;
/**
* 어댑터/인터페이스/클라이언트 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
*
* <p>기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여
* 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다.
* 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고
* {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로
* 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다.
*
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager</pre>
*/
@Component
public class DualInflowControlManager extends InflowControlManager implements DualBucket {
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
// InflowControlUtil이 리플렉션으로 호출하는 진입점
public static synchronized Bucket getBucket() {
return ApplicationContextProvider.getContext().getBean(DualInflowControlManager.class);
}
public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) {
return InflowControlManager.isTargetOfInflowControl(eaiServerManager, eaiMessage);
}
@Autowired
private InflowControlDAO dualDao;
private volatile Map<String, DualCustomBucket> adapterBucketMap = new ConcurrentHashMap<>();
private volatile Map<String, DualCustomBucket> interfaceBucketMap = new ConcurrentHashMap<>();
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> groupMetricsMap = new ConcurrentHashMap<>();
public static class TargetMetrics {
public final AtomicLong allowed = new AtomicLong();
public final AtomicLong rejectedPerSecond = new AtomicLong();
public final AtomicLong rejectedThreshold = new AtomicLong();
public volatile long lastResetTime = System.currentTimeMillis();
public void reset() {
allowed.set(0);
rejectedPerSecond.set(0);
rejectedThreshold.set(0);
lastResetTime = System.currentTimeMillis();
}
}
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
@Override
public void start() throws LifecycleException {
super.start();
try {
initAdapters();
initInterfaces();
initClients();
} catch (Exception e) {
throw new LifecycleException("DualInflowControlManager init failed: " + e.getMessage());
}
}
// -------------------------------------------------------------------------
// 초기화 / 리로드
// -------------------------------------------------------------------------
private void initAdapters() throws Exception {
this.adapterBucketMap = loadBucketMap(InflowType.ADAPTER);
if (logger.isInfo()) logger.info("DualInflowControlManager] initAdapters: " + adapterBucketMap.size() + " buckets");
}
private void initInterfaces() throws Exception {
this.interfaceBucketMap = loadBucketMap(InflowType.INTERFACE);
if (logger.isInfo()) logger.info("DualInflowControlManager] initInterfaces: " + interfaceBucketMap.size() + " buckets");
}
private void initClients() throws Exception {
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
if (logger.isInfo()) logger.info("DualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
}
private Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
Map<String, DualCustomBucket> newMap = new HashMap<>();
for (InflowTargetVO vo : dualDao.getInflowList(type)) {
DualCustomBucket bucket = makeBucket(vo);
if (bucket != null) newMap.put(vo.getName(), bucket);
}
return newMap;
}
@Override
public synchronized void reloadAdapter() throws Exception {
super.reloadAdapter();
initAdapters();
}
@Override
public synchronized void reloadAdapter(String adapter) throws Exception {
super.reloadAdapter(adapter);
reloadBucketMap(adapterBucketMap, InflowType.ADAPTER, adapter);
}
@Override
public synchronized void reloadInterface() throws Exception {
super.reloadInterface();
initInterfaces();
}
@Override
public synchronized void reloadInterface(String inter) throws Exception {
super.reloadInterface(inter);
reloadBucketMap(interfaceBucketMap, InflowType.INTERFACE, inter);
}
public synchronized void reloadClient() throws Exception {
initClients();
}
public synchronized void reloadClient(String clientId) throws Exception {
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
}
@Override
public synchronized void reloadGroup() throws Exception {
super.reloadGroup();
groupMetricsMap.clear();
}
@Override
public synchronized void reloadGroup(String groupId) throws Exception {
super.reloadGroup(groupId);
TargetMetrics m = groupMetricsMap.get(groupId);
if (m != null) m.reset();
}
private void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
Map<String, InflowTargetVO> updated = dualDao.getInflowMap(type, name);
if (updated == null) return;
for (InflowTargetVO vo : updated.values()) {
DualCustomBucket bucket = makeBucket(vo);
if (bucket != null) targetMap.put(vo.getName(), bucket);
else targetMap.remove(vo.getName());
}
}
// -------------------------------------------------------------------------
// 그룹 메트릭 — isGroupPass() 카운팅 및 접근자
// -------------------------------------------------------------------------
@Override
public String isGroupPass(String groupId) {
String result = super.isGroupPass(groupId);
TargetMetrics m = groupMetricsMap.computeIfAbsent(groupId, k -> new TargetMetrics());
if (result == null) {
m.allowed.incrementAndGet();
} else if (CustomGroupBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) {
m.rejectedPerSecond.incrementAndGet();
} else {
m.rejectedThreshold.incrementAndGet();
}
return result;
}
public TargetMetrics getGroupMetrics(String groupId) {
return groupMetricsMap.get(groupId);
}
public Map<String, TargetMetrics> getAllGroupMetrics() {
return Collections.unmodifiableMap(groupMetricsMap);
}
public void resetGroupMetrics(String groupId) {
TargetMetrics m = groupMetricsMap.get(groupId);
if (m != null) m.reset();
}
public void resetAllGroupMetrics() {
groupMetricsMap.values().forEach(TargetMetrics::reset);
}
// -------------------------------------------------------------------------
// DualBucket — 차단 원인 포함 메서드
// -------------------------------------------------------------------------
@Override
public String isAdapterPassDetail(String adapter) {
DualCustomBucket b = adapterBucketMap.get(adapter);
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
}
@Override
public String isInterfacePassDetail(String inter) {
DualCustomBucket b = interfaceBucketMap.get(inter);
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
}
@Override
public String isClientPassDetail(String clientId) {
DualCustomBucket b = clientBucketMap.get(clientId);
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
}
@Override
public InflowTargetVO getClientInflowThreshold(String clientId) {
DualCustomBucket b = clientBucketMap.get(clientId);
return (b == null) ? null : b.getInflowTargetVo();
}
// -------------------------------------------------------------------------
// Bucket 인터페이스 오버라이드 — 이중 버킷으로 교체 (boolean 반환 유지)
// isAdapterPassDetail/isInterfacePassDetail이 토큰을 소비하므로
// 두 메서드를 동일 요청에서 중복 호출하면 토큰이 이중 소비됨에 주의.
// -------------------------------------------------------------------------
@Override
public boolean isAdapterPass(String adapter) {
return isAdapterPassDetail(adapter) == DualCustomBucket.RESULT_PASS;
}
@Override
public boolean isInterfacePass(String inter) {
return isInterfacePassDetail(inter) == DualCustomBucket.RESULT_PASS;
}
// -------------------------------------------------------------------------
// 모니터링용 접근자
// -------------------------------------------------------------------------
public DualCustomBucket getAdapterBucket(String adapter) {
return adapterBucketMap.get(adapter);
}
public DualCustomBucket getInterfaceBucket(String inter) {
return interfaceBucketMap.get(inter);
}
public Map<String, DualCustomBucket> getAdapterBucketMap() {
return adapterBucketMap;
}
public Map<String, DualCustomBucket> getInterfaceBucketMap() {
return interfaceBucketMap;
}
public DualCustomBucket getClientBucket(String clientId) {
return clientBucketMap.get(clientId);
}
public Map<String, DualCustomBucket> getClientBucketMap() {
return clientBucketMap;
}
// -------------------------------------------------------------------------
// 버킷 팩토리
// -------------------------------------------------------------------------
private DualCustomBucket makeBucket(InflowTargetVO vo) {
if (!InflowControlManager.isInflowTarget(vo)) {
return null;
}
LocalBucket perSecondBucket = null;
LocalBucket thresholdBucket = null;
if (vo.getThresholdPerSecond() > 0) {
perSecondBucket = Bucket4j.builder()
.addLimit(Bandwidth.simple(vo.getThresholdPerSecond(), Duration.ofSeconds(1)))
.build();
}
if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) {
thresholdBucket = Bucket4j.builder()
.addLimit(Bandwidth.simple(vo.getThreshold(),
InflowControlManager.getPeriod(vo.getThresholdTimeUnit())))
.build();
}
return new DualCustomBucket(perSecondBucket, thresholdBucket, vo);
}
}
@@ -0,0 +1,80 @@
package com.eactive.eai.common.inflow.dual;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.InflowTargetVO;
import com.eactive.eai.inbound.processor.RequestProcessor;
/**
* 어댑터/인터페이스 유량제어 차단 시 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지
* 에러 메시지에 포함하는 RequestProcessor 확장.
*
* <p>DualInflowControlManager와 함께 사용:
* <pre>
* inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
* </pre>
*
* <p>Spring Bean 등록 후 기존 RequestProcessor 대신 이 클래스를 어댑터에 설정한다.
*/
@Component("dualRequestProcessor")
public class DualRequestProcessor extends RequestProcessor {
@Override
protected String checkClientInflow(Bucket bucket, String clientId) {
if (StringUtils.isBlank(clientId)) return null;
if (bucket instanceof DualBucket) {
return ((DualBucket) bucket).isClientPassDetail(clientId);
}
return null;
}
@Override
protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) {
if (bucket instanceof DualBucket) {
return ((DualBucket) bucket).getClientInflowThreshold(clientId);
}
return null;
}
@Override
protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) {
if (bucket instanceof DualBucket) {
return ((DualBucket) bucket).isAdapterPassDetail(adapterGroupName);
}
return super.checkAdapterInflow(bucket, adapterGroupName);
}
@Override
protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) {
if (bucket instanceof DualBucket) {
return ((DualBucket) bucket).isInterfacePassDetail(eaiSvcCd);
}
return super.checkInterfaceInflow(bucket, eaiSvcCd);
}
/**
* 차단 원인에 따라 에러 메시지에 한도 정보를 포함.
* <ul>
* <li>PER_SECOND: "API 호출 한도 초과 [name: Nreq/sec]"</li>
* <li>THRESHOLD: "API 호출 한도 초과 [name: Nreq/UNIT]"</li>
* <li>그 외: "API 호출 한도 초과 [name]" (기존 동작)</li>
* </ul>
*/
@Override
protected String buildInflowTargetErrorMsg(InflowTargetVO inflowTargetVO, String blockedType) {
if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(blockedType)) {
return String.format("API 호출 한도 초과 [%s: %dreq/sec]",
inflowTargetVO.getName(),
inflowTargetVO.getThresholdPerSecond());
}
if (DualCustomBucket.RESULT_BLOCKED_THRESHOLD.equals(blockedType)) {
return String.format("API 호출 한도 초과 [%s: %dreq/%s]",
inflowTargetVO.getName(),
inflowTargetVO.getThreshold(),
inflowTargetVO.getThresholdTimeUnit());
}
return super.buildInflowTargetErrorMsg(inflowTargetVO, blockedType);
}
}
@@ -88,8 +88,8 @@ import java.util.Map;
public class RequestProcessor extends RequestProcessorSupport {
private static String instid = null;
private static String serverName = null;
protected static String instid = null;
protected static String serverName = null;
//private static boolean isPEAIServer = true;
static {
@@ -100,9 +100,9 @@ public class RequestProcessor extends RequestProcessorSupport {
instid = eaiServerManager.getGroupInstId();
}
private static boolean httpHeaderLogMode = HttpAdapterExtraLogUtil.isHttpHeaderMode();
protected static boolean httpHeaderLogMode = HttpAdapterExtraLogUtil.isHttpHeaderMode();
private ProcessVO setVO(Properties prop,long msgRcvTm) throws RequestProcessorException{
protected ProcessVO setVO(Properties prop,long msgRcvTm) throws RequestProcessorException{
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
String adapterGroupName = prop.getProperty(Processor.ADAPTER_GROUP_NAME);
String adapterName = prop.getProperty(Processor.ADAPTER_NAME);
@@ -1033,13 +1033,20 @@ public class RequestProcessor extends RequestProcessorSupport {
Bucket bucket = InflowControlUtil.getBucket();
String blockedType = "";
// Client 유량제어 체크 — clientId가 있는 경우에만
String clientBlockedType = checkClientInflow(bucket, clientId);
if (clientBlockedType != null) {
blockedType += "C";
}
// Adapter 유량제어 체크
if (!bucket.isAdapterPass(vo.getAdapterGroupName())) {
String adapterBlockedType = checkAdapterInflow(bucket, vo.getAdapterGroupName());
if (adapterBlockedType != null) {
blockedType += "A";
}
// 그룹 우선, 없으면 인터페이스 유량제어 체크
String groupId = bucket.getGroupIdByInterface(vo.getEaiSvcCd());
String groupBlockedType = null;
String interfaceBlockedType = null;
if (groupId != null) {
// 그룹에 속한 인터페이스 → 그룹 유량제어 체크
groupBlockedType = bucket.isGroupPass(groupId);
@@ -1048,7 +1055,8 @@ public class RequestProcessor extends RequestProcessorSupport {
}
} else {
// 그룹에 속하지 않은 인터페이스 → 인터페이스 유량제어 체크
if (!bucket.isInterfacePass(vo.getEaiSvcCd())) {
interfaceBlockedType = checkInterfaceInflow(bucket, vo.getEaiSvcCd());
if (interfaceBlockedType != null) {
blockedType += "I";
}
}
@@ -1057,7 +1065,9 @@ public class RequestProcessor extends RequestProcessorSupport {
if (blockedType.length() > 0) {
InflowTargetVO inflowTargetVO = null;
InflowGroupVO inflowGroupVO = null;
if (blockedType.startsWith("A")) {
if (blockedType.startsWith("C")) {
inflowTargetVO = resolveClientInflowThreshold(bucket, clientId);
} else if (blockedType.startsWith("A")) {
inflowTargetVO = bucket.getAdapterInflowThreashold(vo.getAdapterGroupName());
} else if (blockedType.contains("G")) {
inflowGroupVO = bucket.getGroupInflowThreshold(groupId);
@@ -1078,7 +1088,11 @@ public class RequestProcessor extends RequestProcessorSupport {
inflowGroupVO.getThresholdTimeUnit());
}
} else if (inflowTargetVO != null) {
inflowErrorMsg = String.format("API 호출 한도 초과 [%s]", inflowTargetVO.getName());
String targetBlockedType;
if (blockedType.startsWith("C")) targetBlockedType = clientBlockedType;
else if (blockedType.startsWith("A")) targetBlockedType = adapterBlockedType;
else targetBlockedType = interfaceBlockedType;
inflowErrorMsg = buildInflowTargetErrorMsg(inflowTargetVO, targetBlockedType);
} else {
inflowErrorMsg = "API 호출 한도 초과";
}
@@ -1384,7 +1398,29 @@ public class RequestProcessor extends RequestProcessorSupport {
}
}
private String getInboundErrorResponse(Properties prop, StandardMessage standardMessage, InterfaceMapper mapper
// 클라이언트 유량제어 체크 훅 — 기본 구현은 체크 없음, DJErpRequestProcessor가 오버라이드
protected String checkClientInflow(Bucket bucket, String clientId) {
return null;
}
protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) {
return null;
}
// 어댑터/인터페이스 유량제어 체크 훅 — 서브클래스에서 오버라이드하여 차단 원인(PER_SECOND/THRESHOLD)을 반환 가능
protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) {
return bucket.isAdapterPass(adapterGroupName) ? null : "BLOCKED";
}
protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) {
return bucket.isInterfacePass(eaiSvcCd) ? null : "BLOCKED";
}
protected String buildInflowTargetErrorMsg(InflowTargetVO inflowTargetVO, String blockedType) {
return String.format("API 호출 한도 초과 [%s]", inflowTargetVO.getName());
}
protected String getInboundErrorResponse(Properties prop, StandardMessage standardMessage, InterfaceMapper mapper
, String errCode, String errMsg, String charset) {
String errorResponse = null;
@@ -1413,7 +1449,7 @@ public class RequestProcessor extends RequestProcessorSupport {
return errorResponse;
}
private String getInboundErrorResponseForStandard(StandardMessage standardMessage, InterfaceMapper mapper,
protected String getInboundErrorResponseForStandard(StandardMessage standardMessage, InterfaceMapper mapper,
String errCode, String errMsg, String charset, String messageType) {
mapper.nextGuidSeq(standardMessage);
mapper.setSendRecvDivision(standardMessage, STDMessageKeys.SEND_RECV_CD_RECV);