Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JDBC 라이브러리 구현하기 - 4단계] 페드로(류형욱) 미션 제출합니다. #918

Merged
merged 9 commits into from
Oct 17, 2024
12 changes: 4 additions & 8 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.core.RowMapper;
import com.interface21.jdbc.datasource.DataSourceUtils;
import com.techcourse.domain.User;
import java.sql.Connection;
import java.util.List;
Expand Down Expand Up @@ -35,16 +36,11 @@ public void insert(User user) {
jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail());
}

public void updateUsingExplicitConnection(User user, Connection connection) {
String sql = "UPDATE users SET account = ?, password = ?, email = ? where id = ?";
logSql(sql);
jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
}

public void update(final User user) {
public void update(User user) {
Connection connection = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
String sql = "UPDATE users SET account = ?, password = ?, email = ? WHERE id = ?";
logSql(sql);
jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
}

public List<User> findAll() {
Expand Down
13 changes: 2 additions & 11 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.core.RowMapper;
import com.interface21.jdbc.datasource.DataSourceUtils;
import com.techcourse.domain.UserHistory;
import java.sql.Connection;
import javax.sql.DataSource;
Expand Down Expand Up @@ -32,17 +33,7 @@ public UserHistoryDao(final DataSource dataSource) {

public void log(UserHistory userHistory) {
String sql = "INSERT INTO user_history (user_id, account, password, email, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?)";
logSql(sql);

jdbcTemplate.update(
sql,
userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(),
userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()
);
}

public void logUsingExplicitConnection(UserHistory userHistory, Connection connection) {
String sql = "INSERT INTO user_history (user_id, account, password, email, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?)";
Connection connection = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
logSql(sql);
jdbcTemplate.update(
connection, sql,
Expand Down
35 changes: 35 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,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(UserDao userDao, UserHistoryDao userHistoryDao) {
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}

@Override
public User findById(long id) {
return userDao.findById(id);
}

@Override
public void save(User user) {
userDao.insert(user);
}

@Override
public void changePassword(long id, String newPassword, String createBy) {
User user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
33 changes: 33 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,33 @@
package com.techcourse.service;

import com.interface21.jdbc.transaction.TransactionManager;
import com.techcourse.domain.User;
import javax.sql.DataSource;

public class TxUserService implements UserService {

private final AppUserService appUserService;
private final DataSource dataSource;

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

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

@Override
public void save(User user) {
appUserService.save(user);
}

@Override
public void changePassword(long id, String newPassword, String createBy) {
TransactionManager.executeTransactionOf(
connection -> appUserService.changePassword(id, newPassword, createBy), dataSource
);
}
}
36 changes: 4 additions & 32 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
package com.techcourse.service;

import com.interface21.jdbc.transaction.TransactionManager;
import com.techcourse.dao.UserDao;
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.domain.UserHistory;
import java.sql.Connection;

public class UserService {
public interface UserService {

private final TransactionManager txManager;
private final UserDao userDao;
private final UserHistoryDao userHistoryDao;
User findById(long id);

public UserService(TransactionManager txManager, UserDao userDao, UserHistoryDao userHistoryDao) {
this.txManager = txManager;
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}
void save(User user);

public User findById(long id) {
return userDao.findById(id);
}

public void insert(User user) {
userDao.insert(user);
}

public void changePassword(long id, String newPassword, String createBy) {
txManager.executeTransactionOf(conn -> changePasswordTx(conn, id, newPassword, createBy));
}

private void changePasswordTx(Connection connection, long id, String newPassword, String createBy) {
final var user = findById(id);
user.changePassword(newPassword);
userDao.updateUsingExplicitConnection(user, connection);
userHistoryDao.logUsingExplicitConnection(new UserHistory(user, createBy), connection);
}
void changePassword(long id, String newPassword, String createdBy);
}
Original file line number Diff line number Diff line change
@@ -1,10 +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 {

Expand All @@ -13,7 +12,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) {
}

@Override
public void logUsingExplicitConnection(UserHistory userHistory, Connection connection) {
public void log(UserHistory userHistory) {
throw new DataAccessException();
}
}
18 changes: 9 additions & 9 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.techcourse.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.transaction.TransactionManager;
import com.techcourse.config.DataSourceConfig;
import com.techcourse.dao.UserDao;
import com.techcourse.dao.UserHistoryDao;
Expand All @@ -17,14 +16,12 @@

class UserServiceTest {

private TransactionManager txManager;
private JdbcTemplate jdbcTemplate;
private UserDao userDao;

@BeforeEach
void setUp() {
DataSource dataSource = DataSourceConfig.getInstance();
txManager = new TransactionManager(dataSource);
jdbcTemplate = new JdbcTemplate(dataSource);
userDao = new UserDao(jdbcTemplate);

Expand All @@ -36,7 +33,7 @@ void setUp() {
@Test
void testChangePassword() {
final var userHistoryDao = new UserHistoryDao(jdbcTemplate);
final var userService = new UserService(txManager, userDao, userHistoryDao);
final var userService = new AppUserService(userDao, userHistoryDao);

final var newPassword = "qqqqq";
final var createBy = "gugu";
Expand All @@ -51,13 +48,16 @@ void testChangePassword() {
void testTransactionRollback() {
// 트랜잭션 롤백 테스트를 위해 mock으로 교체
final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate);
final var userService = new UserService(txManager, userDao, userHistoryDao);
// 애플리케이션 서비스
final var appUserService = new AppUserService(userDao, userHistoryDao);
// 트랜잭션 서비스 추상화
final var userService = new TxUserService(appUserService, jdbcTemplate.getDataSource());

final var newPassword = "newPassword";
final var createBy = "gugu";
final var createdBy = "gugu";
// 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다.
assertThrows(DataAccessException.class,
() -> userService.changePassword(1L, newPassword, createBy));
assertThatThrownBy(() -> userService.changePassword(1L, newPassword, createdBy))
.isInstanceOf(DataAccessException.class);

final var actual = userService.findById(1L);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class JdbcTemplate {

private final DataSource dataSource;

public JdbcTemplate(final DataSource dataSource) {
public JdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}

Expand All @@ -37,7 +37,7 @@ public int update(Connection connection, String sql, Object... args) {

public int update(String sql, PreparedStatementSetter psSetter) {
try (Connection connection = dataSource.getConnection()) {
return update(new QueryConnectionHolder(connection, sql), psSetter);
return update(new QueryConnectionHolder(connection, sql), psSetter);
} catch (SQLException e) {
throw new DataAccessException("Update 실패", e);
}
Expand Down Expand Up @@ -75,4 +75,8 @@ private <T> List<T> retrieveRow(RowMapper<T> rowMapper, PreparedStatement ps) th
}
return results;
}

public DataSource getDataSource() {
return dataSource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd

public static void releaseConnection(Connection connection, DataSource dataSource) {
try {
Connection closingConnection = TransactionSynchronizationManager.unbindResource(dataSource);
assert closingConnection.equals(connection);
connection.close();
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.interface21.jdbc.transaction;

import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.datasource.DataSourceUtils;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Logger;
Expand All @@ -10,32 +11,29 @@ public class TransactionManager {

private static final Logger log = Logger.getLogger(TransactionManager.class.getName());

private final DataSource dataSource;

public TransactionManager(DataSource dataSource) {
this.dataSource = dataSource;
private TransactionManager() {
}

public void executeTransactionOf(TransactionalFunction callback) {
Connection connection = null;
public static void executeTransactionOf(TransactionalFunction callback, DataSource dataSource) {
Connection connection = DataSourceUtils.getConnection(dataSource);
boolean shouldThrow = false;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
callback.execute(connection);
connection.commit();
} catch (Exception e) {
gracefulShutdown(connection, Connection::rollback);
shouldThrow = true;
} finally {
gracefulShutdown(connection, Connection::close);
DataSourceUtils.releaseConnection(connection, dataSource);
}

if (shouldThrow) {
throw new DataAccessException("트랜잭션 실행 중 문제가 발생했습니다. 트랜잭션은 롤백됩니다.");
}
}

private void gracefulShutdown(Connection connection, ThrowingConsumer<Connection> connectionConsumer) {
private static void gracefulShutdown(Connection connection, ThrowingConsumer<Connection> connectionConsumer) {
try {
connectionConsumer.accept(connection);
} catch (NullPointerException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
package com.interface21.transaction.support;

import java.util.HashMap;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Map;

public abstract class TransactionSynchronizationManager {

private static final ThreadLocal<Map<DataSource, Connection>> resources = new ThreadLocal<>();
private static final ThreadLocal<Map<DataSource, Connection>> resources = ThreadLocal.withInitial(HashMap::new);

private TransactionSynchronizationManager() {}

public static Connection getResource(DataSource key) {
return null;
Map<DataSource, Connection> connections = resources.get();
return connections.get(key);
}

public static void bindResource(DataSource key, Connection value) {
Map<DataSource, Connection> connections = resources.get();
connections.put(key, value);
}

public static Connection unbindResource(DataSource key) {
return null;
Map<DataSource, Connection> connections = resources.get();
Connection unbindedConnection = connections.remove(key);
deAllocateIfEmpty(connections);
return unbindedConnection;
}

private static void deAllocateIfEmpty(Map<DataSource, Connection> connections) {
if (connections.isEmpty()) {
resources.remove();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ void setUp() throws SQLException {
@DisplayName("트랜잭션 실행 중 예외가 발생하면 롤백된다.")
void rollbackOnException() throws SQLException {
// given
TransactionManager txManager = new TransactionManager(dataSource);
TransactionalFunction txFunction = conn -> {
throw new SQLException();
};

// when & then
assertThatThrownBy(() -> txManager.executeTransactionOf(txFunction))
assertThatThrownBy(() -> TransactionManager.executeTransactionOf(txFunction, dataSource))
.isInstanceOf(DataAccessException.class);
verify(connection).rollback();
verify(connection, never()).commit();
Expand All @@ -46,9 +45,8 @@ void rollbackOnException() throws SQLException {
@Test
@DisplayName("예외 없는 트랜잭션은 정상적으로 커밋된다.")
void commitOnNoException() throws SQLException {
TransactionManager txManager = new TransactionManager(dataSource);
TransactionalFunction txFunction = conn -> {};
txManager.executeTransactionOf(txFunction);
TransactionManager.executeTransactionOf(txFunction, dataSource);

verify(connection).commit();
verify(connection, never()).rollback();
Expand Down
Loading