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

[4단계 - JDBC 라이브러리 구현하기] 포케(이무송) 미션 제출합니다. #878

Open
wants to merge 25 commits into
base: fromitive
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b1df04
refactor: 아주 사소한 리팩터링
fromitive Oct 11, 2024
b51ab40
feat: transaction 적용
fromitive Oct 11, 2024
f284767
feat: transaction 적용할 수 있는 update query 추가
fromitive Oct 12, 2024
342ed40
feat: 비밀번호 변경 시 transaction 적용
fromitive Oct 12, 2024
5517774
test: 의도한 오류 검증구문 추가
fromitive Oct 12, 2024
f8a2872
fix: commit 메서드 추가
fromitive Oct 12, 2024
54ae415
refactor: 함수형 인터페이스를 사용하여 try-catch 구문 최소화
fromitive Oct 12, 2024
ae5b6cc
refactor: 불필요한 제네릭 제거
fromitive Oct 12, 2024
695fda3
fix: rollback 메서드 추가
fromitive Oct 12, 2024
90cf77c
refactor: 예외를 상세하게 처리
fromitive Oct 14, 2024
bf6ac86
refactor: 비즈니스 로직과 트렌젝션 로직 분리
fromitive Oct 14, 2024
67794e6
feat: TransactionSynchronizationManager 구현
fromitive Oct 14, 2024
676a201
feat: connection을 TransactionSynchronizationManager 에서 가져오도록 변경
fromitive Oct 14, 2024
cc6741f
Merge remote-tracking branch 'woowacourse/fromitive' into step4
fromitive Oct 14, 2024
6d13c44
refactor: CheckedExceptionExecutor 관련 객체 jdbc로 이동
fromitive Oct 14, 2024
e6e2d74
refactor: releaseConnection에서 TransactionSynchronizationManager를 관리하도…
fromitive Oct 14, 2024
5388089
refactor: TransactionSynchronizationManager 의존 제거
fromitive Oct 14, 2024
3e1957e
fix: changePassword 할 때 connection이 close되지 않은 이슈 해결
fromitive Oct 14, 2024
493f3a0
refactor: releaseDataSource 코드 리팩터링
fromitive Oct 14, 2024
c6df18a
test: 학습테스트 0 단계
fromitive Oct 15, 2024
82cc622
test: 학습테스트 1 단계
fromitive Oct 15, 2024
8af7a2d
test: 학습테스트 2 단계
fromitive Oct 15, 2024
f53e17b
refactor: close()시 TransactionSynchronizationManager에서도 해제 하도록 변경
fromitive Oct 22, 2024
1900e79
refactor: AppUserService에 Transactional 제거
fromitive Oct 22, 2024
e85e136
fix: get을 할때 값 존재 여부를 검증
fromitive Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 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,34 @@
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 UserDao userDao;
private 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);
}

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

@Override
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));
}
}
52 changes: 52 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,52 @@
package com.techcourse.service;

import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.datasource.DataSourceUtils;
import com.interface21.jdbc.support.SQLExceptionConsumer;
import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

public class TxUserService implements UserService {

private final UserService userService;

public TxUserService(UserService userService) {
this.userService = userService;
}

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

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

@Override
public void changePassword(final long id, final String newPassword, final String createBy) {
DataSource dataSource = DataSourceConfig.getInstance();
Connection connection = DataSourceUtils.getConnection(dataSource);
try {
connection.setAutoCommit(false);
userService.changePassword(id, newPassword, createBy);
connection.commit();
} catch (DataAccessException | SQLException e) {
rollbackTransaction(connection);
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseDataSource(dataSource);
}
}

private void rollbackTransaction(Connection connection) {
SQLExceptionConsumer.execute(() -> {
connection.rollback();
return null;
}, "connection을 rollback하는데 실패했습니다.");
}
}
64 changes: 4 additions & 60 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,68 +1,12 @@
package com.techcourse.service;

import com.interface21.dao.DataAccessException;
import com.techcourse.dao.UserDao;
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.domain.UserHistory;
import com.techcourse.support.SQLExceptionConsumer;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

