From dffba206bd296d8df68feab8140fe3c00886a0b8 Mon Sep 17 00:00:00 2001 From: Mooooooo Date: Wed, 11 Oct 2023 13:03:59 +0900 Subject: [PATCH] =?UTF-8?q?[JDBC=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84=ED=95=98=EA=B8=B0=20-=204?= =?UTF-8?q?=EB=8B=A8=EA=B3=84]=20=EB=AC=B4=EB=AF=BC(=EB=B0=95=EB=AC=B4?= =?UTF-8?q?=ED=98=84)=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=20(#533)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: generator 삭제 * refactor: UserHistory에도 connection 추가 * docs: READEME 업데이트 * feat: Transaction synchronization 적용 * feat: 트랜잭션 서비스 추상화하기 * refactor: try catch 삭제 * chore: 패키지 이동 * refactor: 생성자 수정 * refactor: executor return 값 생성 * refactor: 필요없는 파라미터 제거 * refactor: null 처리 추가 * refactor: TransactionSynchronizationManager 테스트 --- README.md | 3 + .../main/java/com/techcourse/dao/UserDao.java | 7 --- .../techcourse/service/AppUserService.java | 35 ++++++++++++ .../com/techcourse/service/TxUserService.java | 36 ++++++++++++ .../com/techcourse/service/UserService.java | 50 ++--------------- .../techcourse/service/UserServiceTest.java | 14 +++-- .../jdbc/core/JdbcTemplate.java | 10 ---- .../jdbc/core/PreparedStatementExecutor.java | 32 +++-------- .../jdbc/core/PreparedStatementGenerator.java | 11 ---- .../support/BusinessLogicProcessor.java | 7 +++ .../TransactionSynchronizationManager.java | 23 +++++++- .../support/TransactionalExecutor.java | 41 ++++++++++++++ ...TransactionSynchronizationManagerTest.java | 55 +++++++++++++++++++ 13 files changed, 218 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/AppUserService.java create mode 100644 app/src/main/java/com/techcourse/service/TxUserService.java delete mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementGenerator.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/BusinessLogicProcessor.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionalExecutor.java create mode 100644 jdbc/src/test/java/org/springframework/transaction/support/TransactionSynchronizationManagerTest.java diff --git a/README.md b/README.md index 942e0eeffe..0a21649794 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,6 @@ - [x] 2단계 - 리팩터링 - 중복 제거 - [x] 3단계 - Transaction 적용하기 +- [x] 4단계 - Transaction synchronization 적용하기 + - [x] Transaction synchronization 적용하기 + - [x] 트랜잭션 서비스 추상화하기 diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 8ccd2c6b03..279c0748d0 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -4,7 +4,6 @@ import org.springframework.dao.RowMapper; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; import java.util.List; public class UserDao { @@ -34,12 +33,6 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } - public void update(final Connection connection, final User user) { - final var sql = "update users SET account = ?, password = ?, email = ? where id = ?"; - - jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); - } - public List findAll() { final var sql = "select * from users"; 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/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..ee8c5060b6 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,36 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionalExecutor; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionalExecutor transactionalExecutor; + + public TxUserService(final UserService userService, final TransactionalExecutor transactionalExecutor) { + this.userService = userService; + this.transactionalExecutor = transactionalExecutor; + } + + @Override + public User findById(final long id) { + return userService.findById(id); + } + + @Override + public void insert(final User user) { + userService.insert(user); + } + + // override 대상인 메서드는 userService의 메서드를 그대로 위임(delegate)한다. + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionalExecutor.execute( + () -> { + userService.changePassword(id, newPassword, createBy); + return null; + } + ); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 034924b2c5..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,52 +1,10 @@ package com.techcourse.service; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import org.springframework.dao.DataAccessException; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; +public interface UserService { -public class UserService { - - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - private final DataSource dataSource; - - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao, final DataSource dataSource) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - this.dataSource = dataSource; - } - - 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) { - try (final Connection connection = dataSource.getConnection()) { - try { - connection.setAutoCommit(false); - - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(new UserHistory(user, createBy)); - - connection.commit(); - } catch (Exception e) { - connection.rollback(); - throw new DataAccessException(e); - } - } catch (SQLException e) { - throw new DataAccessException(e); - } - } + User findById(final long id); + void insert(final User user); + void changePassword(final long id, final String newPassword, final String createBy); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 9b19275579..24e8e434f4 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionalExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -31,13 +32,13 @@ void setUp() { @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao, DataSourceConfig.getInstance()); + final var appUserService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; - userService.changePassword(1L, newPassword, createBy); + appUserService.changePassword(1L, newPassword, createBy); - final var actual = userService.findById(1L); + final var actual = appUserService.findById(1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } @@ -46,11 +47,14 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao, DataSourceConfig.getInstance()); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService, + new TransactionalExecutor(DataSourceConfig.getInstance())); final var newPassword = "newPassword"; final var createBy = "gugu"; - // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, () -> userService.changePassword(1L, newPassword, createBy)); 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 411c4bb60d..4358a0c11b 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -5,7 +5,6 @@ import org.springframework.exception.WrongResultSizeException; import javax.sql.DataSource; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -30,15 +29,6 @@ public int update(final String sql, final Object... parameters) { ); } - public int update(final Connection connection, final String sql, final Object... parameters) { - return preparedStatementExecutor.execute( - connection, - PreparedStatement::executeUpdate, - sql, - parameters - ); - } - public List query(final RowMapper rowMapper, final String sql, final Object... parameters) { return preparedStatementExecutor.execute( preparedStatement -> findQueryResults(rowMapper, preparedStatement), diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java index 3e2b9c16bb..16dc83990a 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; @@ -24,22 +25,7 @@ public T execute( final String sql, final Object... parameters ) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = generatePreparedStatement(connection, sql, parameters) - ) { - return preparedStatementProcessor.process(preparedStatement); - } catch (final SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } - } - - public T execute( - final Connection connection, - final PreparedStatementProcessor preparedStatementProcessor, - final String sql, - final Object... parameters - ) { + final Connection connection = DataSourceUtils.getConnection(dataSource); try ( final PreparedStatement preparedStatement = generatePreparedStatement(connection, sql, parameters) ) { @@ -54,16 +40,12 @@ private PreparedStatement generatePreparedStatement( final Connection connection, final String sql, final Object... parameters - ) { - try { - log.debug("query : {}", sql); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); + ) throws SQLException { + log.debug("query : {}", sql); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); - setParameters(preparedStatement, parameters); - return preparedStatement; - } catch (SQLException e) { - throw new DataAccessException(e); - } + setParameters(preparedStatement, parameters); + return preparedStatement; } private void setParameters(final PreparedStatement pstmt, final Object[] parameters) throws SQLException { diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementGenerator.java b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementGenerator.java deleted file mode 100644 index 7be04a4f5f..0000000000 --- a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.springframework.jdbc.core; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -@FunctionalInterface -public interface PreparedStatementGenerator { - - PreparedStatement generate(final Connection connection) throws SQLException; -} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/BusinessLogicProcessor.java b/jdbc/src/main/java/org/springframework/transaction/support/BusinessLogicProcessor.java new file mode 100644 index 0000000000..036b2b079a --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/BusinessLogicProcessor.java @@ -0,0 +1,7 @@ +package org.springframework.transaction.support; + +@FunctionalInterface +public interface BusinessLogicProcessor { + + T process(); +} 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..62149aed74 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -2,6 +2,7 @@ import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { @@ -11,13 +12,31 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() {} public static Connection getResource(DataSource key) { - return null; + if (resources.get() == null) { + return null; + } + if (key == null) { + throw new IllegalArgumentException("Key must not be null"); + } + return resources.get().get(key); } public static void bindResource(DataSource key, Connection value) { + if (value == null) { + throw new IllegalArgumentException("Value must not be null"); + } + if (resources.get() == null) { + resources.set(new HashMap<>()); + } + final Map map = resources.get(); + map.put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + if (resources.get() == null) { + throw new IllegalStateException("resource is Empty"); + } + final Map map = resources.get(); + return map.remove(key); } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionalExecutor.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionalExecutor.java new file mode 100644 index 0000000000..6725fc943c --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionalExecutor.java @@ -0,0 +1,41 @@ +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 TransactionalExecutor { + + private final DataSource dataSource; + + public TransactionalExecutor(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(BusinessLogicProcessor businessLogicProcessor) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + final T result = businessLogicProcessor.process(); + connection.commit(); + return result; + } catch (Exception e) { + rollbackAndThrowException(connection); + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + TransactionSynchronizationManager.unbindResource(dataSource); + } + } + + private void rollbackAndThrowException(final Connection connection) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + } +} diff --git a/jdbc/src/test/java/org/springframework/transaction/support/TransactionSynchronizationManagerTest.java b/jdbc/src/test/java/org/springframework/transaction/support/TransactionSynchronizationManagerTest.java new file mode 100644 index 0000000000..4a5035f6c9 --- /dev/null +++ b/jdbc/src/test/java/org/springframework/transaction/support/TransactionSynchronizationManagerTest.java @@ -0,0 +1,55 @@ +package org.springframework.transaction.support; + +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.config.TestDataSource; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +class TransactionSynchronizationManagerTest { + + @Test + void getResource() throws SQLException { + //given + final DataSource dataSource = TestDataSource.getInstance(); + final Connection expected = dataSource.getConnection(); + TransactionSynchronizationManager.bindResource(dataSource, expected); + + //when + final Connection actual = TransactionSynchronizationManager.getResource(dataSource); + + //then + assertThat(actual).isEqualTo(expected); + } + + @Test + void bindResource() throws SQLException { + //given + final DataSource dataSource = TestDataSource.getInstance(); + + //when + TransactionSynchronizationManager.bindResource(TestDataSource.getInstance(), dataSource.getConnection()); + + //then + assertThat(TransactionSynchronizationManager.getResource(dataSource)).isNotNull(); + + } + + @Test + void unbindResource() throws SQLException { + //given + final DataSource dataSource = TestDataSource.getInstance(); + TransactionSynchronizationManager.bindResource(TestDataSource.getInstance(), dataSource.getConnection()); + assertThat(TransactionSynchronizationManager.getResource(dataSource)).isNotNull(); + + //when + TransactionSynchronizationManager.unbindResource(dataSource); + + //then + final Connection resource = TransactionSynchronizationManager.getResource(dataSource); + assertThat(resource).isNull(); + } +}