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단계] 다즐(최우창) 미션 제출합니다. #566

Merged
merged 4 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 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,35 @@
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 final UserDao userDao;
private final UserHistoryDao userHistoryDao;

public AppUserService(UserDao userDao, UserHistoryDao userHistoryDao) {
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}

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

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

@Override
public void changePassword(long id, String newPassword, String createBy) {
var user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
36 changes: 36 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,36 @@
package com.techcourse.service;

import com.techcourse.domain.User;
import org.springframework.transaction.support.TransactionTemplate;

public class TxUserService implements UserService {

private final UserService userService;
private final TransactionTemplate transactionTemplate;

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

@Override
public User findById(long id) {
return transactionTemplate.execute(() -> userService.findById(id));
}

@Override
public void insert(User user) {
transactionTemplate.execute(() -> {
userService.insert(user);
return null;
});
Comment on lines +23 to +26
Copy link
Member

Choose a reason for hiding this comment

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

return값이 없는 경우를 위한 TransactionTemplate 메서드를 만들면 어떨까요?!

Copy link
Member Author

Choose a reason for hiding this comment

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

만들지 말지 고민했었는데 꼼꼼하게 집어주셨네요 ㅎㅎ

말씀해주신 것처럼 return 값이 필요로 하지 않을 때 사용할 수 있는 메서드가 있으면 좋을 것 같아요 👍

}

@Override
public void changePassword(long id, String newPassword, String createBy) {
transactionTemplate.execute(() -> {
userService.changePassword(id, newPassword, createBy);
return null;
});
}
}
34 changes: 4 additions & 30 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,38 +1,12 @@
package com.techcourse.service;

import com.techcourse.dao.UserDao;
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.domain.UserHistory;
import org.springframework.transaction.support.TransactionTemplate;

public class UserService {
public interface UserService {

private final UserDao userDao;
private final UserHistoryDao userHistoryDao;
private final TransactionTemplate transactionTemplate;
User findById(long id);

public UserService(UserDao userDao, UserHistoryDao userHistoryDao, TransactionTemplate transactionTemplate) {
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
this.transactionTemplate = transactionTemplate;
}
void insert(User user);

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

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

public void changePassword(long id, String newPassword, String createBy) {
transactionTemplate.execute(() -> {
var user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
return null;
});
}
void changePassword(long id, String newPassword, String createBy);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;

class UserServiceTest {
class AppUserServiceTest {

private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
Expand All @@ -34,7 +34,7 @@ void setUp() {
@Test
void testChangePassword() {
var userHistoryDao = new UserHistoryDao(jdbcTemplate);
var userService = new UserService(userDao, userHistoryDao, transactionTemplate);
var userService = new AppUserService(userDao, userHistoryDao);

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

var newPassword = "newPassword";
var createBy = "gugu";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.support.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class JdbcTemplate {

private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class);

private final DataSourceTransactionManager transactionManager;
private final DataSource dataSource;

public JdbcTemplate(DataSource dataSource) {
this.transactionManager = new DataSourceTransactionManager(dataSource);
this.dataSource = dataSource;
}

private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) {
var con = transactionManager.getConnection();
var con = DataSourceUtils.getConnection(dataSource);
try (var ps = psc.createPreparedStatement(con)) {
return action.doInPreparedStatement(ps);
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
transactionManager.release(con);
DataSourceUtils.releaseConnection(con, dataSource);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package org.springframework.jdbc.datasource;

import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.transaction.support.TransactionSynchronizationManager;

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

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

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
Connection connection = TransactionSynchronizationManager.getResource(dataSource);
var connection = TransactionSynchronizationManager.getResource(dataSource);
if (connection != null) {
return connection;
}
Expand All @@ -28,8 +28,13 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
return;
}
Comment on lines +31 to +33
Copy link
Member

Choose a reason for hiding this comment

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

외부에서 setActualTransactionActive값을 false로 변경해주어야만 커넥션 자원이 반환되는군요. 이렇게 구현하신 이유가 궁금합니다! releaseConnectinon을 호출하면 바로 자원을 반환하도록 하는 것과 어떤 차이가 있나요?! 당장에 잘 떠오르지가 않네요 ㅎㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

우선 트랜잭션이 살아있는동안은 커넥션을 공유하고 있는 상태라고 판단했어요 🤓

TransactionTemplate만으로 데이터베이스와의 모든 상황을 처리한다며 해당 요소를 판단하지 않아도 될 것 같아요. 항상 TransactionManager에 의해 커넥션 자원이 회수되기 때문입니다.

하지만 단순히 JdbcTemplate를 사용하는 경우에는 메서드마다 커넥션을 반환해주는 작업을 수행합니다. 이때 트랜잭션 범위에 있다면 커넥션을 회수하지 않아야 다음 SQL에서도 같은 커넥션을 공유할 수 있게 됩니다. 이를 판단하지 않고 JdbcTemplate에서 커넥션을 회수해버린다면 TransactionManager에서 커넥션을 다 사용하고 close할 때 이미 close된 커넥션이라는 예외 메시지를 만나게 됩니다.

따라서 트랜잭션이 존재하는 동안은 TransactionManager를 통해 connection 자원이 회수되어야 하기 때문에 커넥션 회수 작업을 수행하지 않도록 하였습니다 !


try {
connection.close();
TransactionSynchronizationManager.unbindResource(dataSource);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,48 @@
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class DataSourceTransactionManager {

private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

private final DataSource dataSource;

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

public void startTransaction() {
public void doGetTransaction() {
try {
var connection = getConnection();
var connection = DataSourceUtils.getConnection(dataSource);
connection.setAutoCommit(false);
connectionHolder.set(connection);
TransactionSynchronizationManager.setActualTransactionActive(true);
} catch (SQLException e) {
throw new RuntimeException("트랜잭션 시작 실패", e);
}
}

public Connection getConnection() {
var connection = connectionHolder.get();
if (connection != null) {
return connection;
}

try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException("커넥션 획득 실패", e);
}
}

public void commit() {
public void doCommit() {
try {
var connection = getConnection();
var connection = DataSourceUtils.getConnection(dataSource);
connection.commit();
close(connection, true);
release(connection);
} catch (SQLException e) {
throw new RuntimeException("커밋 실패", e);
}
}

public void rollback() {
public void doRollback() {
try {
var connection = getConnection();
var connection = DataSourceUtils.getConnection(dataSource);
connection.rollback();
close(connection, true);
release(connection);
} catch (SQLException e) {
throw new RuntimeException("롤백 실패", e);
}
}

public void release(Connection connection) {
if (connectionHolder.get() != connection) {
close(connection, false);
}
}

private void close(Connection connection, boolean clear) {
if (clear) {
connectionHolder.remove();
}

try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException("커넥션 종료 실패", e);
}
private void release(Connection connection) {
TransactionSynchronizationManager.setActualTransactionActive(false);
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
package org.springframework.transaction.support;

import javax.sql.DataSource;
import static java.lang.ThreadLocal.withInitial;

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 = withInitial(HashMap::new);
private static final ThreadLocal<Boolean> actualTransactionActive = withInitial(() -> false);
Copy link
Member

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.

저도 이번에 ThreadLocal을 적용해 보면서 알게 되었습니다 :)


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

public static Connection getResource(DataSource key) {
return null;
return getResources().get(key);
}

public static void bindResource(DataSource key, Connection value) {
getResources().put(key, value);
}

public static Connection unbindResource(DataSource key) {
return null;
return getResources().remove(key);
}

private static Map<DataSource, Connection> getResources() {
return resources.get();
}

public static boolean isActualTransactionActive() {
return actualTransactionActive.get();
}

public static void setActualTransactionActive(boolean active) {
actualTransactionActive.set(active);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public TransactionTemplate(DataSource dataSource) {

public <T> T execute(TransactionCallBack<T> action) {
try {
transactionManager.startTransaction();
transactionManager.doGetTransaction();
T result = action.doInTransaction();
transactionManager.commit();
transactionManager.doCommit();
return result;
} catch (RuntimeException | Error e) {
Copy link
Member

Choose a reason for hiding this comment

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

확실히 Error인 상황에도 롤백이 될 수 있도록 처리하면 좋겠네요! Error를 catch하여 뭔가 의미 있는 행동을 하는 케이스를 여기서 처음 본 것 같네요 ㅋㅋㅋㅋ

Copy link
Member Author

Choose a reason for hiding this comment

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

스프링에서도 Error인 상황에서는 롤백되도록 처리해주고 있더라구요 😀

프레임워크를 학습하며 최대한 따라갈 수 있도록 구현해보았습니다 !

transactionManager.rollback();
transactionManager.doRollback();
throw e;
}
}
Expand Down
Loading