-
Notifications
You must be signed in to change notification settings - Fork 300
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단계] 디투(박정훈) 미션 제출합니다. #593
Changes from 3 commits
bf53527
dafbbe9
b107091
2fef7eb
407ef4d
e1b576b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,39 @@ | ||
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 org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.dao.DataAccessException; | ||
|
||
import java.sql.Connection; | ||
import java.sql.SQLException; | ||
import org.springframework.transaction.support.TransactionTemplate; | ||
|
||
public class UserService { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(UserService.class); | ||
|
||
private final UserDao userDao; | ||
private final UserHistoryDao userHistoryDao; | ||
private final TransactionTemplate transactionTemplate; | ||
|
||
public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { | ||
public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao, final TransactionTemplate transactionTemplate) { | ||
this.userDao = userDao; | ||
this.userHistoryDao = userHistoryDao; | ||
this.transactionTemplate = transactionTemplate; | ||
} | ||
|
||
public User findById(final long id) { | ||
return userDao.findById(id); | ||
} | ||
|
||
public void insert(final User user) { | ||
Connection connection = null; | ||
try { | ||
connection = DataSourceConfig.getInstance().getConnection(); | ||
connection.setAutoCommit(false); | ||
userDao.insert(connection, user); | ||
connection.commit(); | ||
} catch (SQLException e) { | ||
log.error(e.getMessage()); | ||
rollback(connection); | ||
} finally { | ||
release(connection); | ||
} | ||
transactionTemplate.execute(connection -> userDao.insert(user)); | ||
} | ||
|
||
public void changePassword(final long id, final String newPassword, final String createBy) { | ||
Connection connection = null; | ||
try { | ||
connection = DataSourceConfig.getInstance().getConnection(); | ||
connection.setAutoCommit(false); | ||
transactionTemplate.execute(connection -> { | ||
final var user = findById(id); | ||
user.changePassword(newPassword); | ||
userDao.update(connection, user); | ||
userDao.update(user); | ||
userHistoryDao.log(connection, new UserHistory(user, createBy)); | ||
connection.commit(); | ||
} catch (SQLException e) { | ||
log.error(e.getMessage()); | ||
rollback(connection); | ||
} finally { | ||
release(connection); | ||
} | ||
} | ||
|
||
private void rollback(final Connection connection) { | ||
if (connection != null) { | ||
try { | ||
connection.rollback(); | ||
} catch (SQLException e) { | ||
log.error(e.getMessage()); | ||
throw new DataAccessException("rollback 에 실패했습니다."); | ||
} | ||
} | ||
} | ||
|
||
private void release(final Connection connection) { | ||
if (connection != null) { | ||
try { | ||
connection.close(); | ||
} catch (SQLException e) { | ||
log.error(e.getMessage()); | ||
throw new DataAccessException("자원 정리에 실패했습니다."); | ||
} | ||
} | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ void setup() throws SQLException { | |
userDao = new UserDao(dataSource); | ||
connection = dataSource.getConnection(); | ||
final var user = new User("hongsil", "486", "[email protected]"); | ||
userDao.insert(connection, user); | ||
userDao.insert(user); | ||
} | ||
|
||
@Test | ||
|
@@ -54,7 +54,7 @@ void findById_make_exception_when_no_result() { | |
@Test | ||
void findByAccount() { | ||
final var account = "mylove_hongsil"; | ||
userDao.insert(connection, new User(account, "비밀번호486", "[email protected]")); | ||
userDao.insert(new User(account, "비밀번호486", "[email protected]")); | ||
final var user = userDao.findByAccount(account); | ||
|
||
assertThat(user.getAccount()).isEqualTo(account); | ||
|
@@ -63,8 +63,8 @@ void findByAccount() { | |
@Test | ||
void findByAccount_make_exception_when_multiple_result() { | ||
final var user = new User("ditoo", "password", "[email protected]"); | ||
userDao.insert(connection, user); | ||
userDao.insert(connection, user); | ||
userDao.insert(user); | ||
userDao.insert(user); | ||
assertThatThrownBy(() -> userDao.findByAccount(user.getAccount())) | ||
.isInstanceOf(IncorrectResultSizeDataAccessException.class); | ||
} | ||
|
@@ -73,7 +73,7 @@ void findByAccount_make_exception_when_multiple_result() { | |
void insert() { | ||
final var account = "insert-gugu"; | ||
final var user = new User(account, "password", "[email protected]"); | ||
userDao.insert(connection, user); | ||
userDao.insert(user); | ||
|
||
final var actual = userDao.findById(2L); | ||
|
||
|
@@ -86,7 +86,7 @@ void update() { | |
final var user = userDao.findById(1L); | ||
user.changePassword(newPassword); | ||
|
||
userDao.update(connection, user); | ||
userDao.update(user); | ||
|
||
final var actual = userDao.findById(1L); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Disabled; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.transaction.support.TransactionTemplate; | ||
|
||
import javax.sql.DataSource; | ||
|
||
|
@@ -22,22 +23,24 @@ class UserServiceTest { | |
|
||
private JdbcTemplate jdbcTemplate; | ||
private UserDao userDao; | ||
private TransactionTemplate transactionTemplate; | ||
|
||
@BeforeEach | ||
void setUp() throws SQLException { | ||
void setUp() { | ||
final DataSource dataSource = DataSourceConfig.getInstance(); | ||
this.jdbcTemplate = new JdbcTemplate(dataSource); | ||
this.userDao = new UserDao(dataSource); | ||
this.transactionTemplate = new TransactionTemplate(dataSource); | ||
|
||
DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); | ||
final var user = new User("gugu", "password", "[email protected]"); | ||
userDao.insert(dataSource.getConnection(), user); | ||
userDao.insert(user); | ||
} | ||
|
||
@Test | ||
void testChangePassword() { | ||
final var userHistoryDao = new UserHistoryDao(jdbcTemplate); | ||
final var userService = new UserService(userDao, userHistoryDao); | ||
final var userService = new UserService(userDao, userHistoryDao, transactionTemplate); | ||
|
||
final var newPassword = "qqqqq"; | ||
final var createBy = "gugu"; | ||
|
@@ -52,7 +55,7 @@ void testChangePassword() { | |
void testTransactionRollback() { | ||
// 트랜잭션 롤백 테스트를 위해 mock으로 교체 | ||
final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); | ||
final var userService = new UserService(userDao, userHistoryDao); | ||
final var userService = new UserService(userDao, userHistoryDao, transactionTemplate); | ||
|
||
final var newPassword = "newPassword"; | ||
final var createBy = "gugu"; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.springframework.transaction.support; | ||
|
||
import java.sql.Connection; | ||
|
||
@FunctionalInterface | ||
public interface TransactionCallback { | ||
|
||
void doInTransaction(Connection connection); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,58 @@ | ||
package org.springframework.transaction.support; | ||
|
||
import javax.annotation.Nullable; | ||
import javax.sql.DataSource; | ||
import java.sql.Connection; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
public abstract class TransactionSynchronizationManager { | ||
|
||
private static final ThreadLocal<Map<DataSource, Connection>> resources = new ThreadLocal<>(); | ||
private static final ThreadLocal<Boolean> actualTransactionActive = ThreadLocal.withInitial(() -> Boolean.FALSE); | ||
|
||
private TransactionSynchronizationManager() {} | ||
|
||
public static Connection getResource(DataSource key) { | ||
return null; | ||
@Nullable | ||
public static Connection getResource(final DataSource key) { | ||
final Map<DataSource, Connection> map = resources.get(); | ||
if (map == null) { | ||
return null; | ||
} | ||
return map.get(key); | ||
} | ||
|
||
public static void bindResource(DataSource key, Connection value) { | ||
public static void bindResource(final DataSource key, final Connection value) { | ||
final Map<DataSource, Connection> map = resources.get(); | ||
if (map == null) { | ||
final Map<DataSource, Connection> newMap = new HashMap<>(); | ||
newMap.put(key, value); | ||
resources.set(newMap); | ||
return; | ||
} | ||
map.put(key, value); | ||
resources.set(map); | ||
} | ||
|
||
public static Connection unbindResource(DataSource key) { | ||
return null; | ||
public static void unbindResource(final DataSource key) { | ||
final Map<DataSource, Connection> map = resources.get(); | ||
if (map == null) { | ||
throw new IllegalStateException("No resource for key [" + key + "] bound to thread"); | ||
} | ||
if (map.get(key) == null) { | ||
throw new IllegalStateException("No value for key [" + key + "] bound to thread"); | ||
} | ||
map.remove(key); | ||
if (map.isEmpty()) { | ||
resources.remove(); | ||
} | ||
} | ||
|
||
public static boolean isActualTransactionActive() { | ||
return actualTransactionActive.get(); | ||
} | ||
|
||
public static void setActualTransactionActiveTrue() { | ||
actualTransactionActive.set(true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package org.springframework.transaction.support; | ||
|
||
import org.springframework.dao.DataAccessException; | ||
import org.springframework.jdbc.datasource.DataSourceUtils; | ||
|
||
import javax.sql.DataSource; | ||
import java.sql.Connection; | ||
import java.sql.SQLException; | ||
|
||
public class TransactionTemplate { | ||
|
||
private final DataSource dataSource; | ||
|
||
public TransactionTemplate(final DataSource dataSource) { | ||
this.dataSource = dataSource; | ||
} | ||
|
||
public void execute(final TransactionCallback transactionCallback) { | ||
final Connection connection = DataSourceUtils.getConnection(dataSource); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 TransactionTemplate 안에서 TransactionTemplate이 사용되면 어떻게 될까요? 문제가 생길수도 있을 것 같아요. 한 번 확인해보시면 좋을 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 감사합니다. |
||
try { | ||
connection.setAutoCommit(false); | ||
TransactionSynchronizationManager.setActualTransactionActiveTrue(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. connection.setAutoCommit(false)랑 setActualTransactionActiveTrue안에 넣어보는건 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 납득되네요. TransactionSyncronizationManager가 jdbcTemplate 같은 곳에서도 쓰이긴 하니, setAutoCommit 같은 tx의 시작을 의미하는 메서드라인은 txTemlate에 있는게 낫겠네요 |
||
transactionCallback.doInTransaction(connection); | ||
connection.commit(); | ||
} catch (SQLException e) { | ||
rollback(connection); | ||
throw new DataAccessException("transaction 설정에 오류가 발생했습니다."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SQL Chekced Exception을 Unchecked로 바꿔서 Throw해주는 군요. 필수 반영사항은 아닙니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이야 놓쳤네요. 감사합니당. 프린트도 지우고 로그로 바꿨어요. |
||
} finally { | ||
DataSourceUtils.releaseConnection(connection, dataSource); | ||
} | ||
} | ||
|
||
private void rollback(final Connection connection) { | ||
try { | ||
connection.rollback(); | ||
} catch (SQLException e) { | ||
throw new DataAccessException("rollback 에 실패했습니다."); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,19 @@ | ||
package nextstep.jdbc; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.mockito.Mockito; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
|
||
import javax.sql.DataSource; | ||
|
||
class JdbcTemplateTest { | ||
|
||
DataSource dataSource; | ||
JdbcTemplate jdbcTemplate; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jdbcTemplate의 경우에는 진짜 DataSource가 필요해요. 단순히 mock으로는 해결이 안 되죠. 이런 경우에 테스트만을 위한 클래스를 테스트 패키지에서 정의해서 사용하는 방식이 있을 것 같습니다. (이제 테스트를 짜주실 필욘 없습니다. 짜기 위해 어떤 고민을 헀는가가 궁금했어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의존성에 h2를 추가하는 것이 걸렸습니다. 모킹으로 생각해보았는데 DataSource와 Connection 메소드들을 다 만들어준다는 것이 리소스가 너무 많이 드는 작업이라고 생각했어요. |
||
dataSource = Mockito.mock(DataSource.class); | ||
jdbcTemplate = new JdbcTemplate(dataSource); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
트랜잭션 활성화 여부에 따라서, connection을 close 해주는 군요 잘 짜주신 것 같아요,