From a39336b1f676c72f28be73611c54e5dd851fde02 Mon Sep 17 00:00:00 2001 From: Kayoung Yoon Date: Thu, 12 Oct 2023 10:02:05 +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=A1=9C=EC=A7=80(=EC=9C=A4=EA=B0=80?= =?UTF-8?q?=EC=98=81)=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(#561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: UserService 인터페이스 * feat: DAO에서 connection 사용 하지 않도록 TransactionSynchronization 적용 * feat: AppUserService와 TxUserService 구현 * fix: TransactionSynchronizationManager 수정 * refactor: TransactionTemplate 적용 * ThreadLocal.withInitial 사용 * docs: 기능 명세 반영 --- README.md | 16 ++++-- .../main/java/com/techcourse/dao/UserDao.java | 10 ---- .../com/techcourse/dao/UserHistoryDao.java | 6 --- .../techcourse/service/AppUserService.java | 36 +++++++++++++ .../com/techcourse/service/TxUserService.java | 38 ++++++++++++++ .../com/techcourse/service/UserService.java | 50 ++----------------- ...rviceTest.java => AppUserServiceTest.java} | 32 +----------- .../service/MockUserHistoryDao.java | 4 +- .../techcourse/service/TxUserServiceTest.java | 49 ++++++++++++++++++ .../jdbc/core/JdbcTemplate.java | 41 +++------------ .../jdbc/datasource/DataSourceUtils.java | 1 + .../support/TransactionExecution.java | 5 ++ .../TransactionSynchronizationManager.java | 14 ++++-- .../support/TransactionTemplate.java | 44 ++++++++++++++++ 14 files changed, 208 insertions(+), 138 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 rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => AppUserServiceTest.java} (54%) create mode 100644 app/src/test/java/com/techcourse/service/TxUserServiceTest.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionExecution.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java diff --git a/README.md b/README.md index 198b0f45fb..f28b7317e8 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,17 @@ ## 3단계 -- [ ] User 비밀번호 변경 기능 - - [ ] 비밀번호 변경 기능을 구현한다. (UserDao.changePassword()) - - [ ] 누가, 언제, 어떤 비밀번호로 바꿨는지 이력을 남겨야한다. - - [ ] changePassword 원자성을 보장한다 +- [x] User 비밀번호 변경 기능 + - [x] 비밀번호 변경 기능을 구현한다. (UserDao.changePassword()) + - [x] 누가, 언제, 어떤 비밀번호로 바꿨는지 이력을 남겨야한다. + - [x] changePassword 원자성을 보장한다 - 트랜잭션을 설정한다. - userDao와 userHistoryDao를 한 트랜잭션으로 묶으려면 동일한 Connection 객체를 사용하도록 변경 + +## 4단계 +- [x] UserService 인터페이스를 만든다. +- [x] DataSourceUtils 를 사용해서 Connection을 가져온다. + - [x] TransactionSynchronizationManager 에 보관된 Connection 객체를 가져온다. +- [x] TransactionSynchronizationManager를 구현한다. + - [x] 트랜잭션을 시작하기 위한 Connection 객체를 보관한다. +- [ ] AppUserService를 사용해도 Connection을 닫을 수 있도록 수정한다. diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index bcae20e661..575b425af7 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -31,11 +31,6 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } - public void update(final Connection connection, final User user) { - final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; - jdbcTemplate.update(connection, sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); - } - public List findAll() { final var sql = "select id, account, password, email from users"; return jdbcTemplate.query(sql, USER_ROW_MAPPER); @@ -46,11 +41,6 @@ public User findById(final Long id) { return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id); } - public User findById(final Connection connection, final Long id) { - final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); - } - public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, account); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 9554b1adf9..38600a75c1 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -12,12 +12,6 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection connection, final UserHistory userHistory) { - final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; - jdbcTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), - userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); - } - public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), 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..3bac27f084 --- /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(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..932ad4d67f --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,38 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; + +import static org.springframework.transaction.support.TransactionTemplate.execute; + +public class TxUserService implements UserService { + + private final UserService userService; + + public TxUserService(final UserService userService) { + this.userService = userService; + } + + @Override + public User findById(final long id) { + return execute(DataSourceConfig.getInstance(), () -> userService.findById(id)); + } + + @Override + public void insert(final User user) { + execute(DataSourceConfig.getInstance(), () -> { + userService.insert(user); + return null; + }); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + final var dataSource = DataSourceConfig.getInstance(); + execute(dataSource, () -> { + 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 83958e086b..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.config.DataSourceConfig; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import java.sql.Connection; -import java.sql.SQLException; -import org.springframework.dao.DataAccessException; -public class UserService { - - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } - - public User findById(final long id) { - return userDao.findById(id); - } - - public User findById(final Connection connection, final long id) { - return userDao.findById(connection, id); - } - - public void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) throws SQLException { - final var connection = DataSourceConfig.getInstance().getConnection(); - try { - connection.setAutoCommit(false); - final var user = findById(connection, id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - - connection.commit(); - } catch (final Exception e) { - connection.rollback(); - throw new DataAccessException(e); - } finally { - connection.close(); - } - } +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/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java similarity index 54% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 934318af84..29c1f89a33 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -8,14 +8,11 @@ import java.sql.SQLException; import org.junit.jupiter.api.BeforeEach; 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.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; -class UserServiceTest { +class AppUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @@ -33,7 +30,7 @@ void setUp() { @Test void testChangePassword() throws SQLException { 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"; @@ -43,29 +40,4 @@ void testChangePassword() throws SQLException { assertThat(actual.getPassword()).isEqualTo(newPassword); } - - @Test - void testTransactionRollback() { - // given - final var mockUserHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, mockUserHistoryDao); - - final var newPassword = "newPassword"; - final var createBy = "gugu"; - - // when - try { - userService.changePassword(1L, newPassword, createBy); - } catch (final Exception e) { - // do nothing - } - - // then - final var actual = userService.findById(1L); - assertAll( - () -> assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)), - () -> 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 4f6f9aba26..c720ab0d58 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,7 +2,6 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import java.sql.Connection; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -13,7 +12,8 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final Connection connection, final UserHistory userHistory) { + public void log(final UserHistory userHistory) { 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..df13c9c0d5 --- /dev/null +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -0,0 +1,49 @@ +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.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TxUserServiceTest { + + private JdbcTemplate jdbcTemplate; + private UserDao userDao; + + @BeforeEach + void setUp() { + this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + this.userDao = new UserDao(jdbcTemplate); + + DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + } + @Test + void testTransactionRollback() { + // 트랜잭션 롤백 테스트를 위해 mock으로 교체 + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService); + + 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/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index da9c4a0beb..bd437f195e 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,6 +11,7 @@ import javax.sql.DataSource; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -33,30 +34,8 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina return results.get(0); } - public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... params) { - List results = query(connection, sql, rowMapper, params); - if (results.size() > 1) { - throw new DataAccessException("too many result. expected 1 but was " + results.size()); - } - if (results.isEmpty()) { - throw new DataAccessException("no result"); - } - return results.get(0); - } - public List query(final String sql, final RowMapper rowMapper, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql); - final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { - log.debug("query : {}", sql); - return mapResults(rowMapper, resultSet); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } - } - - public List query(final Connection conn, final String sql, final RowMapper rowMapper, final Object... parameters) { + final var conn = getConnection(); try (final PreparedStatement preparedStatement = conn.prepareStatement(sql); final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { log.debug("query : {}", sql); @@ -89,8 +68,8 @@ private List mapResults(final RowMapper rowMapper, final ResultSet res } public void update(final String sql, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + final Connection conn = getConnection(); + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { log.debug("query : {}", sql); setParameters(preparedStatement, parameters); @@ -101,16 +80,8 @@ public void update(final String sql, final Object... parameters) { } } - public void update(final Connection conn, final String sql, final Object... parameters) { - try (final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { - log.debug("query : {}", sql); - - setParameters(preparedStatement, parameters); - preparedStatement.executeUpdate(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } + private Connection getConnection() { + return DataSourceUtils.getConnection(dataSource); } } 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..be2f114ac1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -29,6 +29,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { + TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecution.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecution.java new file mode 100644 index 0000000000..adb01f8801 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecution.java @@ -0,0 +1,5 @@ +package org.springframework.transaction.support; + +public interface TransactionExecution { + T execute(); +} 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..556488f929 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,27 @@ package org.springframework.transaction.support; -import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; +import javax.sql.DataSource; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + return resources.get().get(key); } public static void bindResource(DataSource key, Connection value) { + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + return resources.get().remove(key); } + } 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..499774b3e5 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,44 @@ +package org.springframework.transaction.support; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +public class TransactionTemplate { + + public static T execute(DataSource dataSource, TransactionExecution action) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + T result = action.execute(); + connection.commit(); + return result; + } catch (final Exception e) { + rollback(connection); + throw new DataAccessException(e); + } finally { + setAutoCommitFalse(connection); + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + + private static void rollback(final Connection connection) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + } + + private static void setAutoCommitFalse(final Connection connection) { + try { + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + +}