Skip to content

Commit

Permalink
[JDBC 라이브러리 구현하기 - 4단계] 에단(김석호) 미션 제출합니다. (#556)
Browse files Browse the repository at this point in the history
* docs: 기능구현요구사항 작성

* refactor: 트랜잭션 경계 설정

* test: 격리 레벨 테스트 추가

* test: 트랜잭션 전파 테스트 추가

* docs: 기능 요구 사항 작성

* refactor: Transaction synchronization 적용

* refactor: 트랜잭션 서비스 추상화

* refactor: 쓰레드 풀 사용시 안전하게 쓰레드 로컬을 사용하도록 변경

* refactor: 트랜잭션 로직을 TransactioTemplate으로 분리

* refactor: 사용자 입력값 예외 검증 메세지 변경 및 예외 종류 변경

* refactor: DataSource를 외부에서 주입받도록 변경

* refactor: rollback 메서드를 1 depth로 변경

* refactor: findById에서도 트랜잭션이 적용되도록 변경

* refactor: TransactionTemplate 관련 메소드 springframework 패키지로 이동

* test: 학습테스트 Isolation Level 범위 최소화

* test: 학습테스트 stage2 주석추가
  • Loading branch information
cookienc authored Oct 13, 2023
1 parent 245ac73 commit d26689d
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 186 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
- [x] update()
- [x] JdbcTemplate 으로 중복 제거
- [x] 트랜잭션 경계 설정하기
- [x] Transaction synchronization 적용하기
- [x] 트랜잭션 서비스 추상화하기
7 changes: 0 additions & 7 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.Connection;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -33,12 +32,6 @@ public void update(final User user) {
jdbcTemplate.execute(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
}

public void update(final Connection connection, final User user) {
final var sql = "update users set (account, password, email) = (?, ?, ?) where id = ?";

jdbcTemplate.executeWithConnection(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
}

List<User> findAll() {
final var sql = "select id, account, password, email from users";

Expand Down
16 changes: 0 additions & 16 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import com.techcourse.domain.UserHistory;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;

public class UserHistoryDao {

private final JdbcTemplate jdbcTemplate;
Expand All @@ -25,18 +23,4 @@ public void log(final UserHistory userHistory) {
userHistory.getCreateBy()
);
}

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

jdbcTemplate.executeWithConnection(connection,
sql,
userHistory.getUserId(),
userHistory.getAccount(),
userHistory.getPassword(),
userHistory.getEmail(),
userHistory.getCreatedAt(),
userHistory.getCreateBy()
);
}
}
37 changes: 37 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,37 @@
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(final UserDao userDao, final UserHistoryDao userHistoryDao) {
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}

@Override
public User findById(final long id) {
return userDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException(String.format("id 값으로 해당하는 User 를 찾을 수 없습니다. 입력값 : %s", id)));
}

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

@Override
public void changePassword(final long id, final String newPassword, final String createBy) {
final var user = findById(id);
user.changePassword(newPassword);

userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
30 changes: 30 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,30 @@
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(final UserService userService, final TransactionTemplate transactionTemplate) {
this.userService = userService;
this.transactionTemplate = transactionTemplate;
}

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

@Override
public void insert(final User user) {
transactionTemplate.executeWithTransaction(() -> userService.insert(user));
}

@Override
public void changePassword(final long id, final String newPassword, final String createBy) {
transactionTemplate.executeWithTransaction(() -> userService.changePassword(id, newPassword, createBy));
}
}
79 changes: 4 additions & 75 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,83 +1,12 @@
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 javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.NoSuchElementException;
public interface UserService {

public class UserService {
User findById(final long id);

private static final Logger log = LoggerFactory.getLogger(UserService.class);
void insert(final User user);

private final UserDao userDao;
private final UserHistoryDao userHistoryDao;

UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) {
this.userDao = userDao;
this.userHistoryDao = userHistoryDao;
}

User findById(final long id) {
return userDao.findById(id)
.orElseThrow(() -> new NoSuchElementException("id 값으로 해당하는 User 를 찾을 수 없습니다."));
}

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

void changePassword(final long id, final String newPassword, final String createBy) {
final var user = findById(id);
user.changePassword(newPassword);

Connection connection = null;
try {
final DataSource dataSource = DataSourceConfig.getInstance();
connection = dataSource.getConnection();
connection.setAutoCommit(false);

userDao.update(connection, user);
userHistoryDao.log(connection, new UserHistory(user, createBy));

connection.commit();
} catch (final SQLException | DataAccessException e) {
rollback(connection);
log.error("SQLException occurred");
throw new DataAccessException(e);
} finally {
release(connection);
}
}

private void rollback(final Connection connection) {
if (connection != null) {
try {
connection.rollback();
} catch (final SQLException ex) {
log.error("rollback callback");
throw new DataAccessException(ex);
}
}
}

private void release(final Connection connection) {
if (connection != null) {
try {
connection.setAutoCommit(true);
connection.close();
} catch (final SQLException e) {
log.error("Cannot close Connection");
throw new DataAccessException(e);
}
}
}
void changePassword(final long id, final String newPassword, final String createBy);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;

public class MockUserHistoryDao extends UserHistoryDao {

MockUserHistoryDao(final JdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}

@Override
public void log(final Connection connection, final UserHistory userHistory) {
public void log(final UserHistory userHistory) {
throw new DataAccessException();
}
}
8 changes: 6 additions & 2 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand All @@ -33,7 +34,7 @@ void setUp() {

@Test
void testChangePassword() {
final var userService = new UserService(userDao, userHistoryDao);
final var userService = new AppUserService(userDao, userHistoryDao);

final var newPassword = "qqqqq";
final var createBy = "gugu";
Expand All @@ -47,7 +48,10 @@ void testChangePassword() {
@Test
void testTransactionRollback() {
// 트랜잭션 롤백 테스트를 위해 mock으로 교체
final var userService = new UserService(userDao, mockUserHistoryDao);
// 애플리케이션 서비스
final var appUserService = new AppUserService(userDao, mockUserHistoryDao);
// 트랜잭션 서비스 추상화
final var userService = new TxUserService(appUserService, new TransactionTemplate(DataSourceConfig.getInstance()));

final var newPassword = "newPassword";
final var createBy = "gugu";
Expand Down
23 changes: 4 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 @@ -3,6 +3,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.Connection;
Expand All @@ -24,8 +25,8 @@ public JdbcTemplate(final DataSource dataSource) {
}

public void execute(final String sql, final Object... params) {
final Connection conn = DataSourceUtils.getConnection(dataSource);
try (
final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = conn.prepareStatement(sql)
) {
log.debug("query : {}", sql);
Expand All @@ -34,27 +35,13 @@ public void execute(final String sql, final Object... params) {

pstmt.executeUpdate();
} catch (final SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}

public void executeWithConnection(final Connection conn, final String sql, final Object... params) {
try (final PreparedStatement pstmt = conn.prepareStatement(sql)) {
log.debug("query : {}", sql);

setCondition(params, pstmt);

pstmt.executeUpdate();
} catch (final SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}

public <T> List<T> query(final String sql, final RowMapper<T> rowMapper) {
final Connection conn = DataSourceUtils.getConnection(dataSource);
try (
final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = conn.prepareStatement(sql);
final ResultSet rs = pstmt.executeQuery()
) {
Expand All @@ -69,14 +56,13 @@ public <T> List<T> query(final String sql, final RowMapper<T> rowMapper) {

return results;
} catch (final SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}

public <T> Optional<T> queryForObject(final String sql, final RowMapper<T> rowMapper, final Object... params) {
final Connection conn = DataSourceUtils.getConnection(dataSource);
try (
final Connection conn = dataSource.getConnection();
final PreparedStatement pstmt = getPreparedStatement(conn, sql, params);
final ResultSet rs = pstmt.executeQuery()
) {
Expand All @@ -89,7 +75,6 @@ public <T> Optional<T> queryForObject(final String sql, final RowMapper<T> rowMa

return Optional.empty();
} catch (final SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class DataSourceUtils {

private DataSourceUtils() {}

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
public static Connection getConnection(final DataSource dataSource) throws CannotGetJdbcConnectionException {
Connection connection = TransactionSynchronizationManager.getResource(dataSource);
if (connection != null) {
return connection;
Expand All @@ -22,15 +22,15 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
connection = dataSource.getConnection();
TransactionSynchronizationManager.bindResource(dataSource, connection);
return connection;
} catch (SQLException ex) {
} catch (final SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
public static void releaseConnection(final Connection connection, final DataSource dataSource) {
try {
connection.close();
} catch (SQLException ex) {
} catch (final SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.springframework.transaction;

public class TransactionException extends RuntimeException {

public TransactionException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.springframework.transaction.support;

@FunctionalInterface
public interface TransactionCommandExecutor {

void run();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.springframework.transaction.support;

@FunctionalInterface
public interface TransactionQueryExecutor<T> {

T get();
}
Loading

0 comments on commit d26689d

Please sign in to comment.