From 27854996f66b688ff28f5c1472dde5fa4ef30378 Mon Sep 17 00:00:00 2001 From: ReO Date: Fri, 6 Oct 2023 19:05:35 +0900 Subject: [PATCH 01/12] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/build.gradle | 1 + .../java/transaction/stage1/Stage1Test.java | 47 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/study/build.gradle b/study/build.gradle index c7aff0460e..a7a0c17937 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -1,3 +1,4 @@ + plugins { id "org.springframework.boot" version "2.7.4" id "io.spring.dependency-management" version "1.0.11.RELEASE" diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index 8c29944d4e..8dc9050374 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -1,5 +1,7 @@ package transaction.stage1; +import static org.assertj.core.api.Assertions.assertThat; + import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.h2.jdbcx.JdbcDataSource; @@ -18,8 +20,6 @@ import java.sql.SQLException; import java.util.concurrent.TimeUnit; -import static org.assertj.core.api.Assertions.assertThat; - /** * 격리 레벨(Isolation Level)에 따라 여러 사용자가 동시에 db에 접근했을 때 어떤 문제가 발생하는지 확인해보자. * ❗phantom reads는 docker를 실행한 상태에서 테스트를 실행한다. @@ -34,10 +34,10 @@ * Read phenomena | Dirty reads | Non-repeatable reads | Phantom reads * Isolation level | | | * -----------------|-------------|----------------------|-------------- - * Read Uncommitted | | | - * Read Committed | | | - * Repeatable Read | | | - * Serializable | | | + * Read Uncommitted | + | + | + + * Read Committed | - | + | + + * Repeatable Read | - | - | + + * Serializable | - | - | - */ class Stage1Test { @@ -58,10 +58,10 @@ private void setUp(final DataSource dataSource) { * Read phenomena | Dirty reads * Isolation level | * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | - + * Repeatable Read | - + * Serializable | - */ @Test void dirtyReading() throws SQLException { @@ -81,7 +81,7 @@ void dirtyReading() throws SQLException { final var subConnection = dataSource.getConnection(); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. subConnection.setTransactionIsolation(isolationLevel); @@ -111,10 +111,10 @@ void dirtyReading() throws SQLException { * Read phenomena | Non-repeatable reads * Isolation level | * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | + + * Repeatable Read | - + * Serializable | - */ @Test void noneRepeatable() throws SQLException { @@ -130,7 +130,7 @@ void noneRepeatable() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -173,10 +173,10 @@ void noneRepeatable() throws SQLException { * Read phenomena | Phantom reads * Isolation level | * -----------------|-------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | + + * Repeatable Read | + + * Serializable | - */ @Test void phantomReading() throws SQLException { @@ -184,6 +184,7 @@ void phantomReading() throws SQLException { // testcontainer로 docker를 실행해서 mysql에 연결한다. final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) .withLogConsumer(new Slf4jLogConsumer(log)); + mysql.withUrlParam("allowMultiQueries", "true"); mysql.start(); setUp(createMySQLDataSource(mysql)); @@ -197,7 +198,7 @@ void phantomReading() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -249,8 +250,8 @@ private static DataSource createMySQLDataSource(final JdbcDatabaseContainer c private static DataSource createH2DataSource() { final var jdbcDataSource = new JdbcDataSource(); // h2 로그를 확인하고 싶을 때 사용 -// jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); - jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL;"); + jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); + // jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL;"); jdbcDataSource.setUser("sa"); jdbcDataSource.setPassword(""); return jdbcDataSource; From 275bd8903dd5fac7d27e03df31a9c3da01b853fd Mon Sep 17 00:00:00 2001 From: ReO Date: Mon, 9 Oct 2023 01:42:27 +0900 Subject: [PATCH 02/12] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B82=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/jdbc/JdbcTemplateTest.java | 17 +++++----- .../transaction/stage2/FirstUserService.java | 13 ++++---- .../java/transaction/stage2/Stage2Test.java | 32 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 3740fa7bce..300f1353b5 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -1,12 +1,5 @@ package nextstep.jdbc; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -19,9 +12,13 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.List; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class JdbcTemplateTests { @@ -89,7 +86,9 @@ public void setup() throws Exception { given(this.preparedStatement.executeQuery()).willThrow(sqlException); assertThatExceptionOfType(DataAccessException.class) - .isThrownBy(() -> this.template.query(sql, resultSet -> {return "";})) + .isThrownBy(() -> this.template.query(sql, resultSet -> { + return ""; + })) .withCause(sqlException); verify(this.preparedStatement).close(); verify(this.connection, atLeastOnce()).close(); diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java index 9a1d415c18..a5df05b72b 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -66,18 +66,19 @@ public Set saveAndExceptionWithRequiredNew() { throw new RuntimeException(); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithSupports() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + System.out.println("First_Transaction : " + firstTransactionName); userRepository.save(User.createTest()); logActualTransactionActive(); final var secondTransactionName = secondUserService.saveSecondTransactionWithSupports(); - + System.out.println("Second_Transaction : " + secondTransactionName); return of(firstTransactionName, secondTransactionName); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithMandatory() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -88,7 +89,7 @@ public Set saveFirstTransactionWithMandatory() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNotSupported() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -99,7 +100,7 @@ public Set saveFirstTransactionWithNotSupported() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNested() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -110,7 +111,7 @@ public Set saveFirstTransactionWithNested() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNever() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); diff --git a/study/src/test/java/transaction/stage2/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java index e522bf4365..2e8da4789f 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -45,8 +45,8 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** @@ -59,8 +59,8 @@ void testRequiredNew() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew", "transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew"); } /** @@ -69,12 +69,12 @@ void testRequiredNew() { */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(1); } /** @@ -87,8 +87,8 @@ void testSupports() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** @@ -102,8 +102,8 @@ void testMandatory() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** @@ -119,8 +119,8 @@ void testNotSupported() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported"); } /** @@ -133,8 +133,8 @@ void testNested() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNested"); } /** @@ -146,7 +146,7 @@ void testNever() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNever"); } } From a61dae9ea68a1cf2870e832b3d532b6c28de1bba Mon Sep 17 00:00:00 2001 From: ReO Date: Mon, 9 Oct 2023 16:16:20 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/datasource/DataSourceUtils.java | 7 ++- .../TransactionSynchronizationManager.java | 31 ++++++++-- .../support/TransactionTemplate.java | 56 +++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 3c40bfec52..809ce65195 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -7,20 +7,19 @@ import java.sql.Connection; import java.sql.SQLException; -// 4단계 미션에서 사용할 것 public abstract class DataSourceUtils { private DataSourceUtils() {} public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { - Connection connection = TransactionSynchronizationManager.getResource(dataSource); + Connection connection = TransactionSynchronizationManager.getConnection(dataSource); if (connection != null) { return connection; } try { connection = dataSource.getConnection(); - TransactionSynchronizationManager.bindResource(dataSource, connection); + TransactionSynchronizationManager.bindConnection(dataSource, connection); return connection; } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); @@ -32,6 +31,8 @@ public static void releaseConnection(Connection connection, DataSource dataSourc connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); + } finally { + TransactionSynchronizationManager.unbindConnection(dataSource); } } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..71211fd314 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,7 +1,10 @@ package org.springframework.transaction.support; +import org.springframework.jdbc.CannotGetJdbcConnectionException; + import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { @@ -10,14 +13,32 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() {} - public static Connection getResource(DataSource key) { - return null; + public static Connection getConnection(DataSource dataSource) { + Map dataSourceAndConnection = resources.get(); + if (dataSourceAndConnection == null) { + throw new CannotGetJdbcConnectionException("DataSource에 binding된 Connection이 없습니다."); + } + + return dataSourceAndConnection.get(dataSource); } - public static void bindResource(DataSource key, Connection value) { + public static void bindConnection(DataSource dataSource, Connection connection) { + if (resources.get() == null) { + Map threadResource = new HashMap<>(); + resources.set(threadResource); + } + + resources.get().put(dataSource, connection); } - public static Connection unbindResource(DataSource key) { - return null; + public static Connection unbindConnection(DataSource dataSource) { + if (resources.get() == null || !resources.get().containsKey(dataSource)) { + throw new IllegalArgumentException("등록된 DataSource가 없습니다."); + } + + Map resource = resources.get(); + Connection connection = resource.get(dataSource); + resources.remove(); + return connection; } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java new file mode 100644 index 0000000000..c7a5c109d0 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,56 @@ +package org.springframework.transaction.support; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class TransactionTemplate { + + private final DataSource dataSource; + + public TransactionTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void execute(Runnable runnable) { + Connection connection = DataSourceUtils.getConnection(dataSource); + begin(connection); + try { + runnable.run(); + + commit(connection); + } catch (Exception e) { + rollback(connection); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + + private void begin(Connection connection) { + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private void commit(Connection connection) { + try { + connection.commit(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private void rollback(Connection connection) { + try { + connection.rollback(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } +} From c13f3d63cd3b168eefa817ec0e7f6b9285883869 Mon Sep 17 00:00:00 2001 From: ReO Date: Mon, 9 Oct 2023 16:16:39 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TxUserService.java | 30 ++++++++++ ...rviceTest.java => AppUserServiceTest.java} | 35 +++--------- .../techcourse/service/TxUserServiceTest.java | 56 +++++++++++++++++++ 3 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/TxUserService.java rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => AppUserServiceTest.java} (50%) create mode 100644 app/src/test/java/com/techcourse/service/TxUserServiceTest.java diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..e4c9ce244b --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,30 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionTemplate; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionTemplate transactionTemplate; + + public TxUserService(UserService userService, TransactionTemplate transactionTemplate) { + this.userService = userService; + this.transactionTemplate = transactionTemplate; + } + + @Override + public User findById(long id) { + return userService.findById(id); + } + + @Override + public void insert(User user) { + transactionTemplate.execute(() -> userService.insert(user)); + } + + @Override + public void changePassword(long id, String newPassword, String createBy) { + transactionTemplate.execute(() -> userService.changePassword(id, newPassword, createBy)); + } +} diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java similarity index 50% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 255a0ebfe7..157037dd95 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -5,20 +5,17 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -@Disabled -class UserServiceTest { +class AppUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; + private User user; @BeforeEach void setUp() { @@ -26,38 +23,24 @@ void setUp() { this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + userDao.deleteAll(); + final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); + this.user = userDao.findByAccount("gugu"); } @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; - userService.changePassword(1L, newPassword, createBy); + userService.changePassword(user.getId(), newPassword, createBy); - final var actual = userService.findById(1L); + final var actual = userService.findById(user.getId()); assertThat(actual.getPassword()).isEqualTo(newPassword); } - - @Test - void testTransactionRollback() { - // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); - - final var newPassword = "newPassword"; - final var createBy = "gugu"; - // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); - - final var actual = userService.findById(1L); - - assertThat(actual.getPassword()).isNotEqualTo(newPassword); - } } diff --git a/app/src/test/java/com/techcourse/service/TxUserServiceTest.java b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java new file mode 100644 index 0000000000..f5c57f2a1b --- /dev/null +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -0,0 +1,56 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.dao.UserDao; +import com.techcourse.domain.User; +import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TxUserServiceTest { + + private JdbcTemplate jdbcTemplate; + private UserDao userDao; + private TransactionTemplate transactionTemplate; + private User user; + + @BeforeEach + void setUp() { + DataSource dataSource = DataSourceConfig.getInstance(); + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.userDao = new UserDao(jdbcTemplate); + this.transactionTemplate = new TransactionTemplate(dataSource); + + DatabasePopulatorUtils.execute(dataSource); + userDao.deleteAll(); + + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + this.user = userDao.findByAccount("gugu"); + } + + @Test + void testTransactionRollback() { + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + final var appUserService = new AppUserService(userDao, userHistoryDao); + final var userService = new TxUserService(appUserService, transactionTemplate); + + final var newPassword = "newPassword"; + final var createBy = "gugu"; + + assertThrows(CannotGetJdbcConnectionException.class, + () -> userService.changePassword(1L, newPassword, createBy)); + + final var actual = userService.findById(user.getId()); + + assertThat(actual.getPassword()).isNotEqualTo(newPassword); + } +} From eac3a304c76ebf256ea9db109f27f23b9165299d Mon Sep 17 00:00:00 2001 From: ReO Date: Mon, 9 Oct 2023 16:16:53 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techcourse/service/AppUserService.java | 36 +++++++++++++++++++ .../com/techcourse/service/UserService.java | 28 +++------------ 2 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/AppUserService.java diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java new file mode 100644 index 0000000000..b701da749e --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,36 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService{ + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(UserDao userDao, UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(long id) { + return userDao.findById(id); + } + + @Override + public void insert(User user) { + userDao.insert(user); + } + + @Override + public void changePassword(long id, String newPassword, String createBy) { + User user = findById(id); + user.changePassword(newPassword); + + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 579e97179b..a054542fd2 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,32 +1,12 @@ package com.techcourse.service; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -public class UserService { +public interface UserService { - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; + User findById(long id); - public UserService(UserDao userDao, UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } + void insert(User user); - public User findById(long id) { - return userDao.findById(id); - } - - public void insert(User user) { - userDao.insert(user); - } - - public void changePassword(long id, String newPassword, String createBy) { - User user = findById(id); - user.changePassword(newPassword); - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); - } + void changePassword(long id, String newPassword, String createBy); } From 40a783e15dfdab8637df224b161be2b96e3d662c Mon Sep 17 00:00:00 2001 From: ReO Date: Mon, 9 Oct 2023 16:17:31 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20deleteAll=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserDao.java | 6 ++++++ .../test/java/com/techcourse/dao/UserDaoTest.java | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index bf5f438a7e..048f6d80da 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -63,4 +63,10 @@ public User findByAccount(String account) { } return user.get(); } + + public void deleteAll() { + String sql = "delete from users"; + + jdbcTemplate.update(sql); + } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 773d7faf82..5bee0dad1a 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -11,14 +11,17 @@ class UserDaoTest { private UserDao userDao; + private User user; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); userDao = new UserDao(DataSourceConfig.getInstance()); + userDao.deleteAll(); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); + this.user = userDao.findByAccount("gugu"); } @Test @@ -30,7 +33,7 @@ void findAll() { @Test void findById() { - final var user = userDao.findById(1L); + final var user = userDao.findById(this.user.getId()); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -48,8 +51,9 @@ void insert() { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); userDao.insert(user); + User insertedUser = userDao.findByAccount(account); - final var actual = userDao.findById(2L); + final var actual = userDao.findById(insertedUser.getId()); assertThat(actual.getAccount()).isEqualTo(account); } @@ -57,12 +61,12 @@ void insert() { @Test void update() { final var newPassword = "password99"; - final var user = userDao.findById(1L); + final var user = userDao.findById(this.user.getId()); user.changePassword(newPassword); userDao.update(user); - final var actual = userDao.findById(1L); + final var actual = userDao.findById(user.getId()); assertThat(actual.getPassword()).isEqualTo(newPassword); } From 7ef6aedc123fa106ca9e6cdcde475b4eda0868e0 Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 11:07:09 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=A1=B0=ED=9A=8C=EC=8B=9C?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/service/TxUserService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index e4c9ce244b..4b6499f9ca 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -3,6 +3,8 @@ import com.techcourse.domain.User; import org.springframework.transaction.support.TransactionTemplate; +import java.util.concurrent.atomic.AtomicReference; + public class TxUserService implements UserService { private final UserService userService; @@ -15,7 +17,9 @@ public TxUserService(UserService userService, TransactionTemplate transactionTem @Override public User findById(long id) { - return userService.findById(id); + AtomicReference user = new AtomicReference<>(); + transactionTemplate.execute(() -> user.set(userService.findById(id))); + return user.get(); } @Override From f0104fc5d1b6fa6f2f4b98fed041176ea3d73a0b Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 11:23:50 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=EC=98=88=EC=99=B8=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/support/TransactionSynchronizationManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 71211fd314..0c4ac96dfd 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -15,7 +15,7 @@ private TransactionSynchronizationManager() {} public static Connection getConnection(DataSource dataSource) { Map dataSourceAndConnection = resources.get(); - if (dataSourceAndConnection == null) { + if (dataSourceAndConnection == null || dataSourceAndConnection.get(dataSource) == null) { throw new CannotGetJdbcConnectionException("DataSource에 binding된 Connection이 없습니다."); } From 692ec95c312b367ac106322b1c0c025b9ff580ff Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 11:24:13 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore:=20=EA=B0=9C=ED=96=89=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springframework/transaction/support/TransactionTemplate.java | 1 - 1 file changed, 1 deletion(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java index c7a5c109d0..ba36414634 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -29,7 +29,6 @@ public void execute(Runnable runnable) { } } - private void begin(Connection connection) { try { connection.setAutoCommit(false); From d44941fb4352c7aa61989f7ed3f9483a14c1f927 Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 16:25:32 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=95=88=EC=97=90=EC=84=9C=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=EC=85=98=EC=9D=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/init/DatabasePopulatorUtils.java | 1 - .../service/AppUserServiceTest.java | 1 - .../service/MockUserHistoryDao.java | 3 +- .../techcourse/service/TxUserServiceTest.java | 10 +++-- .../{dao => }/DataAccessException.java | 2 +- .../jdbc/core/JdbcTemplate.java | 42 ++++++++++--------- .../jdbc/datasource/DataSourceUtils.java | 31 +++++++------- .../TransactionSynchronizationManager.java | 17 +++++++- .../support/TransactionTemplate.java | 28 +++++++++---- .../java/nextstep/jdbc/JdbcTemplateTest.java | 2 +- 10 files changed, 84 insertions(+), 53 deletions(-) rename jdbc/src/main/java/org/springframework/{dao => }/DataAccessException.java (95%) diff --git a/app/src/main/java/com/techcourse/support/jdbc/init/DatabasePopulatorUtils.java b/app/src/main/java/com/techcourse/support/jdbc/init/DatabasePopulatorUtils.java index 2df76b932a..261780951a 100644 --- a/app/src/main/java/com/techcourse/support/jdbc/init/DatabasePopulatorUtils.java +++ b/app/src/main/java/com/techcourse/support/jdbc/init/DatabasePopulatorUtils.java @@ -19,7 +19,6 @@ public static void execute(DataSource dataSource) { URL url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql"); File file = new File(url.getFile()); String sql = Files.readString(file.toPath()); - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.execute(sql); } catch (IOException | NullPointerException e) { diff --git a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 157037dd95..e77314e1ea 100644 --- a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -21,7 +21,6 @@ class AppUserServiceTest { void setUp() { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); - DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); userDao.deleteAll(); diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..fac766e94c 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,7 +2,7 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import org.springframework.dao.DataAccessException; +import org.springframework.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; public class MockUserHistoryDao extends UserHistoryDao { @@ -13,6 +13,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { @Override public void log(final UserHistory userHistory) { + System.out.println("asdf"); throw new DataAccessException(); } } diff --git a/app/src/test/java/com/techcourse/service/TxUserServiceTest.java b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java index f5c57f2a1b..b0cda3f205 100644 --- a/app/src/test/java/com/techcourse/service/TxUserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -6,7 +6,7 @@ import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.support.TransactionTemplate; @@ -25,13 +25,15 @@ class TxUserServiceTest { @BeforeEach void setUp() { DataSource dataSource = DataSourceConfig.getInstance(); + + DatabasePopulatorUtils.execute(dataSource); this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); this.transactionTemplate = new TransactionTemplate(dataSource); - DatabasePopulatorUtils.execute(dataSource); userDao.deleteAll(); + final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); this.user = userDao.findByAccount("gugu"); @@ -46,8 +48,8 @@ void testTransactionRollback() { final var newPassword = "newPassword"; final var createBy = "gugu"; - assertThrows(CannotGetJdbcConnectionException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + assertThrows(DataAccessException.class, + () -> userService.changePassword(user.getId(), newPassword, createBy)); final var actual = userService.findById(user.getId()); diff --git a/jdbc/src/main/java/org/springframework/dao/DataAccessException.java b/jdbc/src/main/java/org/springframework/DataAccessException.java similarity index 95% rename from jdbc/src/main/java/org/springframework/dao/DataAccessException.java rename to jdbc/src/main/java/org/springframework/DataAccessException.java index 8f81f3ede9..f3bf8b4a92 100644 --- a/jdbc/src/main/java/org/springframework/dao/DataAccessException.java +++ b/jdbc/src/main/java/org/springframework/DataAccessException.java @@ -1,4 +1,4 @@ -package org.springframework.dao; +package org.springframework; public class DataAccessException extends RuntimeException { diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 3326dd9d15..aac86119cb 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,14 +2,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.dao.DataAccessException; +import org.springframework.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -25,25 +22,27 @@ public JdbcTemplate(DataSource dataSource) { } public void update(String sql, Object... values) { - try ( - Connection connection = dataSource.getConnection(); - PreparedStatement pstmt = connection.prepareStatement(sql); - ) { - log.debug("query : {}", sql); + Connection connection = DataSourceUtils.getConnection(dataSource); + try { + PreparedStatement pstmt = connection.prepareStatement(sql); + log.debug("query : {}", sql); setValues(pstmt, values); pstmt.executeUpdate(); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection); } } public List query(String sql, RowMapper rowMapper, Object... values) { - try ( - Connection connection = dataSource.getConnection(); - PreparedStatement pstmt = connection.prepareStatement(sql); - ) { + Connection connection = DataSourceUtils.getConnection(dataSource); + + try { + PreparedStatement pstmt = connection.prepareStatement(sql); + log.debug("query : {}", sql); setValues(pstmt, values); @@ -57,6 +56,8 @@ public List query(String sql, RowMapper rowMapper, Object... values) { } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection); } } @@ -74,16 +75,19 @@ public Optional queryForObject(String sql, RowMapper rowMapper, Object } public void execute(String sql) { - try ( - Connection connection = dataSource.getConnection(); - Statement stmt = connection.createStatement(); - ) { + Connection connection = DataSourceUtils.getConnection(dataSource); + + try { + Statement stmt = connection.createStatement(); + log.debug("query : {}", sql); stmt.execute(sql); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 809ce65195..61465e55b8 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -11,28 +11,29 @@ public abstract class DataSourceUtils { private DataSourceUtils() {} - public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { - Connection connection = TransactionSynchronizationManager.getConnection(dataSource); - if (connection != null) { - return connection; + public static Connection getConnection(DataSource dataSource) { + if (TransactionSynchronizationManager.isInTransaction()) { + return TransactionSynchronizationManager.getConnection(dataSource); } - + try { - connection = dataSource.getConnection(); - TransactionSynchronizationManager.bindConnection(dataSource, connection); - return connection; - } catch (SQLException ex) { - throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); + return dataSource.getConnection(); + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException(e.getMessage()); } } - public static void releaseConnection(Connection connection, DataSource dataSource) { + public static void releaseConnection(Connection connection) { try { - connection.close(); - } catch (SQLException ex) { + releaseIfNotInTransaction(connection); + } catch (SQLException e) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); - } finally { - TransactionSynchronizationManager.unbindConnection(dataSource); + } + } + + private static void releaseIfNotInTransaction(Connection connection) throws SQLException { + if (!TransactionSynchronizationManager.isInTransaction()) { + connection.close(); } } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 0c4ac96dfd..e183d63b10 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,6 +1,6 @@ package org.springframework.transaction.support; -import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.DataAccessException; import javax.sql.DataSource; import java.sql.Connection; @@ -10,13 +10,14 @@ public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal isInTransaction = ThreadLocal.withInitial(() -> false); private TransactionSynchronizationManager() {} public static Connection getConnection(DataSource dataSource) { Map dataSourceAndConnection = resources.get(); if (dataSourceAndConnection == null || dataSourceAndConnection.get(dataSource) == null) { - throw new CannotGetJdbcConnectionException("DataSource에 binding된 Connection이 없습니다."); + throw new DataAccessException("DataSource에 binding된 Connection이 없습니다."); } return dataSourceAndConnection.get(dataSource); @@ -41,4 +42,16 @@ public static Connection unbindConnection(DataSource dataSource) { resources.remove(); return connection; } + + public static boolean isInTransaction() { + return isInTransaction.get(); + } + + public static void setInTransaction(boolean inTransaction) { + if (inTransaction && isInTransaction()) { + throw new IllegalStateException("이미 트랜잭션이 진행중입니다."); + } + isInTransaction.remove(); + isInTransaction.set(inTransaction); + } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java index ba36414634..9f30f9bd5a 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -1,7 +1,6 @@ package org.springframework.transaction.support; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.DataAccessException; import javax.sql.DataSource; import java.sql.Connection; @@ -16,22 +15,23 @@ public TransactionTemplate(DataSource dataSource) { } public void execute(Runnable runnable) { - Connection connection = DataSourceUtils.getConnection(dataSource); - begin(connection); + Connection connection = begin(); try { runnable.run(); - commit(connection); } catch (Exception e) { rollback(connection); - } finally { - DataSourceUtils.releaseConnection(connection, dataSource); + throw new DataAccessException(e); } } - private void begin(Connection connection) { + private Connection begin() { try { + TransactionSynchronizationManager.setInTransaction(true); + Connection connection = dataSource.getConnection(); connection.setAutoCommit(false); + TransactionSynchronizationManager.bindConnection(dataSource, connection); + return connection; } catch (SQLException e) { throw new DataAccessException(e); } @@ -40,6 +40,7 @@ private void begin(Connection connection) { private void commit(Connection connection) { try { connection.commit(); + release(connection); } catch (SQLException e) { throw new DataAccessException(e); } @@ -48,6 +49,17 @@ private void commit(Connection connection) { private void rollback(Connection connection) { try { connection.rollback(); + release(connection); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private void release(Connection connection) { + try { + connection.close(); + TransactionSynchronizationManager.setInTransaction(false); + TransactionSynchronizationManager.unbindConnection(dataSource); } catch (SQLException e) { throw new DataAccessException(e); } diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 300f1353b5..f316434220 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.dao.DataAccessException; +import org.springframework.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; From 72752e2ed8543900b403bfc197bd7d28069528f0 Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 16:58:18 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=EB=8B=AB=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/TransactionSynchronizationManager.java | 11 +++++++++-- .../transaction/support/TransactionTemplate.java | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index e183d63b10..e1330b83eb 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -32,17 +32,24 @@ public static void bindConnection(DataSource dataSource, Connection connection) resources.get().put(dataSource, connection); } - public static Connection unbindConnection(DataSource dataSource) { + public static Connection unbindConnection(Connection connection, DataSource dataSource) { if (resources.get() == null || !resources.get().containsKey(dataSource)) { throw new IllegalArgumentException("등록된 DataSource가 없습니다."); } Map resource = resources.get(); - Connection connection = resource.get(dataSource); + Connection bindedConnection = resource.get(dataSource); + validateConnection(connection, bindedConnection); resources.remove(); return connection; } + private static void validateConnection(Connection connection, Connection bindedConnection) { + if (connection != bindedConnection) { + throw new IllegalArgumentException("잘못된 Connection입니다."); + } + } + public static boolean isInTransaction() { return isInTransaction.get(); } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java index 9f30f9bd5a..c8865b8dc5 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -57,9 +57,9 @@ private void rollback(Connection connection) { private void release(Connection connection) { try { - connection.close(); TransactionSynchronizationManager.setInTransaction(false); - TransactionSynchronizationManager.unbindConnection(dataSource); + Connection unbindedConnection = TransactionSynchronizationManager.unbindConnection(connection, dataSource); + unbindedConnection.close(); } catch (SQLException e) { throw new DataAccessException(e); } From f5ba5dd6a25e6529a2582c41df99b4358261e7f1 Mon Sep 17 00:00:00 2001 From: ReO Date: Tue, 10 Oct 2023 16:58:35 +0900 Subject: [PATCH 12/12] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index f316434220..9600c91b0c 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -47,8 +47,6 @@ public void setup() throws Exception { verify(this.preparedStatement).setObject(1, "reo"); verify(this.preparedStatement).setObject(2, "1234"); verify(this.preparedStatement).setObject(3, "reo@woowahan.com"); - verify(this.preparedStatement).close(); - verify(this.connection).close(); } @Test @@ -61,8 +59,6 @@ public void setup() throws Exception { assertThatExceptionOfType(DataAccessException.class) .isThrownBy(() -> this.template.update(sql)) .withCause(sqlException); - verify(this.preparedStatement).close(); - verify(this.connection, atLeastOnce()).close(); } @Test @@ -73,9 +69,6 @@ public void setup() throws Exception { }); given(this.preparedStatement.executeQuery()).willReturn(this.resultSet); - - verify(this.preparedStatement).close(); - verify(this.connection).close(); } @Test @@ -90,7 +83,5 @@ public void setup() throws Exception { return ""; })) .withCause(sqlException); - verify(this.preparedStatement).close(); - verify(this.connection, atLeastOnce()).close(); } }