From 6a5bbffbc7fa852923c0b864987823699ce01f09 Mon Sep 17 00:00:00 2001 From: hum02 Date: Thu, 12 Oct 2023 17:34:10 +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-=203,4?= =?UTF-8?q?=EB=8B=A8=EA=B3=84]=20=EB=8F=84=EC=B9=98(=EA=B9=80=EB=8F=99?= =?UTF-8?q?=ED=9D=A0)=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=20(#529)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:비밀번호 변경과 로깅에 대한 트랜잭션 구현 * feat:Transaction synchronization 적용 * refactor:dataSourceUtils로 conection받아 트랜잭션 처리하도록 수정 * refactor:TxUserService로 트랜잭션 로직 분리 * feat:deleteAll과 insert한 터플id 리턴하는 메서드 구현 * refactor:중복된 커넥션 close삭제 * feat:유저의 findById,insert메서드에 트랜잭션 적용 * refactor(TransactionSynchronizationManager):key에 맞는 값 없을 시 null반환함을 명시하도록 수정 * refactor:unbind시 connection close확인 후 unbind하도록 수정 --- .../main/java/com/techcourse/dao/UserDao.java | 14 ++++-- .../com/techcourse/dao/UserHistoryDao.java | 49 ++++-------------- .../techcourse/service/AppUserService.java | 36 +++++++++++++ .../com/techcourse/service/TxUserService.java | 32 ++++++++++++ .../com/techcourse/service/UserService.java | 29 ++--------- app/src/main/resources/schema.sql | 2 +- .../java/com/techcourse/dao/UserDaoTest.java | 21 ++++---- .../techcourse/service/UserServiceTest.java | 29 ++++++----- .../jdbc/core/JdbcTemplate.java | 32 ++++++++++-- .../jdbc/core/StatementCreator.java | 8 +++ .../support/ConnectionManager.java | 32 ++++++++++++ .../support/TransactionHandler.java | 50 +++++++++++++++++++ .../TransactionSynchronizationManager.java | 33 ++++++++++-- 13 files changed, 267 insertions(+), 100 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/ConnectionManager.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionHandler.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 2d105f3a5f..75754aee0c 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -24,10 +24,10 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(final User user) { + public long insert(final User user) { final String sql = "insert into users (account, password, email) values (?, ?, ?)"; - jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + return jdbcTemplate.updateAndReturnKey(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(final User user) { @@ -36,6 +36,12 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } + public void deleteAll() { + final String sql = "delete from users"; + + jdbcTemplate.update(sql); + } + public List findAll() { final String sql = "select id, account, password, email from users"; @@ -57,9 +63,9 @@ public Optional findById(final Long id) { return jdbcTemplate.queryForObject(sql, rowMapper, id); } - public List findByAccount(final String account) { + public Optional findByAccount(final String account) { final String sql = "select id, account, password, email from users where users.account = ?"; - return jdbcTemplate.query(sql, rowMapper, account); + return jdbcTemplate.queryForObject(sql, rowMapper, account); } } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..98c0e64c0b 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,62 +1,31 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; public UserHistoryDao(final DataSource dataSource) { - this.dataSource = dataSource; + this.jdbcTemplate = new JdbcTemplate(dataSource); } public UserHistoryDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.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 (?, ?, ?, ?, ?, ?)"; - - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setLong(1, userHistory.getUserId()); - pstmt.setString(2, userHistory.getAccount()); - pstmt.setString(3, userHistory.getPassword()); - pstmt.setString(4, userHistory.getEmail()); - pstmt.setObject(5, userHistory.getCreatedAt()); - pstmt.setString(6, userHistory.getCreateBy()); - pstmt.executeUpdate(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) {} - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) {} - } + 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()); } } 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..ea12997be5 --- /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(UserDao userDao, UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 아이디의 회원이 존재하지 않습니다.")); + } + + @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..dc5db9a4ac --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,32 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionHandler; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionHandler transactionHandler; + + public TxUserService(final UserService userService) { + this.userService = userService; + this.transactionHandler = new TransactionHandler(DataSourceConfig.getInstance()); + } + + @Override + public User findById(long id) { + return transactionHandler.handle(() -> userService.findById(id)); + } + + @Override + public void insert(User user) { + transactionHandler.handle(() -> userService.insert(user)); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionHandler.handle(() -> 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 924e71ba5d..b14dbcacbf 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,33 +1,12 @@ 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 UserService { +public interface UserService { - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; + User findById(final long id); - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } + void insert(final User user); - public User findById(final long id) { - return userDao.findById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 아이디의 회원이 존재하지 않습니다.")); - } - - public void insert(final User user) { - userDao.insert(user); - } - - 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)); - } + void changePassword(final long id, final String newPassword, final String createBy); } diff --git a/app/src/main/resources/schema.sql b/app/src/main/resources/schema.sql index ba235931f1..25c94108ad 100644 --- a/app/src/main/resources/schema.sql +++ b/app/src/main/resources/schema.sql @@ -1,6 +1,6 @@ create table if not exists users ( id bigint auto_increment, - account varchar(100) not null, + account varchar(100) not null unique, password varchar(100) not null, email varchar(100) not null, primary key(id) diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index b74a42a6b8..1f3c7035de 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -6,21 +6,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; class UserDaoTest { private UserDao userDao; + private long userId; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - userDao = new UserDao(DataSourceConfig.getInstance()); + userDao.deleteAll(); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + userId = userDao.insert(user); } @Test @@ -32,7 +31,7 @@ void findAll() { @Test void findById() { - final var user = userDao.findById(1L).get(); + final var user = userDao.findById(userId).get(); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -40,18 +39,18 @@ void findById() { @Test void findByAccount() { final var account = "gugu"; - List users = userDao.findByAccount(account); + User user = userDao.findByAccount(account).get(); - assertThat(users).extracting("account").containsOnly("gugu"); + assertThat(user.getAccount()).isEqualTo("gugu"); } @Test void insert() { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); - userDao.insert(user); + long insertedId = userDao.insert(user); - final var actual = userDao.findById(2L).get(); + final var actual = userDao.findById(insertedId).get(); assertThat(actual.getAccount()).isEqualTo(account); assertThat(actual.getEmail()).isEqualTo("hkkang@woowahan.com"); @@ -60,12 +59,12 @@ void insert() { @Test void update() { final var newPassword = "password99"; - final var user = userDao.findById(1L).get(); + final var user = userDao.findById(userId).get(); user.changePassword(newPassword); userDao.update(user); - final var actual = userDao.findById(1L).get(); + final var actual = userDao.findById(userId).get(); assertThat(actual.getPassword()).isEqualTo(newPassword); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..22e2380c36 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,41 +5,43 @@ 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.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -@Disabled class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; + private long userId; @BeforeEach void setUp() { - this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + final var dataSource = DataSourceConfig.getInstance(); + this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + userDao.deleteAll(); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + userId = userDao.insert(user); } @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new TxUserService(new AppUserService(userDao, userHistoryDao)); final var newPassword = "qqqqq"; final var createBy = "gugu"; - userService.changePassword(1L, newPassword, createBy); - final var actual = userService.findById(1L); + userService.changePassword(userId, newPassword, createBy); + + final var actual = userService.findById(userId); assertThat(actual.getPassword()).isEqualTo(newPassword); } @@ -48,15 +50,18 @@ 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"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + () -> userService.changePassword(userId, newPassword, createBy)); - final var actual = userService.findById(1L); + final var actual = userService.findById(userId); 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 1dd5baf425..92fa5e7809 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -5,12 +5,14 @@ import org.springframework.jdbc.BadGrammarJdbcException; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.JdbcException; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -30,6 +32,16 @@ public void update(String sql, Object... arguments) { queryTemplate(sql, ps -> ((PreparedStatement) ps).executeUpdate(), arguments); } + public long updateAndReturnKey(String sql, Object... arguments) { + try (Statement statement = queryAndReturnStatement(sql, ps -> ((PreparedStatement) ps).executeUpdate(), arguments)) { + ResultSet resultSet = statement.getGeneratedKeys(); + resultSet.next(); + return resultSet.getLong(1); + } catch (Exception e) { + throw new JdbcException("cannot get key", e); + } + } + public List query(String sql, RowMapper rowMapper, Object... arguments) { return queryTemplate(sql, ps -> { ResultSet rs = ((PreparedStatement) ps).executeQuery(); @@ -56,10 +68,8 @@ public Optional queryForObject(String sql, RowMapper rowMapper, Object } private T queryTemplate(String sql, StatementExecutor statementExecutor, Object... arguments) { - try ( - Connection conn = dataSource.getConnection(); - PreparedStatement pstmt = StatementCreator.createStatement(conn, sql, arguments) - ) { + Connection conn = DataSourceUtils.getConnection(dataSource); + try (PreparedStatement pstmt = StatementCreator.createStatement(conn, sql, arguments)) { log.debug("query : {}", sql); return statementExecutor.execute(pstmt); @@ -69,6 +79,20 @@ private T queryTemplate(String sql, StatementExecutor statementExecutor, } } + private Statement queryAndReturnStatement(String sql, StatementExecutor statementExecutor, Object... arguments) { + Connection conn = DataSourceUtils.getConnection(dataSource); + try { + PreparedStatement pstmt = StatementCreator.createStatementWithKey(conn, sql, arguments); + log.debug("query : {}", sql); + + statementExecutor.execute(pstmt); + return pstmt; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw SqlExceptionTranslator.translateException(e); + } + } + private static class SqlExceptionTranslator { private static final Set BAD_SQL_GRAMMAR_CODES = Set.of( "07", // Dynamic SQL error diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/StatementCreator.java b/jdbc/src/main/java/org/springframework/jdbc/core/StatementCreator.java index 33832e864e..b3205dd993 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/StatementCreator.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/StatementCreator.java @@ -4,6 +4,8 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import static java.sql.Statement.RETURN_GENERATED_KEYS; + public class StatementCreator { private StatementCreator() { @@ -15,6 +17,12 @@ public static PreparedStatement createStatement(Connection connection, String sq return preparedStatement; } + public static PreparedStatement createStatementWithKey(Connection connection, String sql, Object... arguments) throws SQLException { + PreparedStatement preparedStatement = connection.prepareStatement(sql, RETURN_GENERATED_KEYS); + setPreparedStatement(preparedStatement, arguments); + return preparedStatement; + } + private static void setPreparedStatement(PreparedStatement pstmt, Object... arguments) throws SQLException { for (int argumentIdx = 1; argumentIdx < arguments.length + 1; argumentIdx++) { pstmt.setObject(argumentIdx, arguments[argumentIdx - 1]); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/ConnectionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/ConnectionManager.java new file mode 100644 index 0000000000..f2542f8004 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/ConnectionManager.java @@ -0,0 +1,32 @@ +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 abstract class ConnectionManager { + + private ConnectionManager() { + } + + public static void close(DataSource dataSource, Connection connection) { + try { + DataSourceUtils.releaseConnection(connection, dataSource); + TransactionSynchronizationManager.unbindResource(dataSource); + } catch (Exception closeException) { + throw new DataAccessException("failed to close connection", closeException); + } + } + + public static void rollback(Exception e, Connection connection) { + try { + connection.rollback(); + } catch (SQLException rollbackException) { + throw new DataAccessException("failed to rollback transaction", rollbackException); + } + throw new DataAccessException(e); + } +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionHandler.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionHandler.java new file mode 100644 index 0000000000..b47b09c94a --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionHandler.java @@ -0,0 +1,50 @@ +package org.springframework.transaction.support; + +import org.springframework.jdbc.datasource.DataSourceUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.function.Supplier; + +public class TransactionHandler { + + private final DataSource dataSource; + + public TransactionHandler(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void handle(Runnable runnable) { + Connection connection = null; + try { + connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + + runnable.run(); + + connection.commit(); + } catch (Exception e) { + ConnectionManager.rollback(e, connection); + } finally { + ConnectionManager.close(dataSource, connection); + } + } + + public T handle(Supplier supplier) { + Connection connection = null; + try { + connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + + T result = supplier.get(); + + connection.commit(); + return result; + } catch (Exception e) { + ConnectionManager.rollback(e, connection); + return null; + } finally { + ConnectionManager.close(dataSource, connection); + } + } +} 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..fb628ea4c4 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,23 +1,50 @@ package org.springframework.transaction.support; +import org.springframework.dao.DataAccessException; + 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 TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + if (resources.get() == null) { + return null; + } + return resources.get().getOrDefault(key, null); } public static void bindResource(DataSource key, Connection value) { + if (resources.get() == null) { + try { + Map threadResource = new HashMap<>(); + resources.set(threadResource); + } catch (Exception e) { + throw new DataAccessException("cannot get Connection", e); + } + } + + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + try { + Map resource = resources.get(); + Connection connection = resource.get(key); + if (connection.isClosed()) { + resources.remove(); + return connection; + } + throw new DataAccessException("connection not closed. failed to unbind resource"); + } catch (Exception e) { + throw new DataAccessException("resource unbind failed", e); + } } }