-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[3단계 - JDBC 라이브러리 구현하기] 페드로(류형욱) 미션 제출합니다. (#850)
* chore(UserServiceTest): 테스트 코드의 `@Disabled` 삭제 * feat(TransactionManager): 트랜잭션을 관리할 객체 생성 * feat(UserService): 사용자 비밀번호 변경 로직에 트랜잭션 적용 * refactor(JdbcTemplate): 오버로딩된 update() 메서드의 호출 계층 구조 설정 * style(JdbcTemplate): 메서드 순서 변경 * refactor(ThrowingConsumer): 인터페이스 선언 위치 변경 * refactor(JdbcTemplate): update 메서드의 시그니처 추가 지원
- Loading branch information
Showing
10 changed files
with
223 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", "[email protected]"); | ||
DatabasePopulatorUtils.execute(dataSource); | ||
User user = new User("gugu", "password", "[email protected]"); | ||
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"; | ||
|
@@ -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"; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
jdbc/src/main/java/com/interface21/jdbc/core/QueryConnectionHolder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
jdbc/src/main/java/com/interface21/jdbc/transaction/TransactionManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
jdbc/src/main/java/com/interface21/jdbc/transaction/TransactionalFunction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
57 changes: 57 additions & 0 deletions
57
jdbc/src/test/java/com/interface21/jdbc/transaction/TransactionManagerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |