Skip to content

Commit cc41b22

Browse files
authored
[3단계 - Transaction 적용하기] 링크(손준형) 미션 제출합니다. (#1158)
* feat: implement TransactionSynchronizationManager * feat: integrate DataSourceUtils with JdbcTemplate * refactor: migrate UserHistoryDao to JdbcTemplate * feat: apply transaction to UserService.changePassword() * test: enable UserServiceTest and verify transaction
1 parent 7dc4804 commit cc41b22

File tree

6 files changed

+98
-59
lines changed

6 files changed

+98
-59
lines changed

app/src/main/java/com/techcourse/dao/UserHistoryDao.java

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,31 @@
22

33
import com.techcourse.domain.UserHistory;
44
import com.interface21.jdbc.core.JdbcTemplate;
5-
import org.slf4j.Logger;
6-
import org.slf4j.LoggerFactory;
75

86
import javax.sql.DataSource;
9-
import java.sql.Connection;
10-
import java.sql.PreparedStatement;
11-
import java.sql.SQLException;
127

138
public class UserHistoryDao {
149

15-
private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class);
16-
17-
private final DataSource dataSource;
10+
private final JdbcTemplate jdbcTemplate;
1811

1912
public UserHistoryDao(final DataSource dataSource) {
20-
this.dataSource = dataSource;
13+
this.jdbcTemplate = new JdbcTemplate(dataSource);
2114
}
2215

2316
public UserHistoryDao(final JdbcTemplate jdbcTemplate) {
24-
this.dataSource = null;
17+
this.jdbcTemplate = jdbcTemplate;
2518
}
2619

2720
public void log(final UserHistory userHistory) {
2821
final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)";
2922

30-
Connection conn = null;
31-
PreparedStatement pstmt = null;
32-
try {
33-
conn = dataSource.getConnection();
34-
pstmt = conn.prepareStatement(sql);
35-
36-
log.debug("query : {}", sql);
37-
38-
pstmt.setLong(1, userHistory.getUserId());
39-
pstmt.setString(2, userHistory.getAccount());
40-
pstmt.setString(3, userHistory.getPassword());
41-
pstmt.setString(4, userHistory.getEmail());
42-
pstmt.setObject(5, userHistory.getCreatedAt());
43-
pstmt.setString(6, userHistory.getCreateBy());
44-
pstmt.executeUpdate();
45-
} catch (SQLException e) {
46-
log.error(e.getMessage(), e);
47-
throw new RuntimeException(e);
48-
} finally {
49-
try {
50-
if (pstmt != null) {
51-
pstmt.close();
52-
}
53-
} catch (SQLException ignored) {}
54-
55-
try {
56-
if (conn != null) {
57-
conn.close();
58-
}
59-
} catch (SQLException ignored) {}
60-
}
23+
jdbcTemplate.update(sql,
24+
userHistory.getUserId(),
25+
userHistory.getAccount(),
26+
userHistory.getPassword(),
27+
userHistory.getEmail(),
28+
userHistory.getCreatedAt(),
29+
userHistory.getCreateBy()
30+
);
6131
}
6232
}

app/src/main/java/com/techcourse/service/UserService.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,30 @@
44
import com.techcourse.dao.UserHistoryDao;
55
import com.techcourse.domain.User;
66
import com.techcourse.domain.UserHistory;
7+
import com.interface21.dao.DataAccessException;
8+
import com.interface21.jdbc.datasource.DataSourceUtils;
9+
import com.interface21.transaction.support.TransactionSynchronizationManager;
10+
11+
import javax.sql.DataSource;
12+
import java.sql.Connection;
13+
import java.sql.SQLException;
714

815
public class UserService {
916

1017
private final UserDao userDao;
1118
private final UserHistoryDao userHistoryDao;
19+
private final DataSource dataSource;
1220

1321
public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) {
1422
this.userDao = userDao;
1523
this.userHistoryDao = userHistoryDao;
24+
this.dataSource = null;
25+
}
26+
27+
public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao, final DataSource dataSource) {
28+
this.userDao = userDao;
29+
this.userHistoryDao = userHistoryDao;
30+
this.dataSource = dataSource;
1631
}
1732

