From 130fce799cea3f3b9ebbf048a1548c1da4889b4d Mon Sep 17 00:00:00 2001 From: wantaek Date: Thu, 3 Sep 2026 13:58:08 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20setParameter=20=EA=B0=80=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=ED=95=9C=20LobCreator=20=EB=A5=BC=20=EB=8B=AB?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EB=93=B1=EB=A1=9D=ED=95=98=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setParameter 는 LobCreator 를 만든 뒤 setParameterInternal 이 성공해야 LobCreatorSynchronization 을 등록한다. 이 동기화가 beforeCompletion 에서 close 를 부르는 유일한 자리다. setParameterInternal 이 SQLException 이나 IOException 을 던지면 등록에 닿지 못하므로 그 LobCreator 는 닫히지 않는다. 클래스 Javadoc 은 LOB 을 쓰려면 트랜잭션 동기화가 활성이어야 하는 이유를 "LobCreator 를 닫는 동기화를 등록하기 위해서" 라고 적는다. 만든 LobCreator 는 반드시 닫힌다는 뜻이다. 같은 패키지의 BlobSerializableTypeHandler 도 ObjectOutputStream·ObjectInputStream 을 finally 에서 닫아 실패 경로를 함께 덮는다. DefaultLobHandler 는 createTemporaryLob 을 켜면 TemporaryLobCreator 를 돌려주고 그 close 가 만들어 둔 Blob·Clob 의 free 를 부른다. 닫히지 않으면 그만큼이 세션에 남는다. 등록 전에 빠져나가는 경우에만 닫도록 finally 를 두었다. 성공 경로의 동작은 그대로다. --- .../support/AbstractLobTypeHandler.java | 6 ++ .../ibatis/support/LobCreatorCleanupTest.java | 83 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 Persistence/org.egovframe.rte.psl.dataaccess/src/test/java/org/egovframe/rte/psl/orm/ibatis/support/LobCreatorCleanupTest.java diff --git a/Persistence/org.egovframe.rte.psl.dataaccess/src/main/java/org/egovframe/rte/psl/orm/ibatis/support/AbstractLobTypeHandler.java b/Persistence/org.egovframe.rte.psl.dataaccess/src/main/java/org/egovframe/rte/psl/orm/ibatis/support/AbstractLobTypeHandler.java index a5d99750..d85b3d40 100755 --- a/Persistence/org.egovframe.rte.psl.dataaccess/src/main/java/org/egovframe/rte/psl/orm/ibatis/support/AbstractLobTypeHandler.java +++ b/Persistence/org.egovframe.rte.psl.dataaccess/src/main/java/org/egovframe/rte/psl/orm/ibatis/support/AbstractLobTypeHandler.java @@ -97,11 +97,17 @@ public final void setParameter(PreparedStatement ps, int i, Object parameter, St } final LobCreator lobCreator = this.lobHandler.getLobCreator(); + boolean succeeded = false; try { setParameterInternal(ps, i, parameter, jdbcType, lobCreator); + succeeded = true; } catch (IOException ex) { throw new SQLException("I/O errors during LOB access: " + ex.getMessage()); + } finally { + if (!succeeded) { + lobCreator.close(); + } } TransactionSynchronizationManager.registerSynchronization(new LobCreatorSynchronization(lobCreator)); diff --git a/Persistence/org.egovframe.rte.psl.dataaccess/src/test/java/org/egovframe/rte/psl/orm/ibatis/support/LobCreatorCleanupTest.java b/Persistence/org.egovframe.rte.psl.dataaccess/src/test/java/org/egovframe/rte/psl/orm/ibatis/support/LobCreatorCleanupTest.java new file mode 100644 index 00000000..867b5f0a --- /dev/null +++ b/Persistence/org.egovframe.rte.psl.dataaccess/src/test/java/org/egovframe/rte/psl/orm/ibatis/support/LobCreatorCleanupTest.java @@ -0,0 +1,83 @@ +package org.egovframe.rte.psl.orm.ibatis.support; + +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * setParameter 가 만든 LobCreator 는 성공하든 실패하든 정리돼야 한다. + * + *

드라이버가 파라미터 설정을 거부하면 setParameterInternal 이 SQLException 을 던지는데, + * 이때도 LobCreator.close 가 불리거나 close 를 부르는 동기화가 등록돼 있어야 + * 임시 LOB 이 남지 않는다.

+ */ +public class LobCreatorCleanupTest { + + @Test + public void testLobCreatorIsClosedWhenSetParameterFails() { + AtomicInteger closeCount = new AtomicInteger(); + LobHandler lobHandler = countingLobHandler(closeCount); + BlobByteArrayTypeHandler typeHandler = new BlobByteArrayTypeHandler(lobHandler); + PreparedStatement ps = failingPreparedStatement(); + + TransactionSynchronizationManager.initSynchronization(); + try { + assertThrows(SQLException.class, + () -> typeHandler.setParameter(ps, 1, new byte[]{1, 2, 3}, "BLOB")); + + for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { + synchronization.beforeCompletion(); + } + + assertEquals(1, closeCount.get(), + "setParameter 가 실패해도 LobCreator 는 닫혀야 한다"); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + + private LobHandler countingLobHandler(AtomicInteger closeCount) { + LobCreator delegate = new DefaultLobHandler().getLobCreator(); + LobCreator counting = (LobCreator) Proxy.newProxyInstance( + LobCreator.class.getClassLoader(), + new Class[]{LobCreator.class}, + (proxy, method, args) -> { + if ("close".equals(method.getName())) { + closeCount.incrementAndGet(); + } + try { + return method.invoke(delegate, args); + } catch (InvocationTargetException ex) { + throw ex.getCause(); + } + }); + return (LobHandler) Proxy.newProxyInstance( + LobHandler.class.getClassLoader(), + new Class[]{LobHandler.class}, + (proxy, method, args) -> "getLobCreator".equals(method.getName()) ? counting : null); + } + + private PreparedStatement failingPreparedStatement() { + return (PreparedStatement) Proxy.newProxyInstance( + PreparedStatement.class.getClassLoader(), + new Class[]{PreparedStatement.class}, + (proxy, method, args) -> { + if ("setBytes".equals(method.getName())) { + throw new SQLException("데이터 타입이 맞지 않습니다"); + } + return null; + }); + } +}