From 8657cc1e2eada8e3ab826a75ea8db0cde5080f28 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 01:25:44 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20TransactionSynchronizationManager?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/TransactionSynchronizationManager.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java index deeeb81b57..6e1b8f3798 100644 --- a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java @@ -3,21 +3,23 @@ import javax.sql.DataSource; import java.sql.Connection; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(ConcurrentHashMap::new); 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); } } From be89503d1955ee84c05dfd2fda596dae646f91c9 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 01:25:48 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20Transaction=20synchronization=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 16 ++++++---------- .../java/com/techcourse/dao/UserHistoryDao.java | 16 +++------------- .../java/com/techcourse/service/UserService.java | 10 +++++----- .../techcourse/service/MockUserHistoryDao.java | 11 ++--------- .../jdbc/datasource/DataSourceUtils.java | 1 + 5 files changed, 17 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 8d1b8bb553..bd11cd06fc 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -2,16 +2,16 @@ import com.interface21.jdbc.core.JdbcTemplate; import com.interface21.jdbc.core.RowMapper; +import com.interface21.jdbc.datasource.DataSourceUtils; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.sql.Connection; +import javax.sql.DataSource; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import javax.sql.DataSource; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class UserDao { @@ -35,11 +35,7 @@ public void insert(final User user) { public void update(final User user) { final var sql = "update users set account = ?, password = ?, email = ? where id = ?;"; - jdbcTemplate.update(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 = ?;"; + final var conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); jdbcTemplate.update(conn, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 6d6f64d61d..b8bab4a3c9 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -2,6 +2,8 @@ import com.interface21.jdbc.core.JdbcTemplate; import com.interface21.jdbc.core.RowMapper; +import com.interface21.jdbc.datasource.DataSourceUtils; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,19 +30,7 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { 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(), - userHistory.getEmail(), - userHistory.getCreatedAt(), - userHistory.getCreateBy() - ); - } - - public void log(final Connection conn, final UserHistory userHistory) { - final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; + final var conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); jdbcTemplate.update( conn, sql, diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 89e61b1b64..2bd84e20fb 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,6 +1,7 @@ package com.techcourse.service; import com.interface21.dao.DataAccessException; +import com.interface21.jdbc.datasource.DataSourceUtils; import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; @@ -33,16 +34,15 @@ public void insert(final User user) { } public void changePassword(final long id, final String newPassword, final String createBy) { - Connection conn = null; + Connection conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); try { - conn = DataSourceConfig.getInstance().getConnection(); conn.setAutoCommit(false); final var user = findById(id); user.changePassword(newPassword); - userDao.update(conn, user); - userHistoryDao.log(conn, new UserHistory(user, createBy)); + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); conn.commit(); } catch (Exception e) { @@ -60,10 +60,10 @@ public void changePassword(final long id, final String newPassword, final String if (conn != null) { try { conn.setAutoCommit(true); - conn.close(); } catch (SQLException e) { log.atError().log("Error closing connection", e); } + DataSourceUtils.releaseConnection(conn, DataSourceConfig.getInstance()); } } } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index ffdbc0ccd0..f3467b8bb4 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -1,11 +1,9 @@ package com.techcourse.service; -import com.techcourse.dao.UserHistoryDao; -import com.techcourse.domain.UserHistory; import com.interface21.dao.DataAccessException; import com.interface21.jdbc.core.JdbcTemplate; - -import java.sql.Connection; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.UserHistory; public class MockUserHistoryDao extends UserHistoryDao { @@ -13,11 +11,6 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { super(jdbcTemplate); } - @Override - public void log(final Connection conn, final UserHistory userHistory) { - throw new DataAccessException(); - } - @Override public void log(final UserHistory userHistory) { throw new DataAccessException(); diff --git a/jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java index 18b5f3e5de..44ce4e6c25 100644 --- a/jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/com/interface21/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"); } From 0ebe51168b67ea3c4a96eea0851a9115f9f06b8a Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 14:08:41 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20DAO=EA=B0=80=20DataSourceUtils?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=98=EC=A1=B4=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 5 +-- .../com/techcourse/dao/UserHistoryDao.java | 2 -- .../interface21/jdbc/core/JdbcTemplate.java | 33 ++++--------------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index bd11cd06fc..cdc771bf59 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -2,8 +2,6 @@ import com.interface21.jdbc.core.JdbcTemplate; import com.interface21.jdbc.core.RowMapper; -import com.interface21.jdbc.datasource.DataSourceUtils; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,8 +33,7 @@ public void insert(final User user) { public void update(final User user) { final var sql = "update users set account = ?, password = ?, email = ? where id = ?;"; - final var conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); - jdbcTemplate.update(conn, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } public List findAll() { diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index b8bab4a3c9..05ef0c37c4 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -30,9 +30,7 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; - final var conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); jdbcTemplate.update( - conn, sql, userHistory.getUserId(), userHistory.getAccount(), diff --git a/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java index b83191fde4..fc2266536d 100644 --- a/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java @@ -1,16 +1,16 @@ package com.interface21.jdbc.core; +import com.interface21.dao.DataAccessException; +import com.interface21.jdbc.datasource.DataSourceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import javax.sql.DataSource; - -import com.interface21.dao.DataAccessException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class JdbcTemplate { @@ -26,10 +26,6 @@ public int update(final String sql, final Object... args) { return execute(sql, PreparedStatement::executeUpdate, args); } - public int update(final Connection conn, final String sql, final Object... args) { - return execute(conn, sql, PreparedStatement::executeUpdate, args); - } - public T queryForObject(final String sql, final RowMapper rowMapper, final Object... args) { return execute(sql, pstmt -> { try (final var rs = pstmt.executeQuery()) { @@ -54,22 +50,7 @@ public List queryForList(final String sql, final RowMapper rowMapper, } private T execute(final String sql, final StatementCallback action, final Object... args) { - try (final var conn = dataSource.getConnection(); final var pstmt = conn.prepareStatement(sql)) { - log.debug("query : {}", sql); - if (args != null) { - for (int i = 0; i < args.length; i++) { - pstmt.setObject(i + 1, args[i]); - } - } - return action.doInPreparedStatement(pstmt); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); - } - } - - private T execute(final Connection conn, final String sql, final StatementCallback action, final Object... args) { - try (final var pstmt = conn.prepareStatement(sql)) { + try (final var conn = DataSourceUtils.getConnection(dataSource); final var pstmt = conn.prepareStatement(sql)) { log.debug("query : {}", sql); if (args != null) { for (int i = 0; i < args.length; i++) { From f14febf48293862b9a09dfa80d63ad25c37a9d7c Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 14:09:04 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20TransactionSynchronizationManager?= =?UTF-8?q?=EA=B0=80=20ThreadLocal=20=EC=9E=90=EC=9B=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=9B=84=20=ED=95=B4=EC=A0=9C=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/service/UserService.java | 2 ++ .../support/TransactionSynchronizationManager.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 2bd84e20fb..bacdc11821 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -2,6 +2,7 @@ import com.interface21.dao.DataAccessException; import com.interface21.jdbc.datasource.DataSourceUtils; +import com.interface21.transaction.support.TransactionSynchronizationManager; import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; @@ -64,6 +65,7 @@ public void changePassword(final long id, final String newPassword, final String log.atError().log("Error closing connection", e); } DataSourceUtils.releaseConnection(conn, DataSourceConfig.getInstance()); + TransactionSynchronizationManager.clear(); } } } diff --git a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java index 6e1b8f3798..5d027f9d0e 100644 --- a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java @@ -22,4 +22,8 @@ public static void bindResource(DataSource key, Connection value) { public static Connection unbindResource(DataSource key) { return resources.get().remove(key); } + + public static void clear() { + resources.remove(); + } } From 385ce9b723ee11ed9b8fa1393579d0522916ff19 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 14:22:01 +0900 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20JdbcTemplate=EC=9D=B4=20connection?= =?UTF-8?q?=20=EC=9E=90=EC=9B=90=EC=9D=84=20=EB=B0=98=EB=82=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/interface21/jdbc/core/JdbcTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java index fc2266536d..0a61fd44dc 100644 --- a/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java @@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory; import javax.sql.DataSource; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; @@ -50,7 +49,8 @@ public List queryForList(final String sql, final RowMapper rowMapper, } private T execute(final String sql, final StatementCallback action, final Object... args) { - try (final var conn = DataSourceUtils.getConnection(dataSource); final var pstmt = conn.prepareStatement(sql)) { + final var conn = DataSourceUtils.getConnection(dataSource); + try (final var pstmt = conn.prepareStatement(sql)) { log.debug("query : {}", sql); if (args != null) { for (int i = 0; i < args.length; i++) { From 069d40401d9380ca0ac04d9bd6039945fdae6930 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 15:07:34 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20TransactionTemplate=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/TransactionCallback.java | 8 +++ .../transaction/TransactionTemplate.java | 70 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 jdbc/src/main/java/com/interface21/transaction/TransactionCallback.java create mode 100644 jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionCallback.java b/jdbc/src/main/java/com/interface21/transaction/TransactionCallback.java new file mode 100644 index 0000000000..de8959203e --- /dev/null +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionCallback.java @@ -0,0 +1,8 @@ +package com.interface21.transaction; + +import java.sql.SQLException; + +@FunctionalInterface +public interface TransactionCallback { + T doInTransaction() throws SQLException; +} \ No newline at end of file diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java new file mode 100644 index 0000000000..8f646344f1 --- /dev/null +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java @@ -0,0 +1,70 @@ +package com.interface21.transaction; + +import com.interface21.dao.DataAccessException; +import com.interface21.jdbc.datasource.DataSourceUtils; +import com.interface21.transaction.support.TransactionSynchronizationManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class TransactionTemplate { + + private static final Logger log = LoggerFactory.getLogger(TransactionTemplate.class); + + private final DataSource dataSource; + + public TransactionTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(TransactionCallback callback) { + Connection conn = DataSourceUtils.getConnection(dataSource); + try { + conn.setAutoCommit(false); + + T result = callback.doInTransaction(); + + conn.commit(); + return result; + + } catch (Exception e) { + log.atError().log("Transaction is being rolled back", e); + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + throw new DataAccessException("Rollback failed: " + ex.getMessage(), ex); + } + } + throw new DataAccessException("Transaction failed: " + e.getMessage(), e); + + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + } catch (SQLException e) { + log.atError().log("Error setting autoCommit", e); + } + DataSourceUtils.releaseConnection(conn, dataSource); + TransactionSynchronizationManager.clear(); + } + } + } + + public T executeReadOnly(TransactionCallback callback) { + Connection conn = DataSourceUtils.getConnection(dataSource); + try { + return callback.doInTransaction(); + } catch (SQLException e) { + throw new DataAccessException("Read-only operation failed: " + e.getMessage(), e); + } finally { + if (conn != null) { + DataSourceUtils.releaseConnection(conn, dataSource); + TransactionSynchronizationManager.clear(); + } + } + } +} \ No newline at end of file From ce0f941e5fb4dc528868279f408b002738a03da8 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 15:07:54 +0900 Subject: [PATCH 07/14] =?UTF-8?q?refactor:=20UserService=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techcourse/service/AppUserService.java | 39 +++++++++++ .../com/techcourse/service/TxUserService.java | 37 ++++++++++ .../com/techcourse/service/UserService.java | 68 ++----------------- .../techcourse/service/UserServiceTest.java | 18 +++-- 4 files changed, 91 insertions(+), 71 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 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..143e0ee466 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,39 @@ +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AppUserService implements UserService { + + private static final Logger log = LoggerFactory.getLogger(AppUserService.class); + + 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 save(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..c80a9786a8 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,37 @@ +package com.techcourse.service; + +import com.interface21.transaction.TransactionTemplate; +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionTemplate transactionTemplate; + + public TxUserService(final UserService userService) { + this.userService = userService; + this.transactionTemplate = new TransactionTemplate(DataSourceConfig.getInstance()); + } + + @Override + public User findById(final long id) { + return transactionTemplate.executeReadOnly(() -> userService.findById(id)); + } + + @Override + public void save(final User user) { + transactionTemplate.execute(() -> { + userService.save(user); + return null; + }); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionTemplate.execute(() -> { + userService.changePassword(id, newPassword, createBy); + return null; + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index bacdc11821..81d6aa8a31 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,72 +1,12 @@ package com.techcourse.service; -import com.interface21.dao.DataAccessException; -import com.interface21.jdbc.datasource.DataSourceUtils; -import com.interface21.transaction.support.TransactionSynchronizationManager; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.sql.Connection; -import java.sql.SQLException; +public interface UserService { -public class UserService { + User findById(final long id); - private static final Logger log = LoggerFactory.getLogger(UserService.class); + void save(final User user); - 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) { - Connection conn = DataSourceUtils.getConnection(DataSourceConfig.getInstance()); - try { - conn.setAutoCommit(false); - - final var user = findById(id); - user.changePassword(newPassword); - - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); - - conn.commit(); - } catch (Exception e) { - log.atError().log("Transaction is being rolled back", e); - if (conn != null) { - try { - conn.rollback(); - } catch (SQLException ex) { - throw new DataAccessException("Rollback failed: " + ex.getMessage(), ex); - } - } - throw new DataAccessException("Failed to change password: " + e.getMessage(), e); - - } finally { - if (conn != null) { - try { - conn.setAutoCommit(true); - } catch (SQLException e) { - log.atError().log("Error closing connection", e); - } - DataSourceUtils.releaseConnection(conn, DataSourceConfig.getInstance()); - TransactionSynchronizationManager.clear(); - } - } - } + void changePassword(final long id, final String newPassword, final String createdBy); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 3f77b6504b..2efd11bb0c 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -1,14 +1,13 @@ package com.techcourse.service; +import com.interface21.dao.DataAccessException; +import com.interface21.jdbc.core.JdbcTemplate; import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import com.interface21.dao.DataAccessException; -import com.interface21.jdbc.core.JdbcTemplate; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +31,8 @@ void setUp() { @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var appUserService = new AppUserService(userDao, userHistoryDao); + final var userService = new TxUserService(appUserService); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -47,12 +47,16 @@ 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(appUserService); final var newPassword = "newPassword"; - final var createBy = "gugu"; + final var createdBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - assertThrows(DataAccessException.class, () -> userService.changePassword(1L, newPassword, createBy)); + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createdBy)); final var actual = userService.findById(1L); From 079fb6fac0ef7f0d80a95ad1a87a91f727530918 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 15:25:57 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor:=20TxUserService=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=EC=9D=84=20TransactionUserService=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{TxUserService.java => TransactionUserService.java} | 4 ++-- app/src/test/java/com/techcourse/service/UserServiceTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/src/main/java/com/techcourse/service/{TxUserService.java => TransactionUserService.java} (88%) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TransactionUserService.java similarity index 88% rename from app/src/main/java/com/techcourse/service/TxUserService.java rename to app/src/main/java/com/techcourse/service/TransactionUserService.java index c80a9786a8..0f51d14b94 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TransactionUserService.java @@ -4,12 +4,12 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; -public class TxUserService implements UserService { +public class TransactionUserService implements UserService { private final UserService userService; private final TransactionTemplate transactionTemplate; - public TxUserService(final UserService userService) { + public TransactionUserService(final UserService userService) { this.userService = userService; this.transactionTemplate = new TransactionTemplate(DataSourceConfig.getInstance()); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 2efd11bb0c..778d235356 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -32,7 +32,7 @@ void setUp() { void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); final var appUserService = new AppUserService(userDao, userHistoryDao); - final var userService = new TxUserService(appUserService); + final var userService = new TransactionUserService(appUserService); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -50,7 +50,7 @@ void testTransactionRollback() { // 애플리케이션 서비스 final var appUserService = new AppUserService(userDao, userHistoryDao); // 트랜잭션 서비스 추상화 - final var userService = new TxUserService(appUserService); + final var userService = new TransactionUserService(appUserService); final var newPassword = "newPassword"; final var createdBy = "gugu"; From 76c3eb09ca21db90d1a49bd8656db872a4d29973 Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 15:55:07 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=B9=BC=EB=9F=BC=EB=AA=85=20=ED=98=B8=EC=B6=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserHistoryDao.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 05ef0c37c4..489ab175a5 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -2,14 +2,11 @@ import com.interface21.jdbc.core.JdbcTemplate; import com.interface21.jdbc.core.RowMapper; -import com.interface21.jdbc.datasource.DataSourceUtils; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; -import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -47,11 +44,11 @@ private static class UserHistoryRowMapper implements RowMapper { public UserHistory mapRow(final ResultSet resultSet) throws SQLException { return new UserHistory( resultSet.getLong("id"), - resultSet.getLong("userId"), + resultSet.getLong("user_id"), resultSet.getString("account"), resultSet.getString("password"), resultSet.getString("email"), - resultSet.getString("createBy")); + resultSet.getString("created_by")); } } } From dbc36b26abd81a6576e5993d02f23802679034ce Mon Sep 17 00:00:00 2001 From: goohong Date: Mon, 27 Oct 2025 15:56:53 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20clear=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/interface21/transaction/TransactionTemplate.java | 3 --- .../support/TransactionSynchronizationManager.java | 4 ---- 2 files changed, 7 deletions(-) diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java index 8f646344f1..798f81a8c6 100644 --- a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java @@ -2,7 +2,6 @@ import com.interface21.dao.DataAccessException; import com.interface21.jdbc.datasource.DataSourceUtils; -import com.interface21.transaction.support.TransactionSynchronizationManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +48,6 @@ public T execute(TransactionCallback callback) { log.atError().log("Error setting autoCommit", e); } DataSourceUtils.releaseConnection(conn, dataSource); - TransactionSynchronizationManager.clear(); } } } @@ -63,7 +61,6 @@ public T executeReadOnly(TransactionCallback callback) { } finally { if (conn != null) { DataSourceUtils.releaseConnection(conn, dataSource); - TransactionSynchronizationManager.clear(); } } } diff --git a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java index 5d027f9d0e..6e1b8f3798 100644 --- a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java @@ -22,8 +22,4 @@ public static void bindResource(DataSource key, Connection value) { public static Connection unbindResource(DataSource key) { return resources.get().remove(key); } - - public static void clear() { - resources.remove(); - } } From 048aabe0be02b5db30635dd54d974c99b5fba8e3 Mon Sep 17 00:00:00 2001 From: goohong Date: Wed, 29 Oct 2025 15:07:00 +0900 Subject: [PATCH 11/14] =?UTF-8?q?style:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/techcourse/service/TransactionUserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/techcourse/service/TransactionUserService.java b/app/src/main/java/com/techcourse/service/TransactionUserService.java index 0f51d14b94..e8f2a66242 100644 --- a/app/src/main/java/com/techcourse/service/TransactionUserService.java +++ b/app/src/main/java/com/techcourse/service/TransactionUserService.java @@ -34,4 +34,4 @@ public void changePassword(final long id, final String newPassword, final String return null; }); } -} \ No newline at end of file +} From 41057d7b46e5db1851f3411c62af003feee507f5 Mon Sep 17 00:00:00 2001 From: goohong Date: Wed, 29 Oct 2025 15:11:38 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20TransactionTemplate=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=A0=95=EC=B1=85=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/interface21/transaction/TransactionTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java index 798f81a8c6..3cf79f12ea 100644 --- a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java @@ -56,7 +56,7 @@ public T executeReadOnly(TransactionCallback callback) { Connection conn = DataSourceUtils.getConnection(dataSource); try { return callback.doInTransaction(); - } catch (SQLException e) { + } catch (Exception e) { throw new DataAccessException("Read-only operation failed: " + e.getMessage(), e); } finally { if (conn != null) { From f06df3933494d352f7847797b0f18f3bc4154d0e Mon Sep 17 00:00:00 2001 From: goohong Date: Wed, 29 Oct 2025 15:34:48 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20=EC=BD=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20CQS=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TransactionUserService.java | 10 ++---- .../TransactionCallbackWithoutResult.java | 9 ++++++ .../transaction/TransactionTemplate.java | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 jdbc/src/main/java/com/interface21/transaction/TransactionCallbackWithoutResult.java diff --git a/app/src/main/java/com/techcourse/service/TransactionUserService.java b/app/src/main/java/com/techcourse/service/TransactionUserService.java index e8f2a66242..f1797f509b 100644 --- a/app/src/main/java/com/techcourse/service/TransactionUserService.java +++ b/app/src/main/java/com/techcourse/service/TransactionUserService.java @@ -21,17 +21,11 @@ public User findById(final long id) { @Override public void save(final User user) { - transactionTemplate.execute(() -> { - userService.save(user); - return null; - }); + transactionTemplate.executeWithoutResult(() -> userService.save(user)); } @Override public void changePassword(final long id, final String newPassword, final String createBy) { - transactionTemplate.execute(() -> { - userService.changePassword(id, newPassword, createBy); - return null; - }); + transactionTemplate.executeWithoutResult(() -> userService.changePassword(id, newPassword, createBy)); } } diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionCallbackWithoutResult.java b/jdbc/src/main/java/com/interface21/transaction/TransactionCallbackWithoutResult.java new file mode 100644 index 0000000000..07c3fa867b --- /dev/null +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionCallbackWithoutResult.java @@ -0,0 +1,9 @@ +package com.interface21.transaction; + +import java.sql.SQLException; + +@FunctionalInterface +public interface TransactionCallbackWithoutResult { + void doInTransaction() throws SQLException; +} + diff --git a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java index 3cf79f12ea..aac0794af0 100644 --- a/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java +++ b/jdbc/src/main/java/com/interface21/transaction/TransactionTemplate.java @@ -64,4 +64,36 @@ public T executeReadOnly(TransactionCallback callback) { } } } + + public void executeWithoutResult(TransactionCallbackWithoutResult callback) { + Connection conn = DataSourceUtils.getConnection(dataSource); + try { + conn.setAutoCommit(false); + + callback.doInTransaction(); + + conn.commit(); + + } catch (Exception e) { + log.atError().log("Transaction is being rolled back", e); + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + throw new DataAccessException("Rollback failed: " + ex.getMessage(), ex); + } + } + throw new DataAccessException("Transaction failed: " + e.getMessage(), e); + + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + } catch (SQLException e) { + log.atError().log("Error setting autoCommit", e); + } + DataSourceUtils.releaseConnection(conn, dataSource); + } + } + } } \ No newline at end of file From 4e2c7ba07da219ca785c654f57a4bbc235495ba7 Mon Sep 17 00:00:00 2001 From: goohong Date: Wed, 29 Oct 2025 16:31:15 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20bindResource()=EC=97=90=20?= =?UTF-8?q?=EB=B0=A9=EC=96=B4=EC=A0=81=20=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=98=EB=B0=8D=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/support/TransactionSynchronizationManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java index 6e1b8f3798..ca96dfc35c 100644 --- a/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java @@ -16,7 +16,7 @@ public static Connection getResource(DataSource key) { } public static void bindResource(DataSource key, Connection value) { - resources.get().put(key, value); + resources.get().putIfAbsent(key, value); } public static Connection unbindResource(DataSource key) {