From 50842105beaa256f35cc1b4bcac1fd84f13c590f Mon Sep 17 00:00:00 2001 From: Ohjintaek <116645747+Ohjintaek@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:55:54 +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=EC=8D=AC=EC=83=B7(=EC=98=A4=EC=A7=84?= =?UTF-8?q?=ED=83=9D)=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(#601)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 ++- .../main/java/com/techcourse/dao/UserDao.java | 12 +--- .../com/techcourse/dao/UserHistoryDao.java | 5 +- .../techcourse/service/AppUserService.java | 35 ++++++++++++ .../com/techcourse/service/TxUserService.java | 35 ++++++++++++ .../com/techcourse/service/UserService.java | 36 ++---------- .../service/MockUserHistoryDao.java | 4 +- .../techcourse/service/UserServiceTest.java | 17 ++---- .../jdbc/core/JdbcTemplate.java | 49 +++++------------ .../jdbc/datasource/DataSourceUtils.java | 1 + .../support/JdbcTransactionManager.java | 55 +++++++++++++++++++ .../support/TransactionExecutor.java | 34 +----------- .../support/TransactionManager.java | 14 ++--- .../support/TransactionSupplier.java | 6 ++ .../TransactionSynchronizationManager.java | 18 ++++-- 15 files changed, 190 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 create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/JdbcTransactionManager.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionSupplier.java diff --git a/README.md b/README.md index 286b116e06..3ae6094419 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,9 @@ ## 2단계 - try-with-resources 활용하기 - 예외 처리 더 신경쓰기 - - 람다 활용해서 코드량 줄이기 \ No newline at end of file + - 람다 활용해서 코드량 줄이기 + +## 4단계 + - Dao의 메서드에서 connection 객체를 파라미터로 전달받지 않기 + - Connection을 관리하는 기능을 가지는 객체 구현 + - `UserService`에서 비즈니스 로직과 데이터 액세스 로직 분리하기 \ No newline at end of file diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 9ca450648c..5bd1c2e224 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,15 +1,14 @@ package com.techcourse.dao; import com.techcourse.domain.User; - -import java.sql.Connection; -import java.util.List; -import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import javax.sql.DataSource; +import java.util.List; + public class UserDao { private static final Logger log = LoggerFactory.getLogger(UserDao.class); @@ -34,11 +33,6 @@ public void update(final User user) { jdbcTemplate.execute(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } - public void update(final Connection conn, final User user) { - final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; - jdbcTemplate.execute(conn, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); - } - public List findAll() { final var sql = "select id, account, password, email from users"; return jdbcTemplate.query(sql, rowMapper); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 94f9b7f668..46d8e18753 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -6,7 +6,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; -import java.sql.Connection; public class UserHistoryDao { @@ -22,9 +21,9 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection conn, 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.execute(conn, sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), + jdbcTemplate.execute(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..833c01224c --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,35 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.JdbcTransactionManager; +import org.springframework.transaction.support.TransactionManager; + +import javax.sql.DataSource; + +public class TxUserService implements UserService { + + private final TransactionManager transactionManager = new JdbcTransactionManager(); + private final DataSource dataSource; + private final UserService userService; + + public TxUserService(DataSource dataSource, UserService userService) { + this.dataSource = dataSource; + this.userService = userService; + } + + @Override + public User findById(long id) { + return transactionManager.executeAndReturn(dataSource, () -> userService.findById(id)); + } + + @Override + public void insert(User user) { + transactionManager.execute(dataSource, () -> userService.insert(user)); + } + + @Override + public void changePassword(long id, String newPassword, String createBy) { + transactionManager.execute(dataSource, () -> 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 6b63240a1f..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,38 +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 org.springframework.transaction.support.TransactionExecutor; -import org.springframework.transaction.support.TransactionManager; -public class UserService { +public interface UserService { - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - private final TransactionManager transactionManager = new TransactionExecutor(); - - 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) { - transactionManager.execute(DataSourceConfig.getInstance(), connection -> { - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - }); - } + 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 9937b42bbc..2ee12b195f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -5,8 +5,6 @@ import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; - public class MockUserHistoryDao extends UserHistoryDao { public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { @@ -14,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 68826702d1..e2595c6cd0 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,14 +5,10 @@ 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.transaction.support.TransactionManager; - -import java.sql.Connection; +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; @@ -36,7 +32,7 @@ void setUp() { @Test void testChangePassword() { 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"; @@ -51,7 +47,8 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var appUserService = new AppUserService(userDao, userHistoryDao); + final var userService = new TxUserService(DataSourceConfig.getInstance(), appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; @@ -63,8 +60,4 @@ void testTransactionRollback() { assertThat(actual.getPassword()).isNotEqualTo(newPassword); } - - private Connection getConnection() { - return TransactionManager.getConnection(DataSourceConfig.getInstance()); - } } 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 dbb93f4b30..2844eacf54 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.exception.IncorrectQueryArgumentException; import org.springframework.jdbc.exception.IncorrectResultSizeDataAccessException; @@ -27,10 +28,6 @@ public int execute(final String sql, final Object... args) { return manageData(PreparedStatement::executeUpdate, sql, args); } - public int execute(final Connection conn, final String sql, final Object... args) { - return manageData(conn, PreparedStatement::executeUpdate, sql, args); - } - public List query(final String sql, final RowMapper rowMapper, final Object... args) { return manageData(pstmt -> { try (ResultSet rs = pstmt.executeQuery()) { @@ -43,18 +40,6 @@ public List query(final String sql, final RowMapper rowMapper, final O }, sql, args); } - public List query(final Connection conn, final String sql, final RowMapper rowMapper, final Object... args) { - return manageData(conn, pstmt -> { - try (ResultSet rs = pstmt.executeQuery()) { - final List result = new ArrayList<>(); - while (rs.next()) { - result.add(rowMapper.mapRow(rs)); - } - return result; - } - }, sql, args); - } - public T queryForObject(final String sql, final RowMapper rowMapper, final Object... args) { return manageData(pstmt -> { try (ResultSet rs = pstmt.executeQuery()) { @@ -63,32 +48,16 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina }, sql, args); } - public T queryForObject(final Connection conn, final String sql, final RowMapper rowMapper, final Object... args) { - return manageData(conn, pstmt -> { - try (ResultSet rs = pstmt.executeQuery()) { - return getOneResult(rs, rowMapper); - } - }, sql, args); - } - private T manageData(final PreparedStatementImpl qm, final String sql, final Object... args) { - try (Connection conn = dataSource.getConnection(); - PreparedStatement pstmt = setPreparedStatement(conn, sql, args)) { - log.debug("query : {}", sql); - return qm.callback(pstmt); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } - } - - private T manageData(final Connection conn, final PreparedStatementImpl qm, final String sql, final Object... args) { + final Connection conn = DataSourceUtils.getConnection(dataSource); try (PreparedStatement pstmt = setPreparedStatement(conn, sql, args)) { log.debug("query : {}", sql); return qm.callback(pstmt); } catch (SQLException e) { log.error(e.getMessage(), e); throw new RuntimeException(e); + } finally { + closeConnectionIfAutoCommitted(conn); } } @@ -126,4 +95,14 @@ private T getOneResult(final ResultSet rs, final RowMapper rowMapper) thr } throw new IncorrectResultSizeDataAccessException("No result to return"); } + + private void closeConnectionIfAutoCommitted(Connection conn) { + try { + if (conn.getAutoCommit()) { + DataSourceUtils.releaseConnection(conn, dataSource); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } 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..e225b38980 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -30,6 +30,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { connection.close(); + TransactionSynchronizationManager.unbindResource(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/JdbcTransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/JdbcTransactionManager.java new file mode 100644 index 0000000000..fd1de9d997 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/JdbcTransactionManager.java @@ -0,0 +1,55 @@ +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 JdbcTransactionManager implements TransactionManager { + + @Override + public void execute(DataSource dataSource, TransactionExecutor method) { + final var connection = TransactionManager.getConnection(dataSource); + try { + connection.setAutoCommit(false); + method.execute(); + connection.commit(); + } catch (RuntimeException | SQLException e) { + handleTransactionException(connection, e); + } finally { + cleanUpTransaction(dataSource, connection); + } + } + + @Override + public T executeAndReturn(DataSource dataSource, TransactionSupplier method) { + final var connection = TransactionManager.getConnection(dataSource); + try { + connection.setAutoCommit(false); + T result = method.get(); + connection.commit(); + return result; + } catch (RuntimeException | SQLException e) { + handleTransactionException(connection, e); + return null; + } finally { + cleanUpTransaction(dataSource, connection); + } + } + + private static void handleTransactionException(Connection connection, Exception e) { + try { + connection.rollback(); + throw new DataAccessException(e); + } catch (SQLException rollbackException) { + throw new RuntimeException(rollbackException); + } + } + + private void cleanUpTransaction(DataSource dataSource, Connection connection) { + DataSourceUtils.releaseConnection(connection, dataSource); + TransactionSynchronizationManager.unbindResource(dataSource); + } +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java index 1109af639f..ae84933bdc 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java @@ -1,34 +1,6 @@ package org.springframework.transaction.support; -import org.springframework.dao.DataAccessException; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.function.Consumer; - -public class TransactionExecutor implements TransactionManager { - - @Override - public void execute(DataSource dataSource, Consumer consumer) { - final var connection = TransactionManager.getConnection(dataSource); - try { - connection.setAutoCommit(false); - consumer.accept(connection); - connection.commit(); - } catch (RuntimeException | SQLException e) { - try { - connection.rollback(); - throw new DataAccessException(e); - } catch (SQLException rollbackException) { - throw new RuntimeException(rollbackException); - } - } finally { - try { - connection.close(); - } catch (SQLException closeException) { - throw new RuntimeException(closeException); - } - } - } +@FunctionalInterface +public interface TransactionExecutor { + void execute(); } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java index a0b596a365..ae74cdabef 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java @@ -1,19 +1,17 @@ package org.springframework.transaction.support; +import org.springframework.jdbc.datasource.DataSourceUtils; + import javax.sql.DataSource; import java.sql.Connection; -import java.sql.SQLException; -import java.util.function.Consumer; public interface TransactionManager { static Connection getConnection(DataSource dataSource) { - try { - return dataSource.getConnection(); - } catch (SQLException e) { - throw new RuntimeException(e); - } + return DataSourceUtils.getConnection(dataSource); } - void execute(DataSource dataSource, Consumer consumer); + void execute(DataSource dataSource, TransactionExecutor method); + + T executeAndReturn(DataSource dataSource, TransactionSupplier method); } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSupplier.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSupplier.java new file mode 100644 index 0000000000..67705e04fd --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSupplier.java @@ -0,0 +1,6 @@ +package org.springframework.transaction.support; + +@FunctionalInterface +public interface TransactionSupplier { + T get(); +} 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..550d691805 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,32 @@ 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(HashMap::new); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + Map connectionMap = resources.get(); + if (!connectionMap.containsKey(key)) { + return null; + } + return connectionMap.get(key); } public static void bindResource(DataSource key, Connection value) { + Map connectionMap = resources.get(); + connectionMap.put(key, value); + resources.set(connectionMap); } public static Connection unbindResource(DataSource key) { - return null; + final Map connectionMap = resources.get(); + return connectionMap.remove(key); } }