diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 829f0fb78a..175b31a73b 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,6 +1,5 @@ package com.techcourse.dao; -import java.sql.Connection; import java.util.List; import java.util.Optional; @@ -42,14 +41,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 String sql = "update users set account = ?, password = ? , email = ? where id = ?"; - - log.debug("query : {}", sql); - - jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); - } - public Optional findById(final Long id) { final String sql = "select id, account, password, email from users where id = ?"; diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 0950a6c548..fa24747688 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,7 +1,5 @@ package com.techcourse.dao; -import java.sql.Connection; - import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,19 +30,4 @@ public void log(final UserHistory userHistory) { log.debug("query : {}", sql); } - 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() - ); - - log.debug("query : {}", 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..e915b0487a --- /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).orElseThrow(); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + final 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..50bc151452 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,31 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.TransactionManager; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionManager transactionManager; + + public TxUserService(final UserService userService, final TransactionManager transactionManager) { + this.userService = userService; + this.transactionManager = transactionManager; + } + + @Override + public User findById(final long id) { + return transactionManager.runForObject(() -> userService.findById(id)); + } + + @Override + public void insert(final User user) { + transactionManager.run(() -> userService.insert(user)); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionManager.run(() -> 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 3585643a9e..f9c5b63f4a 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,60 +1,13 @@ package com.techcourse.service; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Optional; - -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 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 Optional findById(final long id) { - return userDao.findById(id); - } - public void insert(final User user) { - userDao.insert(user); - } +public interface UserService { - public void changePassword(final long id, final String newPassword, final String createBy) { - Connection connection = null; - try { - connection = DataSourceConfig.getInstance().getConnection(); - connection.setAutoCommit(false); + User findById(final long id); - final User user = findById(id).orElseThrow(); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); + void insert(final User user); - connection.commit(); - } catch (final SQLException e) { - try { - connection.rollback(); - } catch (final SQLException rollbackException) { - throw new DataAccessException(rollbackException); - } - throw new DataAccessException(e); - } finally { - try { - connection.close(); - } catch (final SQLException e) { - throw new DataAccessException(e); - } - } - } + void changePassword(final long id, final String newPassword, final 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 0a371ecfa3..f706216774 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 @@ -4,6 +4,8 @@ import org.slf4j.LoggerFactory; import javax.sql.DataSource; +import org.springframework.jdbc.datasource.DataSourceUtils; + import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -15,8 +17,11 @@ public class DatabasePopulatorUtils { private static final Logger log = LoggerFactory.getLogger(DatabasePopulatorUtils.class); + private DatabasePopulatorUtils() { + } + public static void execute(final DataSource dataSource) { - Connection connection = null; + Connection connection = DataSourceUtils.getConnection(dataSource); Statement statement = null; try { final var url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql"); @@ -39,8 +44,7 @@ public static void execute(final DataSource dataSource) { connection.close(); } } catch (SQLException ignored) {} + DataSourceUtils.releaseConnection(connection, dataSource); } } - - private DatabasePopulatorUtils() {} } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 76727ccc5b..1b9edbbc29 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -14,7 +14,7 @@ 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/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index c3b4abd334..171cae3de1 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.TransactionManager; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -16,11 +17,13 @@ class UserServiceTest { private JdbcTemplate jdbcTemplate; + private TransactionManager transactionManager; private UserDao userDao; @BeforeEach void setUp() { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + this.transactionManager = new TransactionManager(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); @@ -31,13 +34,13 @@ void setUp() { @Test void testChangePassword() { final UserHistoryDao userHistoryDao = new UserHistoryDao(jdbcTemplate); - final UserService userService = new UserService(userDao, userHistoryDao); + final UserService userService = new AppUserService(userDao, userHistoryDao); final String newPassword = "qqqqq"; final String createBy = "gugu"; userService.changePassword(1L, newPassword, createBy); - final User actual = userService.findById(1L).orElseThrow(); + final User actual = userService.findById(1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } @@ -45,17 +48,21 @@ void testChangePassword() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final UserHistoryDao userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final UserService userService = new UserService(userDao, userHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final UserService appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final UserService txUserService = new TxUserService(appUserService, transactionManager); - final String newPassword = "newPassword"; - final String createBy = "gugu"; + final var newPassword = "newPassword"; + final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + () -> txUserService.changePassword(1L, newPassword, createBy)); - final User actual = userService.findById(1L).orElseThrow(); + final var actual = txUserService.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 a3915ca8e6..2710e1ff2b 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -23,26 +24,6 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public void update(final Connection connection, final String sql, final Object... elements) { - execute(connection, sql, PreparedStatement::executeUpdate, elements); - } - - private T execute( - final Connection connection, - final String sql, - final PreparedStatementExecutor executor, - final Object... elements - ) { - try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { - setElements(elements, preparedStatement); - - return executor.action(preparedStatement); - } catch (final SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } - } - public void update(final String sql, final Object... elements) { execute(sql, PreparedStatement::executeUpdate, elements); } @@ -52,9 +33,8 @@ private T execute( final PreparedStatementExecutor executor, final Object... elements ) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql) - ) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { setElements(elements, preparedStatement); return executor.action(preparedStatement); 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/TransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/TransactionManager.java new file mode 100644 index 0000000000..b4d7035a4c --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/TransactionManager.java @@ -0,0 +1,60 @@ +package org.springframework.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionExecutor; + +public class TransactionManager { + + private final DataSource dataSource; + + public TransactionManager(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public T runForObject(final TransactionExecutor transactionExecutor) { + return execute(transactionExecutor); + } + + public void run(final Runnable runnable) { + execute(() -> { + runnable.run(); + return null; + }); + } + + private T execute(final TransactionExecutor transactionExecutor) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try { + return runInConnection(transactionExecutor, connection); + } catch (final SQLException e) { + rollBackConnection(connection); + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + private T runInConnection( + final TransactionExecutor transactionExecutor, + final Connection connection + ) throws SQLException { + connection.setAutoCommit(false); + final T action = transactionExecutor.action(); + connection.commit(); + return action; + } + + private void rollBackConnection(final Connection connection) { + try { + connection.rollback(); + } catch (final SQLException e) { + throw new DataAccessException(e); + } + } + +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java new file mode 100644 index 0000000000..5230c4f877 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java @@ -0,0 +1,8 @@ +package org.springframework.transaction.support; + +@FunctionalInterface +public interface TransactionExecutor { + + T action(); + +} 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..2ebab89d24 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -2,22 +2,30 @@ 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> resources = ThreadLocal.withInitial(() -> new HashMap<>()); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + return getResources().get(key); } public static void bindResource(DataSource key, Connection value) { + getResources().put(key, value); + } + + public static void unbindResource(DataSource key) { + getResources().remove(key); } - public static Connection unbindResource(DataSource key) { - return null; + private static Map getResources() { + return resources.get(); } + } diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 09e7fc6acd..079c0da130 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -90,7 +90,7 @@ class queryForObject는 { @Test - void 결과가_존재하지_않으면_Optional을_반환한다() throws Exception { + void 결과가_존재하지_않으면_Optional_empty를_반환한다() throws Exception { // given when(resultSet.next()).thenReturn(false); final String sql = "select * from users where id = ?";