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/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/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..4b6499f9ca --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,34 @@ +package com.techcourse.service; + +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; + private final TransactionTemplate transactionTemplate; + + public TxUserService(UserService userService, TransactionTemplate transactionTemplate) { + this.userService = userService; + this.transactionTemplate = transactionTemplate; + } + + @Override + public User findById(long id) { + AtomicReference user = new AtomicReference<>(); + transactionTemplate.execute(() -> user.set(userService.findById(id))); + return user.get(); + } + + @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/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); } 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/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); } 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..e77314e1ea 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -5,59 +5,41 @@ 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() { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); 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/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 new file mode 100644 index 0000000000..b0cda3f205 --- /dev/null +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -0,0 +1,58 @@ +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.DataAccessException; +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(); + + DatabasePopulatorUtils.execute(dataSource); + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.userDao = new UserDao(jdbcTemplate); + this.transactionTemplate = new TransactionTemplate(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(DataAccessException.class, + () -> userService.changePassword(user.getId(), newPassword, createBy)); + + final var actual = userService.findById(user.getId()); + + assertThat(actual.getPassword()).isNotEqualTo(newPassword); + } +} 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 3c40bfec52..61465e55b8 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -7,31 +7,33 @@ 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); - if (connection != null) { - return connection; + public static Connection getConnection(DataSource dataSource) { + if (TransactionSynchronizationManager.isInTransaction()) { + return TransactionSynchronizationManager.getConnection(dataSource); } - + try { - connection = dataSource.getConnection(); - TransactionSynchronizationManager.bindResource(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"); } } + + 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 715557fc66..e1330b83eb 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,23 +1,64 @@ package org.springframework.transaction.support; +import org.springframework.DataAccessException; + import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal isInTransaction = ThreadLocal.withInitial(() -> false); private TransactionSynchronizationManager() {} - public static Connection getResource(DataSource key) { - return null; + public static Connection getConnection(DataSource dataSource) { + Map dataSourceAndConnection = resources.get(); + if (dataSourceAndConnection == null || dataSourceAndConnection.get(dataSource) == null) { + throw new DataAccessException("DataSource에 binding된 Connection이 없습니다."); + } + + return dataSourceAndConnection.get(dataSource); + } + + 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 unbindConnection(Connection connection, DataSource dataSource) { + if (resources.get() == null || !resources.get().containsKey(dataSource)) { + throw new IllegalArgumentException("등록된 DataSource가 없습니다."); + } + + Map resource = resources.get(); + 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 void bindResource(DataSource key, Connection value) { + public static boolean isInTransaction() { + return isInTransaction.get(); } - public static Connection unbindResource(DataSource key) { - return null; + 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 new file mode 100644 index 0000000000..c8865b8dc5 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,67 @@ +package org.springframework.transaction.support; + +import org.springframework.DataAccessException; + +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 = begin(); + try { + runnable.run(); + commit(connection); + } catch (Exception e) { + rollback(connection); + throw new DataAccessException(e); + } + } + + 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); + } + } + + private void commit(Connection connection) { + try { + connection.commit(); + release(connection); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private void rollback(Connection connection) { + try { + connection.rollback(); + release(connection); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private void release(Connection connection) { + try { + TransactionSynchronizationManager.setInTransaction(false); + Connection unbindedConnection = TransactionSynchronizationManager.unbindConnection(connection, dataSource); + unbindedConnection.close(); + } 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 3740fa7bce..9600c91b0c 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -1,17 +1,10 @@ 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; 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; @@ -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 { @@ -50,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 @@ -64,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 @@ -76,9 +69,6 @@ public void setup() throws Exception { }); given(this.preparedStatement.executeQuery()).willReturn(this.resultSet); - - verify(this.preparedStatement).close(); - verify(this.connection).close(); } @Test @@ -89,9 +79,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/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; 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"); } }