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단계] 디투(박정훈) 미션 제출합니다. #593

Merged
merged 6 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
9 changes: 5 additions & 4 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.techcourse.domain.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetMapper;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.sql.DataSource;
import java.sql.Connection;
Expand All @@ -23,14 +24,14 @@ public UserDao(final DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public void insert(final Connection connection, final User user) {
public void insert(final User user) {
final String sql = "insert into users (account, password, email) values (?, ?, ?)";
jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail());
jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail());
}

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

public List<User> findAll() {
Expand Down
62 changes: 8 additions & 54 deletions app/src/main/java/com/techcourse/service/UserService.java
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("자원 정리에 실패했습니다.");
}
}
});
}
}
12 changes: 6 additions & 6 deletions app/src/test/java/com/techcourse/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);

Expand All @@ -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);

Expand Down
11 changes: 7 additions & 4 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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";
Expand All @@ -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";
Expand Down
15 changes: 11 additions & 4 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.sql.DataSource;
import java.sql.Connection;
Expand All @@ -28,23 +30,28 @@ public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public int update(final Connection connection, final String sql, final Object... args) {
public int update(final String sql, final Object... args) {
final Connection connection = DataSourceUtils.getConnection(dataSource);
try (final PreparedStatement ps = createPreparedStatement(connection, sql, args)) {
return ps.executeUpdate();
} catch (SQLException e) {
log.error(e.getMessage());
throw new DataAccessException(e);
} finally {
Copy link
Member

Choose a reason for hiding this comment

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

트랜잭션 활성화 여부에 따라서, connection을 close 해주는 군요 잘 짜주신 것 같아요,

if (!TransactionSynchronizationManager.isActualTransactionActive()) {
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
}

public <T> List<T> query(final String sql, final ResultSetMapper<T> resultSetMapper) {
try (final Connection connection = dataSource.getConnection();
final PreparedStatement ps = connection.prepareStatement(sql);
final Connection connection = DataSourceUtils.getConnection(dataSource);
try (final PreparedStatement ps = connection.prepareStatement(sql);
final ResultSet resultSet = ps.executeQuery()
) {
log.debug("query: {}", sql);
final List<T> results = new ArrayList<>();
while(resultSet.next()) {
while (resultSet.next()) {
results.add(resultSetMapper.apply(resultSet));
}
return results;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public static void releaseConnection(Connection connection, DataSource dataSourc
connection.close();
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
} finally {
TransactionSynchronizationManager.unbindResource(dataSource);
}
}
}
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);
Copy link
Member

Choose a reason for hiding this comment

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

만약 TransactionTemplate 안에서 TransactionTemplate이 사용되면 어떻게 될까요?

문제가 생길수도 있을 것 같아요. 한 번 확인해보시면 좋을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다.

try {
connection.setAutoCommit(false);
TransactionSynchronizationManager.setActualTransactionActiveTrue();
Copy link
Member

Choose a reason for hiding this comment

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

connection.setAutoCommit(false)랑 setActualTransactionActiveTrue안에 넣어보는건 어떨까요?
하나로 묶을 수 있는 작업인 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

TransactionSynchronizationManager는 트랜잭션 스레드를 관리하는 객체라고 생각해요. autoCommit을 false로 바꾸는 건 TransactionTemplate의 execute 메소드가 하는 것이 더 어울린다고 생각해서요. connection 관련 작업들을 처리해주고 있어서 외부에서 처리하면 헷갈릴 것 같아요.
또, 정리해주어야하는 자원이기 때문에 최대한 한 파일에서 실행하는 것이 편할 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

The 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 설정에 오류가 발생했습니다.");
Copy link
Member

Choose a reason for hiding this comment

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

SQL Chekced Exception을 Unchecked로 바꿔서 Throw해주는 군요.
이럴 경우에, DataAccessException 에 메시지를 constant 하게 주는 것보다 SQlException e도 같이 넘겨보는건 어때요?

필수 반영사항은 아닙니다.

Copy link
Author

Choose a reason for hiding this comment

The 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 에 실패했습니다.");
}
}
}
14 changes: 14 additions & 0 deletions jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java
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() {
Copy link
Member

Choose a reason for hiding this comment

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

jdbcTemplate의 경우에는 진짜 DataSource가 필요해요.

단순히 mock으로는 해결이 안 되죠.

이런 경우에 테스트만을 위한 클래스를 테스트 패키지에서 정의해서 사용하는 방식이 있을 것 같습니다.

(이제 테스트를 짜주실 필욘 없습니다. 짜기 위해 어떤 고민을 헀는가가 궁금했어요.

Copy link
Author

Choose a reason for hiding this comment

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

의존성에 h2를 추가하는 것이 걸렸습니다. 모킹으로 생각해보았는데 DataSource와 Connection 메소드들을 다 만들어준다는 것이 리소스가 너무 많이 드는 작업이라고 생각했어요.

dataSource = Mockito.mock(DataSource.class);
jdbcTemplate = new JdbcTemplate(dataSource);
}
}
Loading