SpringBoot 테스트 코드 동작 중 "Could not open JDBC Connection for transaction"
1. 오류 코드
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:313) ~[spring-jdbc-6.1.13.jar:6.1.13]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:532) ~[spring-tx-6.1.13.jar:6.1.13]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:405) ~[spring-tx-6.1.13.jar:6.1.13]
at org.springframework.test.context.transaction.TransactionContext.startTransaction(TransactionContext.java:103) ~[spring-test-6.1.13.jar:6.1.13]
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.beforeTestMethod(TransactionalTestExecutionListener.java:236) ~[spring-test-6.1.13.jar:6.1.13]
at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:320) ~[spring-test-6.1.13.jar:6.1.13]
at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:240) ~[spring-test-6.1.13.jar:6.1.13]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$2(TestMethodTestDescriptor.java:167) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:203) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:203) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:166) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:133) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69) ~[junit-jupiter-engine-5.10.3.jar:5.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:198) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:169) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:93) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:58) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:141) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:57) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:63) ~[junit-platform-launcher-1.10.3.jar:1.10.3]
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57) ~[junit5-rt.jar:na]
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) ~[junit-rt.jar:na]
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) ~[idea_rt.jar:na]
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) ~[junit-rt.jar:na]
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) ~[junit-rt.jar:na]
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55) ~[junit-rt.jar:na]
Caused by: org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused: getsockopt: localhost" [90067-224]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:690) ~[h2-2.2.224.jar:2.2.224]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) ~[h2-2.2.224.jar:2.2.224]
at org.h2.message.DbException.get(DbException.java:212) ~[h2-2.2.224.jar:2.2.224]
at org.h2.engine.SessionRemote.connectServer(SessionRemote.java:442) ~[h2-2.2.224.jar:2.2.224]
at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:330) ~[h2-2.2.224.jar:2.2.224]
at org.h2.jdbc.JdbcConnection.(JdbcConnection.java:125) ~[h2-2.2.224.jar:2.2.224]
at org.h2.Driver.connect(Driver.java:59) ~[h2-2.2.224.jar:2.2.224]
at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:137) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:360) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:202) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:461) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:550) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:98) ~[HikariCP-5.1.0.jar:na]
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:111) ~[HikariCP-5.1.0.jar:na]
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:269) ~[spring-jdbc-6.1.13.jar:6.1.13]
... 60 common frames omitted
Caused by: java.net.ConnectException: Connection refused: getsockopt
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[na:na]
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) ~[na:na]
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542) ~[na:na]
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) ~[na:na]
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) ~[na:na]
at java.base/java.net.Socket.connect(Socket.java:751) ~[na:na]
at org.h2.util.NetUtils.createSocket(NetUtils.java:135) ~[h2-2.2.224.jar:2.2.224]
at org.h2.util.NetUtils.createSocket(NetUtils.java:99) ~[h2-2.2.224.jar:2.2.224]
at org.h2.engine.SessionRemote.initTransfer(SessionRemote.java:130) ~[h2-2.2.224.jar:2.2.224]
at org.h2.engine.SessionRemote.connectServer(SessionRemote.java:438) ~[h2-2.2.224.jar:2.2.224]
... 71 common frames omitted
WARNING: A Java agent has been loaded dynamically (C:\Users\theli\.gradle\caches\modules-2\files-2.1\net.bytebuddy\byte-buddy-agent\1.14.19\154da3a65b4f4a909d3e5bdec55d1b2b4cbb6ce1\byte-buddy-agent-1.14.19.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:313)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:532)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:405)
at org.springframework.test.context.transaction.TransactionContext.startTransaction(TransactionContext.java:103)
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.beforeTestMethod(TransactionalTestExecutionListener.java:236)
at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:320)
at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:240)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused: getsockopt: localhost" [90067-224]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:690)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
at org.h2.message.DbException.get(DbException.java:212)
at org.h2.engine.SessionRemote.connectServer(SessionRemote.java:442)
at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:330)
at org.h2.jdbc.JdbcConnection.(JdbcConnection.java:125)
at org.h2.Driver.connect(Driver.java:59)
at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:137)
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:360)
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:202)
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:461)
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:550)
at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:98)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:111)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:269)
... 8 more
Caused by: java.net.ConnectException: Connection refused: getsockopt
at java.base/sun.nio.ch.Net.pollConnect(Native Method)
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682)
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:751)
at org.h2.util.NetUtils.createSocket(NetUtils.java:135)
at org.h2.util.NetUtils.createSocket(NetUtils.java:99)
at org.h2.engine.SessionRemote.initTransfer(SessionRemote.java:130)
at org.h2.engine.SessionRemote.connectServer(SessionRemote.java:438)
... 19 more
Process finished with exit code -1
2. 직면 상황
김영한 님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣던 중,
회원가입 테스트 코드에서 @SpringBootTest, @Transactional 를 추가하여..
스프링을 실행한 테스트 코드를 실행했을 때 오류가 발생했다.
3. 문제 코드
MemberServiceIntegrationTest.java
@SpringBootTest //부트 테스트
@Transactional
class MemberServiceIntegrationTest {
/* 중략 */
@Test
void 회원가입() {
//given -> when -> then 문법
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
/* 후략 */
JdbcMemberRepository.java
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
/* 중략 */
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. 문제 발생 원인
강의에서 레거시 코드라고 말씀하시면서 복붙을 하셨는데,
이때 이미 배우고 써본 내용이라.. 강의자료에서 복사-붙혀넣기를 하지 않고
직접 코드를 작성하는 과정에서 잘못된 코드를 작성하였다.
문제가 된 코드는 사용한 리소스 중 커넥션을
conn.close()로 닫았는데 이게 문제가 되었다.
스프링부트 테스트코드에서 @Transactional 어노테이션을 사용하였는데,
이 어노테이션을 사용하면 스프링부트는 테스트 코드를 실행이 완료된 이후
다시 데이터베이스를 rollback 해준다.
즉, 테스트 코드 내부에서 데이터베이스에 추가, 삭제, 업데이트된 내용이 다시 원래대로 돌아오게 되는 것인데..
conn.close()로 커넥션을 이미 닫아버렸기 때문에
데이터베이스와 연결이 종료된 상태에서 스프링부트가 롤백을 하려고 하니..
연결이 종료된 커넥션이라 문제가 롤백할 수 없는 문제가 발생했다.
5. 문제해결 방법
스프링부트에서 데이터베이스와 미리 여러 개의 커넥션을 열어두고,
데이터베이스에 접속 요청이 들어오면 기존에 열어둔 커넥션으로 통신을 한다.
이러한 데이터베이스와의 소켓 연결을 미리 열어둔 커넥션들을 모아둔 곳을 커넥션 풀이라고 한다.
즉 이러한 커넥션 풀을 사용하고 있기 때문에, 데이터베이스 작업이 모두 완료되면
앞에서 작업한 것과 같이 conn.close()를 사용하여 데이터베이스 커넥션을 닫는 것이 아니라,
커넥션 풀에 커넥션을 반납해야한다.
org.springframework.jdbc.datasource.DataSourceUtils
위 클래스를 사용한 코드로 수정해주면 스프링 커넥션 풀로 커넥션을 반납할 수 있다.
conn.close()를 DataSourceUtils.releaseConnection(conn, dataSource) 로 수정해주었다.
releaseConnection 메소드에서 SQL Exception을 catch하여 처리해주기 때문에
예외처리도 삭제할 수 있었다.
6. 무엇이 달랐을까?
아직 스프링에 대해 자세히 모르기에 정확히 분석은 불가능하겠지만..conn.close()는 명시적으로 내가 바로 connection을 닫은 반면
DataSourceUtils.releaseConnection은 doReleaseConnection을 호출한다.
doReleaseConnection은 TransactionSynchronizationManager를 통해
트랜잭션 동기화 작업이 된 이후에 doCloseConnection을 호출한다.
이후 doCloseConnection 메소드에서 커넥션을 닫아준다.
<참고>
doReleaseConnection을 호출해도 오류는 발생하지 않는데, releaseConnection을 사용하는 이유는 뭘까?
doReleaseConnection은 예외처리를 메소드 안에서 catch해주지 않는데,
releaseConnection은 예외처리를 메소드안에서 모두 처리해주기 때문에
사용하는 개발자가 예외처리를 하지 않아도 된다는 장점이 있다.
즉...만약 예외처리를 개발자가 따로 직접하고 싶다면 doReleaseConnection과 같은 메소드를 사용하면 되고,
이를 스프링 프레임워크에 맡기고 싶다면 releaseConnection을 사용하면 되지 않을까 싶다.
댓글
댓글 쓰기
광고, 스팸 댓글은 삭제 됩니다.