diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..9a825583f9 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,62 +1,30 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import org.springframework.jdbc.core.JdbcTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.time.LocalDateTime; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; +import org.springframework.jdbc.core.JdbcTemplate; public class UserHistoryDao { - private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); - - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; public UserHistoryDao(final DataSource dataSource) { - this.dataSource = dataSource; + this.jdbcTemplate = new JdbcTemplate(dataSource); } public UserHistoryDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.jdbcTemplate = jdbcTemplate; } public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; - - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setLong(1, userHistory.getUserId()); - pstmt.setString(2, userHistory.getAccount()); - pstmt.setString(3, userHistory.getPassword()); - pstmt.setString(4, userHistory.getEmail()); - pstmt.setObject(5, userHistory.getCreatedAt()); - pstmt.setString(6, userHistory.getCreateBy()); - pstmt.executeUpdate(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) {} - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) {} - } + final long userId = userHistory.getUserId(); + final String account = userHistory.getAccount(); + final String password = userHistory.getPassword(); + final String email = userHistory.getEmail(); + final LocalDateTime createdAt = userHistory.getCreatedAt(); + final String createBy = userHistory.getCreateBy(); + jdbcTemplate.update(sql, userId, account, password, email, createdAt, createBy); } } 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..a8d453b094 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,35 @@ +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(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + final var 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/TransactionService.java b/app/src/main/java/com/techcourse/service/TransactionService.java new file mode 100644 index 0000000000..ef069be166 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TransactionService.java @@ -0,0 +1,10 @@ +package com.techcourse.service; + +public abstract class TransactionService { + + protected T appService; + + protected TransactionService(final T appService) { + this.appService = appService; + } +} diff --git a/app/src/main/java/com/techcourse/service/TransactionUserService.java b/app/src/main/java/com/techcourse/service/TransactionUserService.java new file mode 100644 index 0000000000..09e6d6c717 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TransactionUserService.java @@ -0,0 +1,33 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import java.sql.Connection; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionTemplate; + +public class TransactionUserService extends TransactionService implements UserService { + + public TransactionUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + super(new AppUserService(userDao, userHistoryDao)); + } + + public User findById(final long id) { + final Connection connection = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); + final User user = appService.findById(id); + DataSourceUtils.releaseConnection(connection, DataSourceConfig.getInstance()); + return user; + } + + public void insert(final User user) { + TransactionTemplate.executeWithoutReturn(() -> appService.insert(user), DataSourceConfig.getInstance()); + } + + public void changePassword(final long id, final String newPassword, final String createBy) { + TransactionTemplate.executeWithoutReturn( + () -> appService.changePassword(id, newPassword, createBy), DataSourceConfig.getInstance() + ); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fcf2159dc8..b14dbcacbf 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(final long id); - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } + void insert(final User user); - public User findById(final long id) { - return userDao.findById(id); - } - - public void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) { - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); - } + void changePassword(final long id, final String newPassword, final String createBy); } diff --git a/app/src/test/java/com/techcourse/dao/SqlRuntimeExceptionTest.java b/app/src/test/java/com/techcourse/dao/SqlRuntimeExceptionTest.java index bf57ccb40a..87b2121ab7 100644 --- a/app/src/test/java/com/techcourse/dao/SqlRuntimeExceptionTest.java +++ b/app/src/test/java/com/techcourse/dao/SqlRuntimeExceptionTest.java @@ -12,11 +12,11 @@ import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplateBase; -import org.springframework.jdbc.core.error.TableNotFoundSqlRuntimeException; import org.springframework.jdbc.core.error.exception.ColumnSqlRuntimeException; import org.springframework.jdbc.core.error.exception.DataConversionSqlRuntimeException; import org.springframework.jdbc.core.error.exception.MethodNotAllowedSqlRuntimeException; import org.springframework.jdbc.core.error.exception.SyntaxSqlRuntimeException; +import org.springframework.jdbc.core.error.exception.TableNotFoundSqlRuntimeException; import org.springframework.jdbc.core.mapper.ResultSetObjectMapper; class SqlRuntimeExceptionTest { @@ -61,7 +61,7 @@ void executeQuery_insert() { //when //then assertThatThrownBy( - () -> jdbcTemplateExecutionBase.executionBaseWithNonReturn(sql, PreparedStatement::executeQuery, true) + () -> jdbcTemplateExecutionBase.executionBaseWithNonReturn(sql, PreparedStatement::executeQuery) ).isInstanceOf(MethodNotAllowedSqlRuntimeException.ExecuteQuerySqlRuntimeException.class); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..5eca722e30 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -1,20 +1,18 @@ package com.techcourse.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; 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.dao.DataAccessException; +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 { private JdbcTemplate jdbcTemplate; @@ -33,7 +31,7 @@ void setUp() { @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new TransactionUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -48,13 +46,13 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new TransactionUserService(userDao, userHistoryDao); final var newPassword = "newPassword"; final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); 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 acff0d0a29..7ad9674c55 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,59 +11,30 @@ public JdbcTemplate(final DataSource dataSource) { } public T executeQueryForObject(final String sql, final ResultSetObjectMapper mapper) { - return super.executeQueryForObjectBase(sql, mapper, new Object[]{}, TRANSACTION_ENABLE); + return super.executeQueryForObjectBase(sql, mapper, new Object[]{}); } public T executeQueryForObject(final String sql, final ResultSetObjectMapper mapper, final Object... params) { - return super.executeQueryForObjectBase(sql, mapper, params, TRANSACTION_ENABLE); - } - - public T executeQueryForObjectWithoutTransaction(final String sql, final ResultSetObjectMapper mapper) { - return super.executeQueryForObjectBase(sql, mapper, new Object[]{}, TRANSACTION_ENABLE); - } - - public T executeQueryForObjectWithoutTransaction(final String sql, - final ResultSetObjectMapper mapper, - final Object... params) { - return super.executeQueryForObjectBase(sql, mapper, params, TRANSACTION_DISABLE); + return super.executeQueryForObjectBase(sql, mapper, params); } public List executeQueryForObjects(final String sql, final ResultSetObjectMapper mapper) { - return super.executeQueryForObjectsBase(sql, mapper, new Object[]{}, TRANSACTION_ENABLE); + return super.executeQueryForObjectsBase(sql, mapper, new Object[]{}); } public List executeQueryForObjects(final String sql, final ResultSetObjectMapper mapper, final Object... params) { - return super.executeQueryForObjectsBase(sql, mapper, params, TRANSACTION_ENABLE); - } - - public List executeQueryForObjectsWithoutTransaction(final String sql, - final ResultSetObjectMapper mapper) { - return super.executeQueryForObjectsBase(sql, mapper, new Object[]{}, TRANSACTION_DISABLE); - } - - public List executeQueryForObjectsWithoutTransaction(final String sql, - final ResultSetObjectMapper mapper, - final Object... params) { - return super.executeQueryForObjectsBase(sql, mapper, params, TRANSACTION_DISABLE); + return super.executeQueryForObjectsBase(sql, mapper, params); } public void update(final String sql) { - updateBase(sql, new Object[]{}, TRANSACTION_ENABLE); + updateBase(sql, new Object[]{}); } public void update(final String sql, Object... params) { - updateBase(sql, params, TRANSACTION_ENABLE); - } - - public void updateWithoutTransaction(final String sql) { - updateBase(sql, new Object[]{}, TRANSACTION_DISABLE); - } - - public void updateWithoutTransaction(final String sql, Object... params) { - updateBase(sql, params, TRANSACTION_DISABLE); + updateBase(sql, params); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateBase.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateBase.java index 2e174702b3..b2eea495f7 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateBase.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateBase.java @@ -7,48 +7,32 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.error.SqlExceptionConverter; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplateBase { private static final Logger log = LoggerFactory.getLogger(JdbcTemplateBase.class); - protected static final boolean TRANSACTION_ENABLE = false; - protected static final boolean TRANSACTION_DISABLE = true; private final DataSource dataSource; public JdbcTemplateBase(final DataSource dataSource) { this.dataSource = dataSource; } - public void executionBaseWithNonReturn(final String sql, - final JdbcTemplateVoidExecution execution, - final boolean enableTransaction) { - try ( - final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { - connection.setAutoCommit(!enableTransaction); + public void executionBaseWithNonReturn(final String sql, final JdbcTemplateVoidExecution execution) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { log.debug("query : {}", sql); execution.execute(preparedStatement); - if (connection.getAutoCommit() == TRANSACTION_ENABLE) { - connection.commit(); - } } catch (SQLException e) { throw SqlExceptionConverter.convert(e); } } - public T executionBaseWithReturn(final String sql, - final JdbcTemplateExecutor execution, - final boolean enableTransaction) { - - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { - connection.setAutoCommit(!enableTransaction); + public T executionBaseWithReturn(final String sql, final JdbcTemplateExecutor execution) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { log.debug("query : {}", sql); - final T result = execution.execute(preparedStatement); - if (connection.getAutoCommit() == TRANSACTION_ENABLE) { - connection.commit(); - } - return result; + return execution.execute(preparedStatement); } catch (SQLException e) { throw SqlExceptionConverter.convert(e); } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateExecutionBase.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateExecutionBase.java index b1ff77e751..f0211446bd 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateExecutionBase.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplateExecutionBase.java @@ -16,8 +16,7 @@ public JdbcTemplateExecutionBase(final DataSource dataSource) { protected T executeQueryForObjectBase(final String sql, final ResultSetObjectMapper mapper, - final Object[] params, - final boolean isTransactionEnable) { + final Object[] params) { final JdbcTemplateExecutor execution = preparedStatement -> { setParameters(params, preparedStatement); try (final ResultSet resultSet = preparedStatement.executeQuery()) { @@ -27,13 +26,12 @@ protected T executeQueryForObjectBase(final String sql, return null; } }; - return super.executionBaseWithReturn(sql, execution, isTransactionEnable); + return super.executionBaseWithReturn(sql, execution); } protected List executeQueryForObjectsBase(final String sql, final ResultSetObjectMapper mapper, - final Object[] params, - final boolean isTransactionEnable) { + final Object[] params) { final JdbcTemplateExecutor> execution = preparedStatement -> { setParameters(params, preparedStatement); try (final ResultSet resultSet = preparedStatement.executeQuery()) { @@ -44,17 +42,15 @@ protected List executeQueryForObjectsBase(final String sql, return objects; } }; - return super.executionBaseWithReturn(sql, execution, isTransactionEnable); + return super.executionBaseWithReturn(sql, execution); } - protected void updateBase(final String sql, - final Object[] params, - final boolean isTransactionEnable) { + protected void updateBase(final String sql, final Object[] params) { final JdbcTemplateVoidExecution execution = preparedStatement -> { setParameters(params, preparedStatement); preparedStatement.executeUpdate(); }; - super.executionBaseWithNonReturn(sql, execution, isTransactionEnable); + super.executionBaseWithNonReturn(sql, execution); } private void setParameters(final Object[] params, final PreparedStatement preparedStatement) throws SQLException { diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/error/SqlExceptionType.java b/jdbc/src/main/java/org/springframework/jdbc/core/error/SqlExceptionType.java index 05cd411c28..96bfcb7d5b 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/error/SqlExceptionType.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/error/SqlExceptionType.java @@ -8,6 +8,7 @@ import org.springframework.jdbc.core.error.exception.MethodNotAllowedSqlRuntimeException.ExecuteUpdateSqlRuntimeException; import org.springframework.jdbc.core.error.exception.SqlRuntimeException; import org.springframework.jdbc.core.error.exception.SyntaxSqlRuntimeException; +import org.springframework.jdbc.core.error.exception.TableNotFoundSqlRuntimeException; public enum SqlExceptionType { METHOD_NOT_ALLOWED_EXECUTE_UPDATE(90001, ExecuteUpdateSqlRuntimeException::new), diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/error/TableNotFoundSqlRuntimeException.java b/jdbc/src/main/java/org/springframework/jdbc/core/error/exception/TableNotFoundSqlRuntimeException.java similarity index 64% rename from jdbc/src/main/java/org/springframework/jdbc/core/error/TableNotFoundSqlRuntimeException.java rename to jdbc/src/main/java/org/springframework/jdbc/core/error/exception/TableNotFoundSqlRuntimeException.java index f0481f9a36..6cde3d0535 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/error/TableNotFoundSqlRuntimeException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/error/exception/TableNotFoundSqlRuntimeException.java @@ -1,7 +1,6 @@ -package org.springframework.jdbc.core.error; +package org.springframework.jdbc.core.error.exception; import java.sql.SQLException; -import org.springframework.jdbc.core.error.exception.SqlRuntimeException; public class TableNotFoundSqlRuntimeException extends SqlRuntimeException { 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..10d001ed6d 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -1,16 +1,16 @@ package org.springframework.jdbc.datasource; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import javax.sql.DataSource; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.transaction.support.TransactionSynchronizationManager; // 4단계 미션에서 사용할 것 public abstract class DataSourceUtils { - private DataSourceUtils() {} + private DataSourceUtils() { + } public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { Connection connection = TransactionSynchronizationManager.getResource(dataSource); @@ -30,6 +30,10 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { connection.close(); + final Connection unbindConnection = TransactionSynchronizationManager.unbindResource(dataSource); + if (!connection.equals(unbindConnection) && !unbindConnection.isClosed()) { + unbindConnection.close(); + } } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } 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..0ed8cb6922 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,38 @@ package org.springframework.transaction.support; -import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import javax.sql.DataSource; public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = new ThreadLocal<>(); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { + final Map connections = resources.get(); + if (Objects.nonNull(connections) && connections.containsKey(key)) { + return connections.get(key); + } return null; } public static void bindResource(DataSource key, Connection value) { + if (Objects.isNull(resources.get())) { + resources.set(new HashMap<>()); + } + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + final Connection removedConnection = resources.get().remove(key); + if (resources.get().isEmpty()) { + resources.remove(); + } + return removedConnection; } } 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..bdfdc4ebb8 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,48 @@ +package org.springframework.transaction.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Supplier; +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.error.SqlExceptionConverter; +import org.springframework.jdbc.datasource.DataSourceUtils; + +public class TransactionTemplate { + + private TransactionTemplate() { + } + + public static void executeWithoutReturn(final Runnable transactionCallback, final DataSource dataSource) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + transactionCallback.run(); + connection.commit(); + } catch (SQLException e) { + rollback(connection); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + public static T ExecuteWithReturn(final Supplier transactionCallback, final DataSource dataSource) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + return transactionCallback.get(); + } catch (SQLException e) { + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + private static void rollback(final Connection connection) { + try { + connection.rollback(); + } catch (SQLException e) { + throw SqlExceptionConverter.convert(e); + } + } +} diff --git a/study/src/main/java/aop/service/UserService.java b/study/src/main/java/aop/service/UserService.java index 8ce24e21b4..85429cc4dd 100644 --- a/study/src/main/java/aop/service/UserService.java +++ b/study/src/main/java/aop/service/UserService.java @@ -1,11 +1,11 @@ package aop.service; - import aop.domain.User; public interface UserService { User findById(final long id); + void insert(final User user); void changePassword(final long id, final String newPassword, final String createBy); diff --git a/study/src/test/java/aop/stage0/Stage0Test.java b/study/src/test/java/aop/stage0/Stage0Test.java index 079cc6b5a0..4dbfcbcd48 100644 --- a/study/src/test/java/aop/stage0/Stage0Test.java +++ b/study/src/test/java/aop/stage0/Stage0Test.java @@ -1,5 +1,8 @@ package aop.stage0; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + import aop.DataAccessException; import aop.StubUserHistoryDao; import aop.domain.User; @@ -7,18 +10,16 @@ import aop.repository.UserHistoryDao; import aop.service.AppUserService; import aop.service.UserService; +import java.lang.reflect.Proxy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.transaction.PlatformTransactionManager; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; - @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class Stage0Test { @@ -45,7 +46,11 @@ void setUp() { @Test void testChangePassword() { final var appUserService = new AppUserService(userDao, userHistoryDao); - final UserService userService = null; + final UserService userService = (UserService) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{UserService.class}, + new TransactionHandler(platformTransactionManager, appUserService) + ); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -59,12 +64,16 @@ void testChangePassword() { @Test void testTransactionRollback() { final var appUserService = new AppUserService(userDao, stubUserHistoryDao); - final UserService userService = null; + final UserService userService = (UserService) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{UserService.class}, + new TransactionHandler(platformTransactionManager, appUserService) + ); final var newPassword = "newPassword"; final var createBy = "gugu"; assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); diff --git a/study/src/test/java/aop/stage0/TransactionHandler.java b/study/src/test/java/aop/stage0/TransactionHandler.java index 2aa8445aba..1e190cf5f8 100644 --- a/study/src/test/java/aop/stage0/TransactionHandler.java +++ b/study/src/test/java/aop/stage0/TransactionHandler.java @@ -1,15 +1,37 @@ package aop.stage0; +import aop.DataAccessException; +import aop.service.AppUserService; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; public class TransactionHandler implements InvocationHandler { /** * @Transactional 어노테이션이 존재하는 메서드만 트랜잭션 기능을 적용하도록 만들어보자. */ + private final PlatformTransactionManager platformTransactionManager; + + private final AppUserService appUserService; + + public TransactionHandler(final PlatformTransactionManager platformTransactionManager, + final AppUserService appUserService) { + this.platformTransactionManager = platformTransactionManager; + this.appUserService = appUserService; + } + @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { - return null; + final TransactionStatus transaction = platformTransactionManager.getTransaction(null); + try { + final Object result = method.invoke(appUserService, args); + platformTransactionManager.commit(transaction); + return result; + } catch (Exception e) { + platformTransactionManager.rollback(transaction); + throw new DataAccessException(e); + } } }