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 라이브러리 구현 3,4단계] 리오(오영택) 미션 제출합니다. #574

Merged
merged 12 commits into from
Oct 10, 2023
6 changes: 5 additions & 1 deletion app/src/main/java/com/techcourse/service/TxUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.techcourse.domain.User;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.concurrent.atomic.AtomicReference;

public class TxUserService implements UserService {

private final UserService userService;
Expand All @@ -15,7 +17,9 @@ public TxUserService(UserService userService, TransactionTemplate transactionTem

@Override
public User findById(long id) {
Copy link

@somsom13 somsom13 Oct 9, 2023

Choose a reason for hiding this comment

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

findById는 TransactionTemplate 으로 실행시키지 않으신 이유가 궁금해요~!!

AppUserService의 changePassword 내에서 findById를 호출하는 걸 보면, findById도 트랜잭션 내에서 실행시켜줘야 하지 않을까 싶은데 리오는 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

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

그렇네요 제가 readonly=False의 느낌으로 트랜잭션을 설정했었는데, 바론 말대로 한 트랜잭션 내에서 진행이 되어야 겠군요... 감사합니다!

return userService.findById(id);
AtomicReference<User> user = new AtomicReference<>();
transactionTemplate.execute(() -> user.set(userService.findById(id)));
return user.get();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public static void execute(DataSource dataSource) {
URL url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql");
File file = new File(url.getFile());
String sql = Files.readString(file.toPath());

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.execute(sql);
} catch (IOException | NullPointerException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class AppUserServiceTest {
void setUp() {
this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance());
this.userDao = new UserDao(jdbcTemplate);

DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());
userDao.deleteAll();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.UserHistory;
import org.springframework.dao.DataAccessException;
import org.springframework.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;

public class MockUserHistoryDao extends UserHistoryDao {
Expand All @@ -13,6 +13,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) {

@Override
public void log(final UserHistory userHistory) {
System.out.println("asdf");

Choose a reason for hiding this comment

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

앗 ㅎㅎ 🙈

Copy link
Author

Choose a reason for hiding this comment

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

앗! 이대로 머지가 되다니... 슬프군요... 죄송합니다....

Choose a reason for hiding this comment

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

앗.. 이럴 수가.. DM으로 의견 여쭤보고 머지할걸 그랬네용.. 저도 죄송합니다 🥲

throw new DataAccessException();
}
}
10 changes: 6 additions & 4 deletions app/src/test/java/com/techcourse/service/TxUserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;

Expand All @@ -25,13 +25,15 @@ class TxUserServiceTest {
@BeforeEach
void setUp() {
DataSource dataSource = DataSourceConfig.getInstance();

DatabasePopulatorUtils.execute(dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.userDao = new UserDao(jdbcTemplate);
this.transactionTemplate = new TransactionTemplate(dataSource);

DatabasePopulatorUtils.execute(dataSource);
userDao.deleteAll();


final var user = new User("gugu", "password", "[email protected]");
userDao.insert(user);
this.user = userDao.findByAccount("gugu");
Expand All @@ -46,8 +48,8 @@ void testTransactionRollback() {
final var newPassword = "newPassword";
final var createBy = "gugu";

assertThrows(CannotGetJdbcConnectionException.class,
() -> userService.changePassword(1L, newPassword, createBy));
assertThrows(DataAccessException.class,
() -> userService.changePassword(user.getId(), newPassword, createBy));

final var actual = userService.findById(user.getId());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.springframework.dao;
package org.springframework;

public class DataAccessException extends RuntimeException {

Expand Down
42 changes: 23 additions & 19 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.DataAccessException;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
Expand All @@ -25,25 +22,27 @@ public JdbcTemplate(DataSource dataSource) {
}

public void update(String sql, Object... values) {
try (
Connection connection = dataSource.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql);
) {
log.debug("query : {}", sql);
Connection connection = DataSourceUtils.getConnection(dataSource);
Jaeyoung22 marked this conversation as resolved.
Show resolved Hide resolved

try {
PreparedStatement pstmt = connection.prepareStatement(sql);
Copy link

@somsom13 somsom13 Oct 10, 2023

Choose a reason for hiding this comment

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

PreparedStatement는 Connection에 의해 자동으로 닫히는 부분이 아닌 것으로 알고 있어요!

디버깅을 찍어보았을 때,

  • try-with-resources에 connection.prepareStatement(sql) 을 넣었을 경우: JdbcPreparedStatementclose() 호출
  • 일반 try-catch에 connection.prepareStatement를 넣었을 경우: JdbcPreparedStatementclose() 호출되지 않음

인 것 같아요!! PreparedStatement는 이전 코드처럼 닫아주면 어떨까요?.?

Copy link
Author

Choose a reason for hiding this comment

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

앗 그렇군요...
https://www.phind.com/search?cache=h8csh4x43gnfdyz497l20af4
이것만 보고 PreparedStatement도 알아서 닫히겠거니 했는데 진짜 하나하나 확인해야겠네요...
감사합니다!

Choose a reason for hiding this comment

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

헉 어쩌면 제가 디버깅을 잘못한 걸 수도...! 요건 저도 더 확인해볼게요!!
좋은 참고자료 감사합니다 🙇‍♀️

log.debug("query : {}", sql);
setValues(pstmt, values);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseConnection(connection);
}
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... values) {
try (
Connection connection = dataSource.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql);
) {
Connection connection = DataSourceUtils.getConnection(dataSource);

try {
PreparedStatement pstmt = connection.prepareStatement(sql);

log.debug("query : {}", sql);

setValues(pstmt, values);
Expand All @@ -57,6 +56,8 @@ public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... values) {
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseConnection(connection);
}
}

Expand All @@ -74,16 +75,19 @@ public <T> Optional<T> queryForObject(String sql, RowMapper<T> rowMapper, Object
}

public void execute(String sql) {
try (
Connection connection = dataSource.getConnection();
Statement stmt = connection.createStatement();
) {
Connection connection = DataSourceUtils.getConnection(dataSource);

try {
Statement stmt = connection.createStatement();

log.debug("query : {}", sql);

stmt.execute(sql);
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseConnection(connection);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,29 @@ public abstract class DataSourceUtils {

private DataSourceUtils() {}

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
Connection connection = TransactionSynchronizationManager.getConnection(dataSource);
if (connection != null) {
return connection;
public static Connection getConnection(DataSource dataSource) {
if (TransactionSynchronizationManager.isInTransaction()) {
return TransactionSynchronizationManager.getConnection(dataSource);
}

try {
connection = dataSource.getConnection();
TransactionSynchronizationManager.bindConnection(dataSource, connection);
return connection;
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
return dataSource.getConnection();
} catch (SQLException e) {
throw new CannotGetJdbcConnectionException(e.getMessage());
}
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
public static void releaseConnection(Connection connection) {
try {
connection.close();
} catch (SQLException ex) {
releaseIfNotInTransaction(connection);
} catch (SQLException e) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
} finally {
TransactionSynchronizationManager.unbindConnection(dataSource);
}
}

private static void releaseIfNotInTransaction(Connection connection) throws SQLException {
if (!TransactionSynchronizationManager.isInTransaction()) {
connection.close();
}
}
Jaeyoung22 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.springframework.transaction.support;

import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.DataAccessException;

import javax.sql.DataSource;
import java.sql.Connection;
Expand All @@ -10,13 +10,14 @@
public abstract class TransactionSynchronizationManager {

private static final ThreadLocal<Map<DataSource, Connection>> resources = new ThreadLocal<>();
private static final ThreadLocal<Boolean> isInTransaction = ThreadLocal.withInitial(() -> false);
Jaeyoung22 marked this conversation as resolved.
Show resolved Hide resolved

private TransactionSynchronizationManager() {}

public static Connection getConnection(DataSource dataSource) {
Map<DataSource, Connection> dataSourceAndConnection = resources.get();
if (dataSourceAndConnection == null) {
throw new CannotGetJdbcConnectionException("DataSource에 binding된 Connection이 없습니다.");
if (dataSourceAndConnection == null || dataSourceAndConnection.get(dataSource) == null) {
throw new DataAccessException("DataSource에 binding된 Connection이 없습니다.");
}

return dataSourceAndConnection.get(dataSource);
Expand All @@ -31,14 +32,33 @@ public static void bindConnection(DataSource dataSource, Connection connection)
resources.get().put(dataSource, connection);
}

public static Connection unbindConnection(DataSource dataSource) {
public static Connection unbindConnection(Connection connection, DataSource dataSource) {
if (resources.get() == null || !resources.get().containsKey(dataSource)) {
throw new IllegalArgumentException("등록된 DataSource가 없습니다.");
}

Map<DataSource, Connection> resource = resources.get();
Connection connection = resource.get(dataSource);
Connection bindedConnection = resource.get(dataSource);
validateConnection(connection, bindedConnection);
resources.remove();
return connection;
}

private static void validateConnection(Connection connection, Connection bindedConnection) {
if (connection != bindedConnection) {
throw new IllegalArgumentException("잘못된 Connection입니다.");
}
}

public static boolean isInTransaction() {
return isInTransaction.get();
}

public static void setInTransaction(boolean inTransaction) {
if (inTransaction && isInTransaction()) {
throw new IllegalStateException("이미 트랜잭션이 진행중입니다.");
}
isInTransaction.remove();
isInTransaction.set(inTransaction);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.springframework.transaction.support;

import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.DataAccessException;

import javax.sql.DataSource;
import java.sql.Connection;
Expand All @@ -16,23 +15,23 @@ public TransactionTemplate(DataSource dataSource) {
}

public void execute(Runnable runnable) {
Copy link

Choose a reason for hiding this comment

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

리오 코드를 받아서 확인해보았는데, JdbcTemplate 에서는 아예 새로운 커넥션을 생성해서 사용하고 있는 것 같아요 ㅠㅠ

같은 트랜잭션 내에서 실행되기 위해서는 같은 커넥션을 공유해야 하는데, 콘솔에 찍어본 결과 서로 다른 Connection을 사용하고 있는 것 같더라구요!

JdbcTemplate 에서도 DataSourceUtils.getConnection() 을 사용하도록 변경하면 같은 스레드의 같은 트랜잭션에서는 동일한 Connection을 사용하도록 수정할 수 있을 것 같은데 어떻게 생각하시나용?

image

Copy link
Author

Choose a reason for hiding this comment

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

허허 이번 미션을 너무 쉽게 생각했군요....
커넥션 때문에 하루종일 머리 좀 썼습니다...
바론 덕분에 더 깊게 생각할 수 있었네요...! 고맙습니다!

Choose a reason for hiding this comment

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

멋지게 수정하셨군요 👍 👍 최곱니다~~~~~

Copy link

@somsom13 somsom13 Oct 10, 2023

Choose a reason for hiding this comment

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

중요하지는 않은 내용이지만, release를 finally 블럭에서 처리하도록 합칠 수 있지 않을까 싶기도 하네용!

리오 :
여기 왜 댓글이 안달릴까요...?
저도 이 부분 고민했었는데, 뭔가 트랜잭션 관련한 메서드에서 release를 명시하고 싶지 않아서 각 메서드에 따로따로 넣어주는 방법으로 진행했습니다! 바론의 의견도 좋은 방법이라고 생각해요!

Connection connection = DataSourceUtils.getConnection(dataSource);
begin(connection);
Connection connection = begin();
try {
runnable.run();

commit(connection);
} catch (Exception e) {
rollback(connection);
} finally {
DataSourceUtils.releaseConnection(connection, dataSource);
throw new DataAccessException(e);
}
}


private void begin(Connection connection) {
private Connection begin() {
try {
TransactionSynchronizationManager.setInTransaction(true);
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
TransactionSynchronizationManager.bindConnection(dataSource, connection);
return connection;
} catch (SQLException e) {
throw new DataAccessException(e);
}
Expand All @@ -41,6 +40,7 @@ private void begin(Connection connection) {
private void commit(Connection connection) {
try {
connection.commit();
release(connection);
} catch (SQLException e) {
throw new DataAccessException(e);
}
Expand All @@ -49,6 +49,17 @@ private void commit(Connection connection) {
private void rollback(Connection connection) {
try {
connection.rollback();
release(connection);
} catch (SQLException e) {
throw new DataAccessException(e);
}
}

private void release(Connection connection) {
try {
TransactionSynchronizationManager.setInTransaction(false);
Connection unbindedConnection = TransactionSynchronizationManager.unbindConnection(connection, dataSource);
unbindedConnection.close();
} catch (SQLException e) {
throw new DataAccessException(e);
}
Expand Down
Loading
Loading