feat: 템플릿 기반 어댑터 에러 메시지 핸들러 추가
AdapterPropManager "AdapterErrorMessageHandler" 그룹의
{adapterGroupId}.template 키로 등록된 템플릿을 읽어
StandardMessage 및 callProp 값으로 치환한 에러 응답을 생성한다.
변수 치환 규칙:
- ${callprop.키} → callProp.getProperty("키")
- ${MSG.MAIN_MSG.경로} → StandardMessage.findItemValue("경로")
- {{#foreach 경로}}...{{/foreach}} → GRID 배열 반복 (쉼표 구분)
JSON/XML 템플릿 모두 지원
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+439
@@ -0,0 +1,439 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
import com.eactive.eai.message.StandardType;
|
||||
|
||||
/**
|
||||
* TemplateAdapterErrorMsgHandler 단위테스트
|
||||
*
|
||||
* - render() : package-private 이므로 같은 패키지에서 직접 호출
|
||||
* - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에
|
||||
* mock ApplicationContext 를 리플렉션으로 주입하여 AdapterPropManager 격리
|
||||
*/
|
||||
class TemplateAdapterErrorMsgHandlerTest {
|
||||
|
||||
private TemplateAdapterErrorMsgHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new TemplateAdapterErrorMsgHandler();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// StandardMessage 빌더 헬퍼
|
||||
// ================================================================
|
||||
|
||||
/** FIELD 아이템 (type=2) */
|
||||
private static StandardItem field(String name, String value) {
|
||||
return new StandardItem(name, 3, StandardType.FIELD, 1, 0, 300, 1, "", "", value, name);
|
||||
}
|
||||
|
||||
/** GROUP 아이템 (type=3, size=1 → 활성) */
|
||||
private static StandardItem group(String name) {
|
||||
return new StandardItem(name, 2, StandardType.GROUP, 1, 0, 0, 1, "", "", null, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* DJB 표준전문 MSG 영역을 모사한 StandardMessage 생성.
|
||||
*
|
||||
* 구조:
|
||||
* MSG (GROUP)
|
||||
* MAIN_MSG (GROUP)
|
||||
* outp_msg_cd, outp_msg_ctnt, outp_msg_desc
|
||||
* MSG_LIST (GRID)
|
||||
* row[n]: outp_msg_cd, outp_msg_ctnt, outp_msg_desc, err_occu_item_nm
|
||||
*
|
||||
* @param listRows null 또는 빈 배열이면 MSG_LIST 행 없음
|
||||
*/
|
||||
private StandardMessage buildMsg(String errCode, String errMsg, String errDesc,
|
||||
String[][] listRows) {
|
||||
StandardMessage msg = new StandardMessage();
|
||||
|
||||
StandardItem msgGroup = group("MSG");
|
||||
|
||||
StandardItem mainMsg = group("MAIN_MSG");
|
||||
mainMsg.addItem(field("outp_msg_cd", errCode));
|
||||
mainMsg.addItem(field("outp_msg_ctnt", errMsg));
|
||||
mainMsg.addItem(field("outp_msg_desc", errDesc));
|
||||
msgGroup.addItem(mainMsg);
|
||||
|
||||
StandardItem msgList = new StandardItem(
|
||||
"MSG_LIST", 2, StandardType.GRID, 0, 0, 0, 1, "", "", null, "MSG_LIST");
|
||||
msgList.addItem(field("outp_msg_cd", ""));
|
||||
msgList.addItem(field("outp_msg_ctnt", ""));
|
||||
msgList.addItem(field("outp_msg_desc", ""));
|
||||
msgList.addItem(field("err_occu_item_nm", ""));
|
||||
|
||||
if (listRows != null) {
|
||||
for (String[] row : listRows) {
|
||||
LinkedHashMap<String, StandardItem> rowMap = new LinkedHashMap<>();
|
||||
rowMap.put("outp_msg_cd", field("outp_msg_cd", get(row, 0)));
|
||||
rowMap.put("outp_msg_ctnt", field("outp_msg_ctnt", get(row, 1)));
|
||||
rowMap.put("outp_msg_desc", field("outp_msg_desc", get(row, 2)));
|
||||
rowMap.put("err_occu_item_nm", field("err_occu_item_nm", get(row, 3)));
|
||||
msgList.addArray(rowMap);
|
||||
}
|
||||
}
|
||||
msgGroup.addItem(msgList);
|
||||
msg.addItem(msgGroup);
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static String get(String[] arr, int idx) {
|
||||
return (arr != null && arr.length > idx) ? arr[idx] : "";
|
||||
}
|
||||
|
||||
private static Properties props(String... keyValues) {
|
||||
Properties p = new Properties();
|
||||
for (int i = 0; i < keyValues.length - 1; i += 2) {
|
||||
p.setProperty(keyValues[i], keyValues[i + 1]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 스칼라 변수 치환 ${path}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("StandardMessage 스칼라 변수 치환 ${path}")
|
||||
class ScalarSubstitution {
|
||||
|
||||
@Test
|
||||
@DisplayName("단순 JSON 템플릿 치환")
|
||||
void 단순_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "상세설명", null);
|
||||
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
|
||||
|
||||
String result = handler.render(template, msg, null);
|
||||
|
||||
assertEquals("{\"code\":\"E001\",\"msg\":\"오류발생\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일 변수 중복 치환")
|
||||
void 동일_변수_중복_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
String template = "${MSG.MAIN_MSG.outp_msg_cd}/${MSG.MAIN_MSG.outp_msg_cd}";
|
||||
|
||||
assertEquals("E001/E001", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 경로는 빈 문자열로 치환")
|
||||
void 없는_경로_빈_문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${NONE.PATH}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("변수 없는 템플릿은 원본 그대로 반환")
|
||||
void 변수_없는_템플릿_원본() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("{\"static\":\"value\"}", handler.render("{\"static\":\"value\"}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("outp_msg_desc 필드 치환")
|
||||
void outp_msg_desc_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "상세설명입니다", null);
|
||||
|
||||
assertEquals("상세설명입니다", handler.render("${MSG.MAIN_MSG.outp_msg_desc}", msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// callProp 변수 치환 ${callprop.키}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("callProp 변수 치환 ${callprop.키}")
|
||||
class CallPropSubstitution {
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 값을 템플릿에서 참조")
|
||||
void callProp_단순_참조() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001");
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 과 StandardMessage 혼합 참조")
|
||||
void callProp_stdmsg_혼합() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"code\":\"E001\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 에 없는 키는 빈 문자열")
|
||||
void callProp_없는_키_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = new Properties();
|
||||
|
||||
assertEquals("", handler.render("${callprop.NONE_KEY}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 이 null 이면 빈 문자열")
|
||||
void callProp_null_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${callprop.ADAPTER_NAME}", msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 배열 반복 {{#foreach path}}...{{/foreach}}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("배열 반복 블록 {{#foreach}}")
|
||||
class ForeachBlock {
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 1건 렌더링")
|
||||
void 단건_렌더링() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "",
|
||||
new String[][]{{"E001", "메시지A", "설명A", "fieldA"}});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[{\"c\":\"E001\",\"m\":\"메시지A\"}]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 2건 → 쉼표로 연결")
|
||||
void 다건_쉼표_연결() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "",
|
||||
new String[][]{
|
||||
{"E001", "오류A", "설명A", "fieldA"},
|
||||
{"E002", "오류B", "설명B", "fieldB"}
|
||||
});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[{\"c\":\"E001\"},{\"c\":\"E002\"}]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 가 비어있으면 foreach 결과는 빈 문자열")
|
||||
void 빈_배열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("foreach 경로가 없으면 빈 문자열")
|
||||
void 없는_경로() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
String template = "[{{#foreach NONE.LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("err_occu_item_nm 필드 포함 렌더링")
|
||||
void err_occu_item_nm_포함() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "",
|
||||
new String[][]{{"E001", "오류A", "설명A", "accountNo"}});
|
||||
String template = "{{#foreach MSG.MSG_LIST}}{\"field\":\"${err_occu_item_nm}\"}{{/foreach}}";
|
||||
|
||||
assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 혼합 템플릿 (스칼라 + callProp + 배열)
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("혼합 템플릿")
|
||||
class MixedTemplate {
|
||||
|
||||
@Test
|
||||
@DisplayName("JSON – MAIN_MSG + callProp + MSG_LIST 2건")
|
||||
void JSON_전체_혼합() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "대표설명",
|
||||
new String[][]{
|
||||
{"E001", "오류A", "설명A", "fieldA"},
|
||||
{"E002", "오류B", "설명B", "fieldB"}
|
||||
});
|
||||
Properties callProp = props("ADAPTER_NAME", "TEST_ADAPTER");
|
||||
|
||||
String template =
|
||||
"{" +
|
||||
"\"adapter\":\"${callprop.ADAPTER_NAME}\"," +
|
||||
"\"resultCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
|
||||
"\"resultMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"," +
|
||||
"\"errors\":[{{#foreach MSG.MSG_LIST}}" +
|
||||
"{\"code\":\"${outp_msg_cd}\",\"msg\":\"${outp_msg_ctnt}\",\"field\":\"${err_occu_item_nm}\"}" +
|
||||
"{{/foreach}}]" +
|
||||
"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals(
|
||||
"{\"adapter\":\"TEST_ADAPTER\"," +
|
||||
"\"resultCode\":\"E001\",\"resultMsg\":\"대표오류\"," +
|
||||
"\"errors\":[{\"code\":\"E001\",\"msg\":\"오류A\",\"field\":\"fieldA\"}," +
|
||||
"{\"code\":\"E002\",\"msg\":\"오류B\",\"field\":\"fieldB\"}]}",
|
||||
result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("XML – 스칼라 + callProp 치환")
|
||||
void XML_스칼라_callProp() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "", null);
|
||||
Properties callProp = props("IF_ID", "IF_001");
|
||||
String template = "<error><ifId>${callprop.IF_ID}</ifId><code>${MSG.MAIN_MSG.outp_msg_cd}</code></error>";
|
||||
|
||||
assertEquals("<error><ifId>IF_001</ifId><code>E001</code></error>",
|
||||
handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("XML – foreach 배열")
|
||||
void XML_foreach_배열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "",
|
||||
new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}});
|
||||
String template =
|
||||
"<errors>{{#foreach MSG.MSG_LIST}}" +
|
||||
"<item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>" +
|
||||
"{{/foreach}}</errors>";
|
||||
|
||||
assertEquals(
|
||||
"<errors>" +
|
||||
"<item><code>E001</code><msg>오류A</msg></item>," +
|
||||
"<item><code>E002</code><msg>오류B</msg></item>" +
|
||||
"</errors>",
|
||||
handler.render(template, msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// generateNonStandardErrorResponseMessage – AdapterPropManager mock
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateNonStandardErrorResponseMessage")
|
||||
class GenerateMethod {
|
||||
|
||||
private void injectMockPropManager(AdapterPropManager mockPropManager) throws Exception {
|
||||
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
|
||||
Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockPropManager);
|
||||
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
|
||||
ctxField.setAccessible(true);
|
||||
ctxField.set(null, mockCtx);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 미설정 시 null 반환")
|
||||
void 템플릿_없으면_null() throws Exception {
|
||||
AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("빈 템플릿 시 null 반환")
|
||||
void 빈_템플릿_null() throws Exception {
|
||||
AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" ");
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 설정 시 StandardMessage 치환 결과 반환")
|
||||
void 템플릿_stdmsg_치환() throws Exception {
|
||||
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
|
||||
Mockito.when(mockProp.getProperty(
|
||||
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
|
||||
eq("MY_GROUP.template"))).thenReturn(template);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertEquals("{\"code\":\"E999\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 값이 템플릿에 치환되어 반환")
|
||||
void 템플릿_callProp_치환() throws Exception {
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
|
||||
Mockito.when(mockProp.getProperty(
|
||||
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
|
||||
eq("MY_GROUP.template"))).thenReturn(template);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", callProp, null, msg);
|
||||
|
||||
assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 키는 {adapterGroupId}.template 형식으로 조회")
|
||||
void 템플릿_키_형식_검증() throws Exception {
|
||||
AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
handler.generateNonStandardErrorResponseMessage(
|
||||
"SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
Mockito.verify(mockProp).getProperty(
|
||||
TemplateAdapterErrorMsgHandler.PROP_GROUP,
|
||||
"SAMPLE_GROUP.template");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user