1833
public User findById(final long id) {
@@ -24,6 +39,34 @@ public void insert(final User user) {
2439
}
2540

2641
public void changePassword(final long id, final String newPassword, final String createBy) {
42+
if (dataSource == null) {
43+
// 트랜잭션 없이 실행
44+
executeChangePassword(id, newPassword, createBy);
45+
return;
46+
}
47+
48+
// 트랜잭션과 함께 실행
49+
final Connection connection = DataSourceUtils.getConnection(dataSource);
50+
try {
51+
connection.setAutoCommit(false);
52+
53+
executeChangePassword(id, newPassword, createBy);
54+
55+
connection.commit();
56+
} catch (Exception e) {
57+
try {
58+
connection.rollback();
59+
} catch (SQLException rollbackException) {
60+
throw new DataAccessException("Failed to rollback transaction", rollbackException);
61+
}
62+
throw new DataAccessException(e);
63+
} finally {
64+
TransactionSynchronizationManager.unbindResource(dataSource);
65+
DataSourceUtils.releaseConnection(connection, dataSource);
66+
}
67+
}
68+
69+
private void executeChangePassword(final long id, final String newPassword, final String createBy) {
2770
final var user = findById(id);
2871
user.changePassword(newPassword);
2972
userDao.update(user);

app/src/test/java/com/techcourse/service/UserServiceTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
import com.interface21.dao.DataAccessException;
99
import com.interface21.jdbc.core.JdbcTemplate;
1010
import org.junit.jupiter.api.BeforeEach;
11-
import org.junit.jupiter.api.Disabled;
1211
import org.junit.jupiter.api.Test;
1312

1413
import static org.assertj.core.api.Assertions.assertThat;
1514
import static org.junit.jupiter.api.Assertions.assertThrows;
1615

17-
@Disabled
1816
class UserServiceTest {
1917

2018
private JdbcTemplate jdbcTemplate;
@@ -33,7 +31,7 @@ void setUp() {
3331
@Test
3432
void testChangePassword() {
3533
final var userHistoryDao = new UserHistoryDao(jdbcTemplate);
36-
final var userService = new UserService(userDao, userHistoryDao);
34+
final var userService = new UserService(userDao, userHistoryDao, DataSourceConfig.getInstance());
3735

3836
final var newPassword = "qqqqq";
3937
final var createBy = "gugu";
@@ -48,7 +46,7 @@ void testChangePassword() {
4846
void testTransactionRollback() {
4947
// 트랜잭션 롤백 테스트를 위해 mock으로 교체
5048
final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate);
51-
final var userService = new UserService(userDao, userHistoryDao);
49+
final var userService = new UserService(userDao, userHistoryDao, DataSourceConfig.getInstance());
5250

5351
final var newPassword = "newPassword";
5452
final var createBy = "gugu";

jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,25 @@ public JdbcTemplate(final DataSource dataSource) {
3434
/**
3535
* SQL 실행의 핵심 메서드. 자원 관리 관련 중복 부분을 함수형 인터페이스를 이용하여 분리함.
3636
* - Connection, PreparedStatement를 생성하고, 파라미터를 바인딩한 후 StatementExecutor를 통해 실행한다.
37-
* - 자원 해제를 try-with-resources로 자동 처리한다.
37+
* - Connection은 트랜잭션 컨텍스트를 고려하여 DataSourceUtils를 통해 관리
38+
* - PreparedStatement는 자동으로 close
3839
*
3940
* @param sql 실행할 SQL문
4041
* @param args PreparedStatement에 바인딩할 파라미터 배열
4142
* @param executor PreparedStatement를 실행하는 로직을 가진 함수형 인터페이스
4243
* @return 제네릭 타입 결과값 (쿼리 결과나 update 결과 등)
4344
*/
4445
public <R> R execute(String sql, Object[] args, StatementExecutor<R> executor) {
45-
try (
46-
Connection conn = getConnection();
47-
PreparedStatement pstmt = conn.prepareStatement(sql)
48-
) {
46+
final Connection conn = getConnection();
47+
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
4948
setPreparedStatementParameter(args, pstmt);
5049
log.info("query = {}", sql);
5150

5251
return executor.execute(pstmt);
5352
} catch (SQLException e) {
5453
throw new DataAccessException("sql 실행 과정에서 문제가 발생하였습니다.", e);
54+
} finally {
55+
com.interface21.jdbc.datasource.DataSourceUtils.releaseConnection(conn, dataSource);
5556
}
5657
}
5758

@@ -133,16 +134,13 @@ private <T> List<T> getQueryResult(RowMapper<T> rowMapper, PreparedStatement pst
133134
}
134135

135136
/**
136-
* DataSource에서 새로운 데이터베이스 Connection 객체 획득
137+
* DataSource에서 데이터베이스 Connection 객체 획득
138+
* DataSourceUtils를 사용하여 트랜잭션 컨텍스트에서 관리되는 Connection을 재사용
137139
*
138140
* @return 데이터베이스에 연결된 Connection 객체
139-
* @throws DataAccessException 커넥션 획득 과정에서 SQL 오류가 발생한 경우
141+
* @throws DataAccessException 커넥션 획득 과정에서 오류가 발생한 경우
140142
*/
141143
private Connection getConnection() {
142-
try {
143-
return dataSource.getConnection();
144-
} catch (SQLException e) {
145-
throw new DataAccessException(e);
146-
}
144+
return com.interface21.jdbc.datasource.DataSourceUtils.getConnection(dataSource);
147145
}
148146
}

jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
2828
}
2929

3030
public static void releaseConnection(Connection connection, DataSource dataSource) {
31+
if (connection == null) {
32+
return;
33+
}
34+
35+
// 트랜잭션 컨텍스트에서 관리되는 Connection인 경우 닫지 않음
36+
Connection boundConnection = TransactionSynchronizationManager.getResource(dataSource);
37+
if (boundConnection == connection) {
38+
return;
39+
}
40+
41+
// 트랜잭션 컨텍스트에 없는 Connection만 닫음
3142
try {
3243
connection.close();
3344
} catch (SQLException ex) {

jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import javax.sql.DataSource;
44
import java.sql.Connection;
5+
import java.util.HashMap;
56
import java.util.Map;
67

78
public abstract class TransactionSynchronizationManager {
@@ -11,13 +12,31 @@ public abstract class TransactionSynchronizationManager {
1112
private TransactionSynchronizationManager() {}
1213

1314
public static Connection getResource(DataSource key) {
14-
return null;
15+
final Map<DataSource, Connection> map = resources.get();
16+
if (map == null) {
17+
return null;
18+
}
19+
return map.get(key);
1520
}
1621

1722
public static void bindResource(DataSource key, Connection value) {
23+
Map<DataSource, Connection> map = resources.get();
24+
if (map == null) {
25+
map = new HashMap<>();
26+
resources.set(map);
27+
}
28+
map.put(key, value);
1829
}
1930

2031
public static Connection unbindResource(DataSource key) {
21-
return null;
32+
final Map<DataSource, Connection> map = resources.get();
33+
if (map == null) {
34+
return null;
35+
}
36+
final Connection connection = map.remove(key);
37+
if (map.isEmpty()) {
38+
resources.remove();
39+
}
40+
return connection;
2241
}
2342
}

0 commit comments

Comments
 (0)