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

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

Merged
merged 7 commits into from
Oct 14, 2024
7 changes: 7 additions & 0 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.core.RowMapper;
import com.techcourse.domain.User;
import java.sql.Connection;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
Expand Down Expand Up @@ -34,6 +35,12 @@ 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) {
String sql = "UPDATE users SET account = ?, password = ?, email = ? WHERE id = ?";
logSql(sql);
Expand Down
19 changes: 17 additions & 2 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.core.RowMapper;
import com.techcourse.domain.UserHistory;
import java.sql.Connection;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -31,7 +32,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 (?, ?, ?, ?, ?, ?)";
log.debug("query : {}", sql);
logSql(sql);

jdbcTemplate.update(
sql,
Expand All @@ -40,9 +41,23 @@ public void log(UserHistory userHistory) {
);
}

public void logUsingExplicitConnection(UserHistory userHistory, Connection connection) {
String sql = "INSERT INTO user_history (user_id, account, password, email, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?)";
logSql(sql);
jdbcTemplate.update(
connection, sql,
userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(),
userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()
);
}

public UserHistory findById(Long id) {
String sql = "SELECT id, user_id, account, password, email, created_at, created_by FROM user_history WHERE id = ?";
log.debug("query : {}", sql);
logSql(sql);
return jdbcTemplate.queryForObject(sql, ROW_MAPPER, id);
}

private void logSql(String sql) {
log.debug("query : {}", sql);
}
}
20 changes: 14 additions & 6 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
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 {

private final TransactionManager txManager;
private final UserDao userDao;
private final UserHistoryDao userHistoryDao;

public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) {
public UserService(TransactionManager txManager, UserDao userDao, UserHistoryDao userHistoryDao) {
this.txManager = txManager;
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}

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

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

public void changePassword(final long id, final String newPassword, final String createBy) {
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.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
userDao.updateUsingExplicitConnection(user, connection);
userHistoryDao.logUsingExplicitConnection(new UserHistory(user, createBy), connection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.techcourse.domain.UserHistory;
import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.core.JdbcTemplate;
import java.sql.Connection;

public class MockUserHistoryDao extends UserHistoryDao {

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

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

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

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;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
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;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Disabled
class UserServiceTest {

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

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

DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());
final var user = new User("gugu", "password", "hkkang@woowahan.com");
DatabasePopulatorUtils.execute(dataSource);
User user = new User("gugu", "password", "hkkang@woowahan.com");
userDao.insert(user);
}

@Test
void testChangePassword() {
final var userHistoryDao = new UserHistoryDao(jdbcTemplate);
final var userService = new UserService(userDao, userHistoryDao);
final var userService = new UserService(txManager, userDao, userHistoryDao);

final var newPassword = "qqqqq";
final var createBy = "gugu";
Expand All @@ -48,7 +51,7 @@ void testChangePassword() {
void testTransactionRollback() {
// 트랜잭션 롤백 테스트를 위해 mock으로 교체
final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate);
final var userService = new UserService(userDao, userHistoryDao);
final var userService = new UserService(txManager, userDao, userHistoryDao);

final var newPassword = "newPassword";
final var createBy = "gugu";
Expand Down
30 changes: 21 additions & 9 deletions jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public int update(QueryConnectionHolder queryConnectionHolder, PreparedStatementSetter preparedStatementSetter) {
try {
PreparedStatement preparedStatement = queryConnectionHolder.getAsPreparedStatement();
preparedStatementSetter.setValues(preparedStatement);
return preparedStatement.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Update 실패", e);
}
}

public int update(Connection connection, String sql, Object... args) {
return update(new QueryConnectionHolder(connection, sql), new PreparedStatementArgumentsSetter(args));
}

public int update(String sql, PreparedStatementSetter psSetter) {
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
psSetter.setValues(ps);
return ps.executeUpdate();
try (Connection connection = dataSource.getConnection()) {
return update(new QueryConnectionHolder(connection, sql), psSetter);
} catch (SQLException e) {
throw new DataAccessException("Update 실패", e);
}
Expand All @@ -35,11 +47,6 @@ public int update(String sql, Object... args) {
return update(sql, new PreparedStatementArgumentsSetter(args));
}

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
List<T> query = query(sql, rowMapper, args);
return query.isEmpty() ? null : query.getLast();
}

public <T> List<T> query(String sql, PreparedStatementSetter psSetter, RowMapper<T> rowMapper) {
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
Expand All @@ -54,6 +61,11 @@ public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
return query(sql, new PreparedStatementArgumentsSetter(args), rowMapper);
}

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
List<T> query = query(sql, rowMapper, args);
return query.isEmpty() ? null : query.getLast();
}

private <T> List<T> retrieveRow(RowMapper<T> rowMapper, PreparedStatement ps) throws SQLException {
List<T> results = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.interface21.jdbc.core;

import com.interface21.dao.DataAccessException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Objects;

public class QueryConnectionHolder {

private final String query;
private final Connection connection;

public QueryConnectionHolder(Connection connection, String query) {
this.connection = Objects.requireNonNull(connection);
this.query = Objects.requireNonNull(query);
}

public PreparedStatement getAsPreparedStatement() {
try {
return connection.prepareStatement(query);
} catch (SQLException e) {
throw new DataAccessException("PreparedStatement 생성 실패", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.interface21.jdbc.transaction;

import com.interface21.dao.DataAccessException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Logger;
import javax.sql.DataSource;

public class TransactionManager {

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

private final DataSource dataSource;

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

public void executeTransactionOf(TransactionalFunction callback) {
Connection connection = null;
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);
}
if (shouldThrow) {
throw new DataAccessException("트랜잭션 실행 중 문제가 발생했습니다. 트랜잭션은 롤백됩니다.");
}
}

private void gracefulShutdown(Connection connection, ThrowingConsumer<Connection> connectionConsumer) {
try {
connectionConsumer.accept(connection);
} catch (NullPointerException e) {
log.warning("Connection을 찾을 수 없습니다.");
} catch (SQLException e) {
throw new DataAccessException(e.getMessage(), e);
}
}

@FunctionalInterface
private interface ThrowingConsumer<T> {

void accept(T connection) throws SQLException;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.interface21.jdbc.transaction;

import java.sql.Connection;
import java.sql.SQLException;

@FunctionalInterface
public interface TransactionalFunction {

void execute(Connection connection) throws SQLException;
}
Comment on lines +6 to +10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consumer 역할로 보이는데 Function이라는 클래스명은 혼동을 줄 수 있을 것 같아요! (ThrowingConsumer도 있기 때문에)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그럴 수도 있겠네요!

사실 ThrowingConsumer 보다는 TransactionalFunction이 더 중요한 역할을 하는데요, ThrowingConsumer<T, E> 형태로 예외를 던지면서 consume 하는 전역 인터페이스를 만들어서 사용하려는 것이 의도였는데 지금은 사실 던질 수 있는 예외가 SQLException으로 고정되어 있고 TransactionManager에서만 사용하다 보니 굳이 클래스 외부에 있을 필요가 없을 것 같네요.

ThrowingConsumer 인터페이스 선언 위치를 TransactionManager 내부로 변경하였습니다. TransactionalFunction 인터페이스는 그대로 유지할게요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.interface21.jdbc.transaction;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import com.interface21.dao.DataAccessException;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class TransactionManagerTest {

private DataSource dataSource;
private Connection connection;

@BeforeEach
void setUp() throws SQLException {
dataSource = mock(DataSource.class);
connection = mock(Connection.class);
given(dataSource.getConnection()).willReturn(connection);
}

@Test
@DisplayName("트랜잭션 실행 중 예외가 발생하면 롤백된다.")
void rollbackOnException() throws SQLException {
// given
TransactionManager txManager = new TransactionManager(dataSource);
TransactionalFunction txFunction = conn -> {
throw new SQLException();
};

// when & then
assertThatThrownBy(() -> txManager.executeTransactionOf(txFunction))
.isInstanceOf(DataAccessException.class);
verify(connection).rollback();
verify(connection, never()).commit();
verify(connection).close();
}

@Test
@DisplayName("예외 없는 트랜잭션은 정상적으로 커밋된다.")
void commitOnNoException() throws SQLException {
TransactionManager txManager = new TransactionManager(dataSource);
TransactionalFunction txFunction = conn -> {};
txManager.executeTransactionOf(txFunction);

verify(connection).commit();
verify(connection, never()).rollback();
verify(connection).close();
}
}