Skip to content

Commit

Permalink
[JDBC 라이브러리 구현하기 - 4단계] 모디(전제희) 미션 제출합니다. (#597)
Browse files Browse the repository at this point in the history
* refactor: JdbcTemplate이 UncheckedException을 외부로 위임하지 않도록 변경

* feat: TxUserService 구현

* refactor: JdbcTemplate의 close connection 부분 메서드 분리

* refactor: JdbcTemplate의 인스턴스화 로직 변경

* refactor: 사용되는 함수형 인터페이스 직접 정의

* refactor: connection 생성 삭제 책임을 DataSourceUtils로 집중
  • Loading branch information
jaehee329 authored Oct 12, 2023
1 parent dac0dd4 commit 707d5c9
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 116 deletions.
32 changes: 32 additions & 0 deletions app/src/main/java/com/techcourse/service/AppUserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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;
}

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) {
User user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
111 changes: 111 additions & 0 deletions app/src/main/java/com/techcourse/service/TxUserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.techcourse.service;

import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
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 TxUserService implements UserService {

private final AppUserService appUserService;
private final DataSource datasource = DataSourceConfig.getInstance();

public TxUserService(AppUserService appUserService) {
this.appUserService = appUserService;
}

@Override
public User findById(long id) {
return execute(() -> appUserService.findById(id));
}

@Override
public void insert(User user) {
execute(() -> appUserService.insert(user));
}

@Override
public void changePassword(long id, String newPassword, String createBy) {
execute(() -> appUserService.changePassword(id, newPassword, createBy));
}

private void execute(ExecutableWithoutReturn executable) {
try (TransactionExecutor executor = TransactionExecutor.initTransaction(datasource)) {
try {
executable.execute();
executor.commit();
} catch (Exception e) {
executor.rollback();
throw new DataAccessException();
}
}
}

private <T> T execute(ExecutableWithReturn<T> executable) {
try (TransactionExecutor executor = TransactionExecutor.initTransaction(datasource)) {
try {
T result = executable.execute();
executor.commit();
return result;
} catch (Exception e) {
executor.rollback();
throw new DataAccessException();
}
}
}

@FunctionalInterface
private interface ExecutableWithoutReturn {
void execute();
}

@FunctionalInterface
private interface ExecutableWithReturn<T> {
T execute();
}

private static class TransactionExecutor implements AutoCloseable {

private final DataSource dataSource;
private final Connection connection;

private TransactionExecutor(DataSource dataSource, Connection connection) {
this.dataSource = dataSource;
this.connection = connection;
}

public static TransactionExecutor initTransaction(DataSource dataSource) {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
connection.setAutoCommit(false);
return new TransactionExecutor(dataSource, connection);
} catch (SQLException e) {
throw new RuntimeException();
}
}

public void commit() {
try {
connection.commit();
} catch (SQLException e) {
throw new RuntimeException();
}
}

public void rollback() {
try {
connection.rollback();
} catch (SQLException e) {
throw new RuntimeException();
}
}

@Override
public void close() {
DataSourceUtils.releaseConnection(dataSource);
}
}
}
59 changes: 4 additions & 55 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,61 +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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class UserService {
public interface UserService {

private static final Logger log = LoggerFactory.getLogger(UserService.class);
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 conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
TransactionSynchronizationManager.bindResource(dataSource, conn);
User user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
conn.commit();
} catch (Exception e) {
log.error(e.getMessage(), e);
if (conn != null) {
try {
TransactionSynchronizationManager.unbindResource(dataSource);
conn.rollback();
conn.close();
} catch (SQLException ex) {
log.error(ex.getMessage(), ex);
}
}
throw new DataAccessException();
}
}
User findById(final long id);
void insert(final User user);
void changePassword(final long id, final String newPassword, final String createBy);
}
7 changes: 5 additions & 2 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,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";
Expand All @@ -46,7 +46,10 @@ 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";
Expand Down

This file was deleted.

71 changes: 35 additions & 36 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.springframework.jdbc.core;

import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -11,7 +13,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.IncorrectResultSizeDataAccessException;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class JdbcTemplate {

Expand All @@ -23,32 +25,49 @@ public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public int update(String sql, Object... args) throws DataAccessException {
public int update(String sql, Object... args) {
return execute(sql, (pstmt) -> {
prepareStatement(pstmt, args);
return pstmt.executeUpdate();
});
}

public <T> T queryForObject(String sql, Class<T> requiredType, Object... args)
throws DataAccessException {
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
return execute(sql, (pstmt) -> {
prepareStatement(pstmt, args);
try (ResultSet rs = pstmt.executeQuery()) {
int columnCount = rs.getMetaData().getColumnCount();
if (rs.first() && rs.isLast()) {
Object[] initArgs = new Object[columnCount];
for (int i = 1; i <= columnCount; i++) {
initArgs[i - 1] = rs.getObject(i);
}
return InstantiateUtil.instantiate(rs, requiredType, initArgs);
}
throw new IncorrectResultSizeDataAccessException();
return instantiate(rs, requiredType);
}
});
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
private <T> T instantiate(ResultSet rs, Class<T> requiredType) throws Exception {
int columnCount = rs.getMetaData().getColumnCount();
if (rs.first() && rs.isLast()) {
Object[] initArgs = new Object[columnCount];
for (int i = 1; i <= columnCount; i++) {
initArgs[i - 1] = rs.getObject(i);
}
return instantiate(rs, requiredType, initArgs);
}
throw new IncorrectResultSizeDataAccessException();
}

private <T> T instantiate(ResultSet rs, Class<T> requiredType, Object[] initArgs)
throws Exception {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();

Class<?>[] columnTypes = new Class[columnCount];
for (int i = 1; i <= columnCount; i++) {
int columnType = metaData.getColumnType(i);
columnTypes[i - 1] = ColumnTypes.convertToClass(columnType);
}
Constructor<?> constructor = requiredType.getDeclaredConstructor(columnTypes);
return requiredType.cast(constructor.newInstance(initArgs));
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper) {
return execute(sql, (pstmt) -> {
try (ResultSet resultSet = pstmt.executeQuery()) {
List<T> results = new ArrayList<>();
Expand All @@ -67,34 +86,14 @@ private void prepareStatement(PreparedStatement pstmt, Object[] args) throws SQL
}

private <T> T execute(String sql, StatementExecution<PreparedStatement, T> function) {
Connection conn = null;
Connection conn;
try {
conn = getConnection();
conn = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = conn.prepareStatement(sql);
return function.apply(pstmt);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
try {
if (isConnectionManuallyInstantiated(conn)) {
conn.close();
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}
}

private Connection getConnection() throws SQLException {
if (TransactionSynchronizationManager.getResource(dataSource) != null) {
return TransactionSynchronizationManager.getResource(dataSource);
}
return dataSource.getConnection();
}

private boolean isConnectionManuallyInstantiated(Connection conn) {
return conn != null && TransactionSynchronizationManager.getResource(dataSource) == null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
}
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
public static void releaseConnection(DataSource dataSource) {
try {
Connection connection = TransactionSynchronizationManager.unbindResource(dataSource);
connection.close();
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
Expand Down

0 comments on commit 707d5c9

Please sign in to comment.