APP 승인 시 GW 반영 로직 개선
eapim-admin CI / build (push) Has been cancelled

- GwClientSyncService 분리 및 트랜잭션 관리 강화
- 승인 화면에 GW 반영 필수 정보 노출 추가
- JSP 동적 행 노출 및 승인 데이터 검증 로직 적용
This commit is contained in:
Rinjae
2026-08-13 14:37:31 +09:00
parent 71e433a2a5
commit 760000070d
5 changed files with 134 additions and 40 deletions
@@ -246,6 +246,10 @@ public class PortalApprovalManService extends BaseService {
appApprovalUI.setClientName(appRequest.getClientName());
// 신청 사유는 상세 내용과 별개 항목으로도 내려준다(해지 신청 화면에서 "해지 사유"로 표기)
appApprovalUI.setRequestReason(appRequest.getReason());
// 승인 시 GW 클라이언트로 그대로 반영되는 항목이므로 승인자가 검토할 수 있게 함께 내려준다.
appApprovalUI.setIpWhitelist(appRequest.getIpWhitelist());
appApprovalUI.setCallbackUrl(appRequest.getCallbackUrl());
appApprovalUI.setAppDescription(appRequest.getAppDescription());
appApprovalUI.getApprovers().forEach(approver -> {
if (approver.getUser() == null) {
@@ -0,0 +1,84 @@
package com.eactive.eai.rms.onl.apim.approval.app;
import com.eactive.apim.portal.apprequest.entity.AppRequest;
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
import com.eactive.apim.portal.apprequest.entity.GwAction;
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* APP 승인 시 GW 인증 클라이언트(APIGW 스키마 TSEAIAU01) 반영을 <b>승인 트랜잭션과 분리된 새 트랜잭션</b>에서
* 수행하고 커밋까지 끝낸다.
*
* <p>분리가 필요한 이유: GW 는 RELOAD_CLIENT 명령을 받으면 자기 커넥션으로 DB 를 다시 읽어 메모리에 적재한다
* ({@code OAuth2Manager#reloadClient} 는 캐시에서 제거 후 {@code getClientById} 결과가 있을 때만 다시 넣는다).
* 승인 트랜잭션 안에서 INSERT 한 직후 broadcast 하면 그 행이 아직 커밋되지 않아 GW 가 찾지 못하고,
* 신규(NEW) 클라이언트는 캐시에 올라가지 못한 채 리로드가 무의미해진다. 변경(MODIFY)도 커밋 전 옛 값이
* 다시 적재된다. 따라서 broadcast 는 반드시 이 메서드가 반환(=커밋)된 뒤에 수행해야 한다.
*
* <p>트랜잭션 매니저는 APIGW 스키마를 라우팅하는 멀티테넌트 EMF 쪽({@code transactionManager})을 사용한다.
* Hibernate 는 Session 을 열 때 테넌트를 한 번만 해석하므로, 새 트랜잭션을 여는 이 시점에
* {@link DataSourceContextHolder} 가 APIGW 로 설정돼 있어야 한다.
*/
@Service
@RequiredArgsConstructor
public class GwClientSyncService {
private static final Logger logger = LoggerFactory.getLogger(GwClientSyncService.class);
private final ClientManService clientManService;
private final CredentialUIMapper credentialUIMapper;
@Transactional(transactionManager = "transactionManager",
propagation = Propagation.REQUIRES_NEW,
rollbackFor = ApprovalDeployException.class)
public void apply(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException {
// 새 Session 이 열리는 시점의 테넌트가 APIGW 여야 TSEAIAU01 에 접근할 수 있다.
DataSourceType type = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(type);
AppRequestType action = appRequest.getType();
if (AppRequestType.NEW.equals(action)) {
clientManService.insert(credentialUIMapper.mapClient(credentialUI));
} else if (AppRequestType.MODIFY.equals(action)) {
clientManService.update(credentialUIMapper.mapClient(credentialUI));
} else if (AppRequestType.DELETE.equals(action)) {
applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid());
}
}
/**
* API 이용해지(DELETE) 승인 시 GW 클라이언트(TSEAIAU01) 처리.
* 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다.
* - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0')
* GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고,
* 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다.
*/
private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException {
try {
if (GwAction.DELETE.equals(gwAction)) {
clientManService.delete(clientId);
logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId);
} else {
clientManService.blockClient(clientId);
logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId);
}
} catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) {
logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId);
} catch (RuntimeException e) {
logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e);
throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage());
}
}
}
@@ -2,7 +2,6 @@ package com.eactive.eai.rms.onl.apim.approval.app;
import com.eactive.apim.portal.apprequest.entity.AppRequest;
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
import com.eactive.apim.portal.apprequest.entity.GwAction;
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.approval.entity.Approval;
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
@@ -10,6 +9,7 @@ import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendService;
import com.eactive.eai.agent.command.CommandResult;
import com.eactive.eai.agent.command.CommonCommand;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
@@ -18,9 +18,7 @@ import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService;
import com.eactive.eai.rms.onl.apim.approval.RandomStringGenerator;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
import java.util.Arrays;
import java.util.Collections;
@@ -55,9 +53,8 @@ public class PortalAppApprovalListener implements ApprovalListener {
private final ApiGroupService apiGroupService;
private final AppRequestRepository appRequestRepository;
private final CredentialManService credentialManService;
private final CredentialUIMapper credentialUIMapper;
private final ClientManService clientManService;
private final MessageSendService messageSendService;
private final GwClientSyncService gwClientSyncService;
@Override
@@ -127,47 +124,23 @@ public class PortalAppApprovalListener implements ApprovalListener {
private void syncTargetServer(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException {
AppRequestType action = appRequest.getType();
DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW");
// TSEAIAU01/TSEAISY02 는 APIGW 스키마에 있으므로 라우팅을 먼저 설정한다.
// (GW 반영은 새 트랜잭션에서, 이어지는 broadcast 의 서버목록 조회도 이 테넌트로 수행된다)
DataSourceType type = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(type);
if (action.equals(AppRequestType.NEW)) {
clientManService.insert(credentialUIMapper.mapClient(credentialUI));
} else if (action.equals(AppRequestType.MODIFY)) {
clientManService.update(credentialUIMapper.mapClient(credentialUI));
} else if (action.equals(AppRequestType.DELETE)) {
applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid());
}
// GW 반영은 별도 트랜잭션에서 커밋까지 끝낸다. GW 는 리로드 명령을 받고 DB 를 다시 읽으므로
// 커밋 전에 broadcast 하면 신규 클라이언트를 찾지 못해 리로드가 무의미해진다.
gwClientSyncService.apply(appRequest, credentialUI);
CommonCommand.builder()
// broadcast 는 서버별 전송 실패를 예외로 던지지 않고 결과 맵에 담아 정상 반환한다.
// 결과를 버리면 리로드 실패가 아무 흔적 없이 묻히므로 서버별 응답을 반드시 남긴다.
CommandResult reloadResult = CommonCommand.builder()
.name(CommonCommand.RELOAD_CLIENT_COMMAND)
.args(credentialUI.getClientid())
.build().broadcast(agentUtilService);
}
/**
* API 이용해지 승인 시 GW 클라이언트(TSEAIAU01) 처리.
* 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다.
* - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0')
* GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고,
* 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다.
*/
private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException {
try {
if (GwAction.DELETE.equals(gwAction)) {
clientManService.delete(clientId);
logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId);
} else {
clientManService.blockClient(clientId);
logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId);
}
} catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) {
logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId);
} catch (RuntimeException e) {
logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e);
throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage());
}
logger.info("GW 클라이언트 리로드 broadcast - clientId: {}, 서버별 응답: {}",
credentialUI.getClientid(), reloadResult.getMessage(false));
}
private CredentialUI modifyClientUI(AppRequest appRequest) {
@@ -33,4 +33,9 @@ public class PortalAppApprovalUI extends PortalApprovalUI {
//신청자가 입력한 사유(PTL_APP_REQUEST.REASON). 해지 신청은 사유가 필수라 별도 항목으로 노출한다.
private String requestReason;
//신청서(PTL_APP_REQUEST)에 담긴 Client 등록 정보. 승인 시 GW(TSEAIAU01)로 그대로 반영되므로 승인 화면에 노출한다.
private String ipWhitelist;
private String callbackUrl;
private String appDescription;
}