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단계] 아코(안석환) 미션 제출합니다. #564

Merged
merged 14 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@
- [x] UserHistoryDao jdbcTemplate 이용하게 수정
- [x] 트랜잭션 동기화하기
- [x] jdbcTemplate에 connection을 받는 update메소드 오버라이드하기
## 트랜잭션 synchronization 적용하기
- [x] Transaction synchronization 적용하기
- [x] TransactionSynchronizationManager 구현하기
- [x] Dao에 connection 인자 제거하기
- [x] 트랜잭션 서비스 추상화하기

17 changes: 8 additions & 9 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package com.techcourse.dao;

import com.techcourse.domain.User;

import java.sql.Connection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.result.RowMapper;

import java.util.List;

public class UserDao {

private static final Logger log = LoggerFactory.getLogger(UserDao.class);
private static final RowMapper<User> userRowMapper = (rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
);

private final JdbcTemplate jdbcTemplate;
Expand All @@ -30,9 +29,9 @@ public void insert(final User user) {
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
5 changes: 1 addition & 4 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;

public class UserHistoryDao {

private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class);
Expand All @@ -17,11 +15,10 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

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

jdbcTemplate.update(
connection,
sql,
userHistory.getUserId(),
userHistory.getAccount(),
Expand Down
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(final UserDao userDao, final 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) {
final var user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
28 changes: 28 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,28 @@
package com.techcourse.service;

import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
import com.techcourse.support.transaction.TransactionExecutor;

public class TxUserService {
Copy link
Member

Choose a reason for hiding this comment

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

프록시의 역할을 하는 것 같은데 UserService 인터페이스를 구현하고 있지 않네요!

Copy link
Author

Choose a reason for hiding this comment

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

이 부분 실수를 범했네요 수정하겠습니다!


private final UserService userService;
private final TransactionExecutor transactionExecutor;

public TxUserService(final UserService userService) {
this.userService = userService;
this.transactionExecutor = new TransactionExecutor(DataSourceConfig.getInstance());
}

public User findById(final long id) {
return userService.findById(id);
}
Copy link
Member

Choose a reason for hiding this comment

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

조회는 트랜잭션이 없어도 괜찮을까요?!

스프링에서는 조회의 경우에도 @Transactional(readonly = true)처럼 설정을 해주는데요.

이런 경우 DB 레벨에서 최적화가 가능해지는 것으로 알고 있어요. (DB가 읽기 전용 최적화를 지원한다면)

적용은 안해주셔도 좋으나, 읽기 전용 트랜잭션으로 findById를 감싸는 방법도 고민해보셨으면 좋겠습니다~

Copy link
Author

Choose a reason for hiding this comment

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

저도 리뷰를 받고 고민을 해보니 데이터를 조회하는 것도 결국에는 transaction의 격리수준을 보장 받아야 하기 때문에 데이터를 조회하는 것도 transsaction을 적용하겠습니다.

Copy link
Author

Choose a reason for hiding this comment

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

connection.setReadOnly()설정이 있어서 이를 적용했습니다!

Copy link
Member

Choose a reason for hiding this comment

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

굿 제가 의도하신대로 잘 해주셨네요!


public void insert(final User user) {
transactionExecutor.execute(() -> userService.insert(user));
}

public void changePassword(final long id, final String newPassword, final String createBy) {
transactionExecutor.execute(() -> userService.changePassword(id, newPassword, createBy));
}
}
54 changes: 4 additions & 50 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,58 +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.springframework.jdbc.exception.DataAccessException;
import org.springframework.jdbc.exception.RollbackFailException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
public interface UserService {

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

private final UserDao userDao;
private final UserHistoryDao userHistoryDao;
void insert(final User user);

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

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

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

public void changePassword(final long id, final String newPassword, final String createBy) throws SQLException {
final DataSource dataSource = DataSourceConfig.getInstance();
final Connection connection = dataSource.getConnection();
try {
connection.setAutoCommit(false);
final var user = findById(id);
user.changePassword(newPassword);
userDao.update(connection, user);
userHistoryDao.log(connection, new UserHistory(user, createBy));
connection.commit();
} catch (SQLException e) {
rollback(connection);
} finally {
connection.close();
}
}

private static void rollback(Connection connection) {
try {
connection.rollback();
throw new DataAccessException("데이터에 접근할 수 없습니다.");
} catch (SQLException exception) {
throw new RollbackFailException("롤백을 실패했습니다.");
}
}
void changePassword(final Long id, final String newPassword, final String createBy);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.techcourse.support.transaction;

import java.sql.SQLException;

@FunctionalInterface
public interface ServiceLogicExecutor {

void execute() throws SQLException;
}
Copy link
Member

Choose a reason for hiding this comment

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

Runnable을 사용하지 않고 따로 ServiceLogicExecutor를 사용해주신 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

구현을 할 당시에 Runnable이 떠오르지 않았어서 구현을 했었는데 Runnable로 처리가 가능할것 같네요! 이 부분 수정하겠습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.techcourse.support.transaction;

import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.exception.DataAccessException;
import org.springframework.jdbc.exception.RollbackFailException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class TransactionExecutor {

private final DataSource dataSource;

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

public void execute(final ServiceLogicExecutor serviceLogicExecutor) {
final Connection connection = DataSourceUtils.getConnection(dataSource);
try {
connection.setAutoCommit(false);
serviceLogicExecutor.execute();
connection.commit();
} catch (SQLException e) {
rollback(connection);
Copy link
Member

Choose a reason for hiding this comment

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

롤백은 SQLException이 발생할 때만 수행되어야 할까요?

SQLException에 대해서만 rollback을 수행하면 어떤 문제가 생길 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

Runtime 시점에 예외를 처리하지 못할 것 같습니다. Runtim에도 예외가 발생할 수 있게 수정하겠습니다.
꼼꼼한 리뷰 감사합니다!☺️

} finally {
DataSourceUtils.releaseConnection(connection, dataSource);
}
}

private static void rollback(Connection connection) {
try {
connection.rollback();
throw new DataAccessException("데이터에 접근할 수 없습니다.");
} catch (SQLException exception) {
throw new RollbackFailException("롤백을 실패했습니다.");
}
}
}
21 changes: 8 additions & 13 deletions app/src/test/java/com/techcourse/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
package com.techcourse.dao;

import static org.assertj.core.api.Assertions.assertThat;

import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class UserDaoTest {

private JdbcTemplate jdbcTemplate;
private UserDao userDao;
private DataSource dataSource;

@BeforeEach
void setup() {
dataSource = DataSourceConfig.getInstance();
final DataSource dataSource = DataSourceConfig.getInstance();
DatabasePopulatorUtils.execute(dataSource);
jdbcTemplate = new JdbcTemplate(dataSource);
userDao = new UserDao(jdbcTemplate);
Expand Down Expand Up @@ -71,13 +67,12 @@ void insert() {
}

@Test
void update() throws SQLException {
final Connection connection = dataSource.getConnection();
void update() {
final String newPassword = "password99";
final User user = userDao.findById(1L);
user.changePassword(newPassword);

userDao.update(connection, user);
userDao.update(user);

final var actual = userDao.findById(1L);

Expand Down
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 {

public 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();
}
}
6 changes: 4 additions & 2 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ void setUp() {
@Test
void testChangePassword() throws SQLException {
final var userHistoryDao = new UserHistoryDao(jdbcTemplate);
final var userService = new UserService(userDao, userHistoryDao);

final var userService = new AppUserService(userDao, userHistoryDao);

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

final var newPassword = "newPassword";
final var createBy = "gugu";
Expand Down
Loading
Loading