ConcatN 가변 파라미터 문자열 연결 함수 추가 및 단위테스트 작성

- ConcatN: numberOfParameters=-1로 파라미터 수 제한 없이 문자열 연결
- 기존 Concat(2개 고정)을 대체 가능하며 하위 호환성 유지
- ConcatNFunctionTest: JEP 직접 사용, 파라미터 수 변화 검증
- ConcatNParserTest: Parser 클래스 경유, 변수/상수 혼합 표현식 및 성능 검증

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-05-08 13:25:37 +09:00
parent 17f6df5068
commit 2d3f659012
3 changed files with 354 additions and 0 deletions
@@ -0,0 +1,38 @@
package com.eactive.eai.transformer.function.generic;
import java.util.Stack;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
public class ConcatN extends PostfixMathCommand {
public ConcatN() {
numberOfParameters = -1;
}
@SuppressWarnings("unchecked")
public void run(Stack inStack) throws ParseException {
checkStack(inStack);
int n = curNumberOfParameters;
if (n < 1) {
throw new ParseException("concatN() requires at least 1 parameter");
}
String[] params = new String[n];
for (int i = n - 1; i >= 0; i--) {
Object p = inStack.pop();
if (!(p instanceof String)) {
throw new ParseException("Invalid parameter type: parameter " + (i + 1) + " is not a string");
}
params[i] = (String) p;
}
StringBuilder sb = new StringBuilder();
for (String s : params) {
sb.append(s);
}
inStack.push(sb.toString());
}
}
@@ -0,0 +1,164 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.nfunk.jep.JEP;
import com.eactive.eai.transformer.function.generic.ConcatN;
@DisplayName("ConcatN 가변 파라미터 함수 테스트")
class ConcatNFunctionTest {
private JEP jep;
@BeforeEach
void setUp() {
jep = new JEP();
jep.addStandardFunctions();
jep.addStandardConstants();
jep.setAllowUndeclared(true);
jep.addFunction("concatN", new ConcatN());
}
private String eval(String expr) throws Exception {
jep.parseExpression(expr);
assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo());
Object result = jep.getValueAsObject();
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
return (String) result;
}
// ─────────────────────────────────────────────────────────────────
// 파라미터 수 변화 테스트
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("파라미터 1개 — 입력값 그대로 반환")
void testOneParam() throws Exception {
jep.addVariable("a", "Hello");
String result = eval("concatN(a)");
assertEquals("Hello", result);
}
@Test
@DisplayName("파라미터 2개 — 기존 Concat 동작과 동일")
void testTwoParams() throws Exception {
jep.addVariable("a", "Hello");
jep.addVariable("b", "World");
String result = eval("concatN(a, b)");
assertEquals("HelloWorld", result);
}
@Test
@DisplayName("파라미터 3개")
void testThreeParams() throws Exception {
jep.addVariable("a", "2024");
jep.addVariable("sep", "-");
jep.addVariable("b", "01");
String result = eval("concatN(a, sep, b)");
assertEquals("2024-01", result);
}
@Test
@DisplayName("파라미터 5개 — 날짜 + 시간 조합")
void testFiveParams() throws Exception {
jep.addVariable("year", "2024");
jep.addVariable("dash", "-");
jep.addVariable("month", "05");
jep.addVariable("dash2", "-");
jep.addVariable("day", "08");
String result = eval("concatN(year, dash, month, dash2, day)");
assertEquals("2024-05-08", result);
}
@Test
@DisplayName("파라미터 7개 — 날짜 + 공백 + 시간 조합")
void testSevenParams() throws Exception {
jep.addVariable("year", "2024");
jep.addVariable("dash", "-");
jep.addVariable("month", "05");
jep.addVariable("day", "08");
jep.addVariable("space", " ");
jep.addVariable("hour", "14");
jep.addVariable("minute", "30");
String result = eval("concatN(year, dash, month, dash, day, space, hour)");
assertEquals("2024-05-08 14", result);
}
// ─────────────────────────────────────────────────────────────────
// 변수 재할당 후 동일 표현식 재평가 — 파라미터 수 유지, 값만 변경
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("동일 표현식(3개), 변수 값만 변경하여 반복 평가")
void testReuseExpressionWithDifferentValues() throws Exception {
jep.parseExpression("concatN(code, dash, seq)");
assertFalse(jep.hasError());
jep.addVariable("code", "KOR");
jep.addVariable("dash", "-");
jep.addVariable("seq", "001");
assertEquals("KOR-001", (String) jep.getValueAsObject());
jep.addVariable("code", "USA");
jep.addVariable("seq", "999");
assertEquals("USA-999", (String) jep.getValueAsObject());
jep.addVariable("code", "JPN");
jep.addVariable("seq", "042");
assertEquals("JPN-042", (String) jep.getValueAsObject());
}
// ─────────────────────────────────────────────────────────────────
// 리터럴 / 혼합 사용
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("리터럴 문자열만 5개 사용")
void testLiteralsOnly() throws Exception {
String result = eval("concatN(\"A\", \"B\", \"C\", \"D\", \"E\")");
assertEquals("ABCDE", result);
}
@Test
@DisplayName("변수와 리터럴 혼합 — 괄호로 감싸기")
void testMixedVariableAndLiteral() throws Exception {
jep.addVariable("code", "KOR");
jep.addVariable("seq", "001");
String result = eval("concatN(\"[\", code, \"-\", seq, \"]\")");
assertEquals("[KOR-001]", result);
}
// ─────────────────────────────────────────────────────────────────
// 오류 케이스
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("비문자열(숫자) 파라미터 포함 → 오류 발생")
void testNonStringParamCausesError() {
// JEP 는 정수 리터럴을 Double 로 처리하므로 평가 시 오류가 발생해야 함
jep.parseExpression("concatN(\"A\", 123, \"C\")");
if (!jep.hasError()) {
jep.getValueAsObject();
assertTrue(jep.hasError(), "비문자열 파라미터가 포함된 경우 오류가 발생해야 합니다");
}
}
}
@@ -0,0 +1,152 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.eactive.eai.transformer.engine.Parser;
import com.eactive.eai.transformer.function.generic.ConcatN;
/**
* ParserTest 스타일로 Parser 를 통해 concatN 함수를 검증한다.
*
* - 표현식은 parse() 한 번 → 변수값만 교체하며 반복 평가 (JEP 재사용 패턴)
* - 변수 : parser.addVariable() 로 바인딩
* - 상수 : 표현식 안에 "문자열" 리터럴로 직접 기입
*/
@DisplayName("ConcatN - Parser 변수/상수 혼합 테스트")
class ConcatNParserTest {
private Parser parser;
@BeforeEach
void setUp() {
parser = new Parser();
// DB 등록 전 테스트이므로 직접 등록
parser.getJep().addFunction("concatn", new ConcatN());
}
// ─────────────────────────────────────────────────────────────────
// 1. 변수만 사용
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("변수 2개 — parse 1회, 값 교체 반복 평가")
void testTwoVariables() throws Exception {
parser.parse("concatn(a, b)");
parser.addVariable("a", "Hello");
parser.addVariable("b", "World");
assertEquals("HelloWorld", parser.getValueAsObject());
parser.addVariable("a", "foo");
parser.addVariable("b", "bar");
assertEquals("foobar", parser.getValueAsObject());
}
@Test
@DisplayName("변수 3개")
void testThreeVariables() throws Exception {
parser.parse("concatn(v1, v2, v3)");
parser.addVariable("v1", "AAA");
parser.addVariable("v2", "BBB");
parser.addVariable("v3", "CCC");
assertEquals("AAABBBCCC", parser.getValueAsObject());
}
@Test
@DisplayName("변수 5개")
void testFiveVariables() throws Exception {
parser.parse("concatn(y, m, d, h, mi)");
parser.addVariable("y", "2024");
parser.addVariable("m", "05");
parser.addVariable("d", "08");
parser.addVariable("h", "14");
parser.addVariable("mi", "30");
assertEquals("20240508" + "14" + "30", parser.getValueAsObject());
}
// ─────────────────────────────────────────────────────────────────
// 2. 변수 + 상수(리터럴) 혼합
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("날짜 구분자 '-' 상수 삽입 — 변수 교체 반복")
void testDateWithSeparator() throws Exception {
parser.parse("concatn(year, \"-\", month, \"-\", day)");
parser.addVariable("year", "2024");
parser.addVariable("month", "01");
parser.addVariable("day", "15");
assertEquals("2024-01-15", parser.getValueAsObject());
parser.addVariable("year", "2025");
parser.addVariable("month", "12");
parser.addVariable("day", "31");
assertEquals("2025-12-31", parser.getValueAsObject());
}
@Test
@DisplayName("접두·접미 상수로 감싸기")
void testWrapWithConstants() throws Exception {
parser.parse("concatn(\"[\", code, \"-\", seq, \"]\")");
parser.addVariable("code", "KOR");
parser.addVariable("seq", "001");
assertEquals("[KOR-001]", parser.getValueAsObject());
parser.addVariable("code", "USA");
parser.addVariable("seq", "999");
assertEquals("[USA-999]", parser.getValueAsObject());
}
@Test
@DisplayName("앞에 상수 접두어 + 변수 1개")
void testConstantPrefix() throws Exception {
parser.parse("concatn(\"REQ-\", txId)");
parser.addVariable("txId", "20240508001");
assertEquals("REQ-20240508001", parser.getValueAsObject());
parser.addVariable("txId", "20240508002");
assertEquals("REQ-20240508002", parser.getValueAsObject());
}
@Test
@DisplayName("변수 사이에 공백 상수 삽입")
void testSpaceSeparator() throws Exception {
parser.parse("concatn(firstName, \" \", lastName)");
parser.addVariable("firstName", "Gil-dong");
parser.addVariable("lastName", "Hong");
assertEquals("Gil-dong Hong", parser.getValueAsObject());
}
// ─────────────────────────────────────────────────────────────────
// 3. 성능 측정 (ParserTest 스타일)
// ─────────────────────────────────────────────────────────────────
@Test
@DisplayName("100만 회 반복 평가 — parse 1회 재사용 성능")
void testPerformance() throws Exception {
String expr = "concatn(code, \"-\", seq)";
parser.parse(expr);
Object result = null;
long t = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
parser.addVariable("code", "KOR");
parser.addVariable("seq", String.valueOf(i));
result = parser.getValueAsObject();
}
long elapsed = System.currentTimeMillis() - t;
System.out.println(String.format("%s = %s %dms", expr, result, elapsed));
assertEquals("KOR-999999", result);
}
}