diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 170dc965af..5bcbc81881 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,7 +1,6 @@ package com.techcourse.dao; import com.techcourse.domain.User; -import java.sql.Connection; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -40,16 +39,6 @@ public void update(final User user) { jdbcTemplate.update(sql, account, password, email, id); } - public void update(final Connection connection, final User user) { - final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; - final String account = user.getAccount(); - final String password = user.getPassword(); - final String email = user.getEmail(); - final long id = user.getId(); - - jdbcTemplate.update(connection, sql, account, password, email, id); - } - public List findAll() { final var sql = "select * from users"; diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 7eb35a1f6b..257373f9fc 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,7 +1,6 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import java.sql.Connection; import org.springframework.jdbc.core.JdbcTemplate; public class UserHistoryDao { @@ -12,10 +11,10 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection connection, final UserHistory userHistory) { + 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(connection, sql, + jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy() 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..d217fd3859 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,31 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import org.springframework.transaction.TransactionExecutor; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionExecutor transactionExecutor; + + public TxUserService(final AppUserService appUserService) { + this.userService = appUserService; + this.transactionExecutor = new TransactionExecutor(DataSourceConfig.getInstance()); + } + + @Override + public User findById(final long id) { + return transactionExecutor.execute(() -> userService.findById(id), true); + } + + @Override + public void insert(final User user) { + transactionExecutor.execute(() -> userService.insert(user), false); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionExecutor.execute(() -> userService.changePassword(id, newPassword, createBy), true); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 6e14b5a9b4..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,56 +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 javax.sql.DataSource; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.datasource.DataSourceUtils; -public class UserService { +public interface 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 void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) { - DataSource dataSource = DataSourceConfig.getInstance(); - Connection connection = null; - - try { - connection = dataSource.getConnection(); - connection.setAutoCommit(false); - - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - - connection.commit(); - } catch (SQLException e) { - try { - connection.rollback(); - } catch (SQLException ex) { - throw new DataAccessException(ex); - } - } finally { - DataSourceUtils.releaseConnection(connection, dataSource); - } - } + 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/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 4f6f9aba26..2ee12b195f 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,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 181407480a..5cf8a5b813 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -25,14 +25,44 @@ void setUp() { this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance(), ResourceNames.SCHEMA_RESOURCE_NAME); + DatabasePopulatorUtils.execute(DataSourceConfig.getInstance(), ResourceNames.TRUNCATE_RESOURCE_NAME); + } + + @Test + void testInsert() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + + final var userHistoryDao = new UserHistoryDao(jdbcTemplate); + final var userService = new AppUserService(userDao, userHistoryDao); + + final var actual = userService.findById(1L); + + assertThat(actual).isNotNull(); + } + + @Test + void testFindById() { final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); + + final var userHistoryDao = new UserHistoryDao(jdbcTemplate); + final var userService = new AppUserService(userDao, userHistoryDao); + + final var actual = userService.findById(1L); + + assertThat(actual).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(user); } @Test void testChangePassword() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + 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"; @@ -45,9 +75,15 @@ void testChangePassword() { @Test void testTransactionRollback() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + // Application Service + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 Service 추상화 + final var userService = new TxUserService(appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; diff --git a/app/src/test/resources/truncate.sql b/app/src/test/resources/truncate.sql index e9368db0dc..555a6e1d96 100644 --- a/app/src/test/resources/truncate.sql +++ b/app/src/test/resources/truncate.sql @@ -1 +1 @@ -TRUNCATE TABLE USERS; +TRUNCATE TABLE USERS restart identity; 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 157257f0e7..ffa6d6fb55 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,6 +1,5 @@ package org.springframework.jdbc.core; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; @@ -21,10 +20,6 @@ public void update(final String sql, final Object... args) { preparedStatementExecutor.execute(sql, PreparedStatement::executeUpdate, args); } - public void update(final Connection connection, final String sql, final Object... args) { - preparedStatementExecutor.execute(connection, sql, PreparedStatement::executeUpdate, args); - } - public List query(final String sql, final RowMapper rowMapper, final Object... args) { return preparedStatementExecutor.execute(sql, pstmt -> { ResultSet resultSet = pstmt.executeQuery(); 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 ba4d584087..180b74bcaf 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementExecutor.java @@ -19,21 +19,9 @@ public PreparedStatementExecutor(final DataSource dataSource) { this.dataSource = dataSource; } - public T execute(final Connection connection, final String sql, final PreparedStatementCallback preparedStatementCallback, final Object... args) { - try (final PreparedStatement pstmt = connection.prepareStatement(sql)) { - log.debug("query : {}", sql); - - setPreparedStatementArguments(pstmt, args); - return preparedStatementCallback.doInPreparedStatement(pstmt); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } - } - public T execute(final String sql, final PreparedStatementCallback preparedStatementCallback, final Object... args) { - try (final Connection connection = DataSourceUtils.getConnection(dataSource); - final PreparedStatement pstmt = connection.prepareStatement(sql)) { + Connection connection = DataSourceUtils.getConnection(dataSource); + try (final PreparedStatement pstmt = connection.prepareStatement(sql)) { log.debug("query : {}", sql); setPreparedStatementArguments(pstmt, args); 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..9626b50011 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -1,11 +1,10 @@ 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 { @@ -29,6 +28,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/ServiceCallback.java b/jdbc/src/main/java/org/springframework/transaction/ServiceCallback.java new file mode 100644 index 0000000000..f3027694fd --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/ServiceCallback.java @@ -0,0 +1,7 @@ +package org.springframework.transaction; + +@FunctionalInterface +public interface ServiceCallback { + + T doInAction(); +} diff --git a/jdbc/src/main/java/org/springframework/transaction/ServiceExecutor.java b/jdbc/src/main/java/org/springframework/transaction/ServiceExecutor.java new file mode 100644 index 0000000000..0b21ea50f6 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/ServiceExecutor.java @@ -0,0 +1,7 @@ +package org.springframework.transaction; + +@FunctionalInterface +public interface ServiceExecutor { + + void doInAction(); +} diff --git a/jdbc/src/main/java/org/springframework/transaction/TransactionExecutor.java b/jdbc/src/main/java/org/springframework/transaction/TransactionExecutor.java new file mode 100644 index 0000000000..676adc413d --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/TransactionExecutor.java @@ -0,0 +1,59 @@ +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; + +public class TransactionExecutor { + + private final DataSource dataSource; + + public TransactionExecutor(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public void execute(final ServiceExecutor serviceExecutor, final boolean isReadOnly) { + Connection connection = DataSourceUtils.getConnection(dataSource); + + try { + connection.setAutoCommit(false); + connection.setReadOnly(isReadOnly); + serviceExecutor.doInAction(); + + connection.commit(); + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + public T execute(final ServiceCallback serviceCallback, boolean isReadOnly) { + Connection connection = DataSourceUtils.getConnection(dataSource); + + try { + connection.setAutoCommit(false); + connection.setReadOnly(isReadOnly); + + T result = serviceCallback.doInAction(); + + connection.commit(); + return result; + } catch (SQLException e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + return null; + } +} 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..7d7287f389 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,8 +1,9 @@ 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 { @@ -11,13 +12,39 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() {} public static Connection getResource(DataSource key) { - return null; + Map connectionMap = resources.get(); + if (connectionMap == null) { + return null; + } + return connectionMap.get(key); } public static void bindResource(DataSource key, Connection value) { + Map connectionMap = resources.get(); + if (connectionMap == null) { + connectionMap = new HashMap<>(); + resources.set(connectionMap); + } + + if (connectionMap.containsKey(key)) { + throw new IllegalStateException("fail to bind resource because of already exist bound thread"); + } + + connectionMap.put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + Map connectionMap = resources.get(); + if (connectionMap == null) { + return null; + } + Connection connectionToRemove = connectionMap.remove(key); + if (connectionToRemove == null) { + throw new IllegalStateException("fail to unbind resource because of not found value"); + } + if (connectionMap.isEmpty()) { + resources.remove(); + } + return connectionToRemove; } }