public class UserService {
public interface UserService {

private UserDao userDao;
private UserHistoryDao userHistoryDao;
private DataSource dataSource;
User findById(final long id);

public UserService(DataSource datasource, UserDao userDao, UserHistoryDao userHistoryDao) {
this.dataSource = datasource;
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}
void insert(final User user);

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 connection = SQLExceptionConsumer.execute(dataSource::getConnection, "connetion을 가져오는데 실패했습니다");
try {
connection.setAutoCommit(false);
doChangePassword(id, newPassword, createBy, connection);
connection.commit();
} catch (SQLException e) {
rollbackTransaction(connection);
throw new DataAccessException(e);
} finally {
closeConnection(connection);
}
}

private void closeConnection(Connection connection) {
SQLExceptionConsumer.execute(() -> {
connection.close();
return null;
}, "connection을 닫는데 실패했습니다.");
}

private void rollbackTransaction(Connection connection) {
SQLExceptionConsumer.execute(() -> {
connection.rollback();
return null;
}, "connection을 rollback하는데 실패했습니다.");
}


private void doChangePassword(long id, String newPassword, String createBy, Connection connection) {
User user = findById(id);
user.changePassword(newPassword);
userDao.update(connection, user);
userHistoryDao.log(connection, new UserHistory(user, createBy));
}
void changePassword(final long id, final String newPassword, final String createBy);
}
14 changes: 8 additions & 6 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -33,9 +32,10 @@ void setUp() {
}

