From 586a58bba094594d999dfc44a7f4291ec546eb88 Mon Sep 17 00:00:00 2001 From: wantaek Date: Thu, 3 Sep 2026 10:24:31 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20primitive=20float=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20sqlTypes=20=ED=91=9C=EA=B8=B0=20=EB=B6=88=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=EB=A1=9C=20JDBC=20=EB=B0=B0=EC=B9=98=20=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=EC=97=90=EC=84=9C=20SQLException=20=EC=9D=B4=20?= =?UTF-8?q?=EB=82=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...vMethodMapItemPreparedStatementSetter.java | 2 +- .../EgovMethodMapFloatSqlTypeTest.java | 216 ++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/database/EgovMethodMapFloatSqlTypeTest.java diff --git a/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/database/support/EgovMethodMapItemPreparedStatementSetter.java b/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/database/support/EgovMethodMapItemPreparedStatementSetter.java index 3252da56..3bce803a 100755 --- a/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/database/support/EgovMethodMapItemPreparedStatementSetter.java +++ b/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/database/support/EgovMethodMapItemPreparedStatementSetter.java @@ -70,7 +70,7 @@ public void setValues(T item, PreparedStatement ps, String[] params, String[] sq ps.setBoolean(i + 1, (Boolean) reflector.invokeGettterMethod(item, params[i], methodMap)); } else if (sqlTypes[i].equals("long")) { ps.setLong(i + 1, (Long) reflector.invokeGettterMethod(item, params[i], methodMap)); - } else if (sqlTypes[i].equals("Float")) { + } else if (sqlTypes[i].equals("float") || sqlTypes[i].equals("Float")) { ps.setFloat(i + 1, (Float) reflector.invokeGettterMethod(item, params[i], methodMap)); } else if (sqlTypes[i].equals("BigDecimal")) { ps.setBigDecimal(i + 1, (BigDecimal) reflector.invokeGettterMethod(item, params[i], methodMap)); diff --git a/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/database/EgovMethodMapFloatSqlTypeTest.java b/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/database/EgovMethodMapFloatSqlTypeTest.java new file mode 100644 index 00000000..1a9e57df --- /dev/null +++ b/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/database/EgovMethodMapFloatSqlTypeTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2008-2024 MOIS(Ministry of the Interior and Safety). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.egovframe.rte.bat.core.item.database; + +import org.egovframe.rte.bat.core.item.database.support.EgovMethodMapItemPreparedStatementSetter; +import org.egovframe.rte.bat.core.reflection.EgovReflectionSupport; +import org.junit.jupiter.api.Test; +import org.springframework.batch.item.Chunk; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCallback; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * sqlTypes 산출부와 소비부의 float 타입 표기 일치 검증. + * + *
+ * 산출부 EgovReflectionSupport.getSqlTypeArray 는 Class.getSimpleName() 을 쓰므로
+ * primitive 필드에 대해 "int", "double", "long" 처럼 소문자 이름을 낸다.
+ * primitive float 도 마찬가지로 "float" 이 산출되며, 소비부
+ * EgovMethodMapItemPreparedStatementSetter 가 이를 받아 ps.setFloat 으로 넘겨야 한다.
+ * 
+ * + * @author 배치실행개발팀 + * @since 2026.09.03 + */ +class EgovMethodMapFloatSqlTypeTest { + + /** primitive float 과, 정상 동작하는 형제 primitive(double)를 함께 담은 VO. */ + public static class PrimitiveFloatVo { + private double score; + private float rate; + + public PrimitiveFloatVo(double score, float rate) { + this.score = score; + this.rate = rate; + } + + public double getScore() { + return score; + } + + public float getRate() { + return rate; + } + } + + /** wrapper Float 필드 VO. 기존 동작 보존 확인용. */ + public static class WrapperFloatVo { + private Float ratio; + + public WrapperFloatVo(Float ratio) { + this.ratio = ratio; + } + + public Float getRatio() { + return ratio; + } + } + + /** ps.setXxx 호출을 (메소드명, 인덱스, 값) 으로 기록하는 PreparedStatement 대역. */ + private static class RecordingPsHandler implements InvocationHandler { + private final List calls = new ArrayList(); + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + String name = method.getName(); + if (name.startsWith("set") && args != null && args.length == 2) { + calls.add(new Object[]{name, args[0], args[1]}); + return null; + } + if ("hashCode".equals(name)) { + return System.identityHashCode(proxy); + } + if ("equals".equals(name)) { + return proxy == args[0]; + } + if ("toString".equals(name)) { + return "RecordingPs"; + } + return null; + } + } + + private static PreparedStatement newRecordingPs(RecordingPsHandler handler) { + return (PreparedStatement) Proxy.newProxyInstance( + RecordingPsHandler.class.getClassLoader(), + new Class[]{PreparedStatement.class}, handler); + } + + /** + * 산출부가 낸 sqlTypes 를 그대로 소비부에 넘겼을 때, primitive float 이 + * 형제 primitive 와 동일하게 ps.setFloat 으로 설정되어야 한다. + */ + @Test + void primitiveFloatIsSetLikeSiblingPrimitives() throws Exception { + String[] params = {"score", "rate"}; + PrimitiveFloatVo item = new PrimitiveFloatVo(88.5d, 1.25f); + + EgovReflectionSupport support = new EgovReflectionSupport(); + support.generateGetterMethodMap(params, item); + Map methodMap = support.getMethodMap(); + String[] sqlTypes = support.getSqlTypeArray(params, item); + + // 산출부는 primitive 를 소문자 이름으로 낸다. + assertEquals("double", sqlTypes[0]); + assertEquals("float", sqlTypes[1]); + + RecordingPsHandler handler = new RecordingPsHandler(); + PreparedStatement ps = newRecordingPs(handler); + + new EgovMethodMapItemPreparedStatementSetter() + .setValues(item, ps, params, sqlTypes, methodMap); + + assertEquals(2, handler.calls.size()); + + assertEquals("setDouble", handler.calls.get(0)[0]); + assertEquals(1, handler.calls.get(0)[1]); + assertEquals(88.5d, handler.calls.get(0)[2]); + + assertEquals("setFloat", handler.calls.get(1)[0]); + assertEquals(2, handler.calls.get(1)[1]); + assertEquals(1.25f, handler.calls.get(1)[2]); + } + + /** + * wrapper Float 필드는 기존과 동일하게 ps.setFloat 으로 설정되어야 한다. + */ + @Test + void wrapperFloatIsStillSet() throws Exception { + String[] params = {"ratio"}; + WrapperFloatVo item = new WrapperFloatVo(2.5f); + + EgovReflectionSupport support = new EgovReflectionSupport(); + support.generateGetterMethodMap(params, item); + Map methodMap = support.getMethodMap(); + String[] sqlTypes = support.getSqlTypeArray(params, item); + + assertEquals("Float", sqlTypes[0]); + + RecordingPsHandler handler = new RecordingPsHandler(); + PreparedStatement ps = newRecordingPs(handler); + + new EgovMethodMapItemPreparedStatementSetter() + .setValues(item, ps, params, sqlTypes, methodMap); + + assertEquals(1, handler.calls.size()); + assertEquals("setFloat", handler.calls.get(0)[0]); + assertEquals(1, handler.calls.get(0)[1]); + assertEquals(2.5f, handler.calls.get(0)[2]); + } + + /** + * EgovJdbcBatchItemWriter.write() 를 경유하는 실제 청크 쓰기 경로에서도 primitive float 이 + * ps.setFloat 으로 설정되어야 한다. sqlTypes 산출과 소비가 모두 프레임워크 내부에서 일어난다. + * PreparedStatement 대역은 형제 테스트의 FakePsHandler 를 재사용한다. + */ + @Test + void primitiveFloatIsSetThroughWriter() throws Exception { + final EgovJdbcBatchWriteReflectionTest.FakePsHandler handler = + new EgovJdbcBatchWriteReflectionTest.FakePsHandler(); + + EgovJdbcBatchItemWriter writer = new EgovJdbcBatchItemWriter(); + writer.setSql("insert into sample (score, rate) values (?, ?)"); + writer.setParams(new String[]{"score", "rate"}); + writer.setItemPreparedStatementSetter(new EgovMethodMapItemPreparedStatementSetter()); + writer.setSimpleJdbcTemplate(new JdbcTemplate() { + @Override + public X execute(String sql, PreparedStatementCallback action) { + try { + return action.doInPreparedStatement((PreparedStatement) Proxy.newProxyInstance( + EgovMethodMapFloatSqlTypeTest.class.getClassLoader(), + new Class[]{PreparedStatement.class}, handler)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + }); + writer.afterPropertiesSet(); + + writer.write(new Chunk(List.of(new PrimitiveFloatVo(88.5d, 1.25f)))); + + assertEquals(2, handler.calls.size()); + + assertEquals("setDouble", handler.calls.get(0)[0]); + assertEquals(1, handler.calls.get(0)[1]); + assertEquals(88.5d, handler.calls.get(0)[2]); + + assertEquals("setFloat", handler.calls.get(1)[0]); + assertEquals(2, handler.calls.get(1)[1]); + assertEquals(1.25f, handler.calls.get(1)[2]); + } + +}