@Test
void testChangePassword() throws SQLException {
void testChangePassword() {
final var userHistoryDao = new UserHistoryDao(jdbcTemplate);
final var userService = new UserService(dataSource, userDao, userHistoryDao);
final var appUserService = new AppUserService(userDao, userHistoryDao);
final var userService = new TxUserService(appUserService);

final var newPassword = "qqqqq";
final var createBy = "gugu";
Expand All @@ -50,12 +50,14 @@ void testChangePassword() throws SQLException {
void testTransactionRollback() {
// 트랜잭션 롤백 테스트를 위해 mock으로 교체
final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate);
final var userService = new UserService(dataSource, 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));
() -> userService.changePassword(1L, newPassword, createdBy));

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

Expand Down
10 changes: 6 additions & 4 deletions jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.interface21.jdbc.core;

import com.interface21.dao.DataAccessException;
import com.interface21.jdbc.datasource.DataSourceUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
Expand Down Expand Up @@ -38,16 +39,17 @@ public void update(String sql, Connection connection, PreparedStatementSetter pr

private <T> T execute(String sql, PreparedStatementSetter preparedStatementSetter,
PreparedStatementStrategy<T> strategy) {
try (Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
) {
Connection connection = DataSourceUtils.getConnection(dataSource);
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatementSetter.setValues(preparedStatement);
return strategy.execute(preparedStatement);
} catch (SQLException e) {
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseConnection(dataSource);
}
}

private <T> T executeWithExternalConnection(String sql, Connection connection,
PreparedStatementSetter preparedStatementSetter,
PreparedStatementStrategy<T> strategy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import com.interface21.jdbc.CannotGetJdbcConnectionException;
import com.interface21.transaction.support.TransactionSynchronizationManager;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

// 4단계 미션에서 사용할 것
public abstract class DataSourceUtils {

private DataSourceUtils() {}
private DataSourceUtils() {
}

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
Connection connection = TransactionSynchronizationManager.getResource(dataSource);
Expand All @@ -27,8 +27,20 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
}
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
public static void releaseConnection(DataSource dataSource) {

Choose a reason for hiding this comment

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

분리된 이유를 설명해주실 수 있나요??ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

jdbctemplate에서 execute메서드를 실행할 때 트렌젝션의 유무의 따라 커넥션을 닫는 여부를 결정해야 하는데,

트렌젝션이 설정되어 있으면 롤백을 위해 커넥션을 닫으면 안되서 분리하게 되었습니다..!

try {
Connection connection = TransactionSynchronizationManager.getResource(dataSource);
if (connection != null && connection.getAutoCommit()) {
releaseDataSource(dataSource);
}
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
}
}

public static void releaseDataSource(DataSource dataSource) {
try {
Connection connection = TransactionSynchronizationManager.unbindResource(dataSource);
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,4 +1,4 @@
package com.techcourse.support;
package com.interface21.jdbc.support;

@FunctionalInterface
public interface CheckedExceptionExecutor<R, T extends Throwable> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.techcourse.support;
package com.interface21.jdbc.support;

import com.interface21.dao.DataAccessException;
import java.sql.SQLException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package com.interface21.transaction.support;

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

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);

Choose a reason for hiding this comment

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

👍👍👍


private TransactionSynchronizationManager() {}
private TransactionSynchronizationManager() {
}

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

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

public static Connection unbindResource(DataSource key) {
return null;
Map<DataSource, Connection> binds = resources.get();
return binds.remove(key);
}
}
50 changes: 25 additions & 25 deletions jdbc/src/test/java/com/interface21/jdbc/core/JdbcTemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class JdbcTemplateTest {
void setup() throws Exception {
given(this.dataSource.getConnection()).willReturn(this.connection);
given(this.connection.prepareStatement(anyString())).willReturn(this.preparedStatement);
given(this.connection.getAutoCommit()).willReturn(true);
given(this.preparedStatement.executeQuery()).willReturn(this.resultSet);
given(this.preparedStatement.executeQuery(anyString())).willReturn(this.resultSet);
given(this.preparedStatement.getConnection()).willReturn(this.connection);
Expand Down Expand Up @@ -99,31 +100,6 @@ void updateWithNoParameter() throws SQLException {
verifyNoInteractions(resultSet);
}

@DisplayName("파라미터로 받은 connection을 이용해 update 실행할 때 connection은 close되지 않는다.")
@Test
void updateWithConnectionParameter() throws SQLException {
// given
Connection externalConnection = mock(Connection.class);
PreparedStatement externalPreparedStatement = mock(PreparedStatement.class);
given(externalConnection.prepareStatement(anyString())).willReturn(externalPreparedStatement);

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
PreparedStatementSetter preparedStatementSetter = (preparedStatement) -> {
};

// when
jdbcTemplate.update("insert into user (account, age, email) values ('pororo', 20, '[email protected]')",
externalConnection,
preparedStatementSetter);

// then
verify(externalPreparedStatement, times(0)).setObject(anyInt(), any());
verify(externalPreparedStatement).close();
verify(externalConnection, times(0)).close();
verifyNoInteractions(resultSet);
}


@DisplayName("query 내에 쿼리문 실행 중 예외가 발생할 경우 관련된 리소스들이 닫혀야 한다.")
@Test
void queryWithExecuteQueryException() throws SQLException {
Expand Down Expand Up @@ -332,6 +308,30 @@ void queryForObjectWithNoResultException() throws SQLException {
verify(this.preparedStatement).close();
}

@DisplayName("connection.setAutoCommit이 false일 경우 close되지 않는다.")
@Test
void notClosedWhenSetAutoCommitIsFalse() throws SQLException {
// given
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
given(connection.getAutoCommit()).willReturn(false);
PreparedStatementSetter preparedStatementSetter = (preparedStatement) -> {
preparedStatement.setString(1, "pororo");
preparedStatement.setString(2, "poke");
};
SQLException sqlException = new SQLException("업데이트 중 예외 발생");
given(this.preparedStatement.executeUpdate()).willThrow(sqlException);

// when
assertThatExceptionOfType(DataAccessException.class)
.isThrownBy(() -> jdbcTemplate.update("update error", preparedStatementSetter))
.withCause(sqlException);

// then
verify(preparedStatement).close();
verify(connection, times(0)).close();
verifyNoInteractions(resultSet);
}

record TestUser(String account, String password) {
}
}
2 changes: 0 additions & 2 deletions study/src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# mysql 8.0.30부터는 statement.execute()으로 여러 쿼리를 한 번에 실행할 수 없다.
# 멀티 쿼리 옵션을 url로 전달하도록 수정하는 방법을 찾아서 적용하자.
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account VARCHAR(100) NOT NULL,
Expand Down
Loading