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단계] 모디(전제희) 미션 제출합니다. #597

Merged
merged 6 commits into from
Oct 12, 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
32 changes: 32 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,32 @@
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;
}

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) {
User user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
}
}
Comment on lines +10 to +32

Choose a reason for hiding this comment

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

오태식이 돌아왔구나~
다시 돌아온 비즈니스 로직이 좋아요

111 changes: 111 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,111 @@
package com.techcourse.service;

import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class TxUserService implements UserService {

private final AppUserService appUserService;
private final DataSource datasource = DataSourceConfig.getInstance();

public TxUserService(AppUserService appUserService) {
this.appUserService = appUserService;
}

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

@Override
public void insert(User user) {
execute(() -> appUserService.insert(user));
}

@Override
public void changePassword(long id, String newPassword, String createBy) {
execute(() -> appUserService.changePassword(id, newPassword, createBy));
}

private void execute(ExecutableWithoutReturn executable) {
try (TransactionExecutor executor = TransactionExecutor.initTransaction(datasource)) {
try {
executable.execute();
executor.commit();
} catch (Exception e) {
executor.rollback();
throw new DataAccessException();
}
}
}

private <T> T execute(ExecutableWithReturn<T> executable) {
try (TransactionExecutor executor = TransactionExecutor.initTransaction(datasource)) {
try {
T result = executable.execute();
executor.commit();
return result;
} catch (Exception e) {
executor.rollback();
throw new DataAccessException();
}
}
}

@FunctionalInterface
private interface ExecutableWithoutReturn {
void execute();
}

@FunctionalInterface
private interface ExecutableWithReturn<T> {
T execute();
}

private static class TransactionExecutor implements AutoCloseable {

private final DataSource dataSource;
private final Connection connection;

private TransactionExecutor(DataSource dataSource, Connection connection) {
this.dataSource = dataSource;
this.connection = connection;
}
Comment on lines +70 to +78

Choose a reason for hiding this comment

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

오... AutoCloseable에 이너클래스까지 ㄷㄷ
덕분에 코드가 깔끔하네요!


public static TransactionExecutor initTransaction(DataSource dataSource) {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
connection.setAutoCommit(false);
return new TransactionExecutor(dataSource, connection);
} catch (SQLException e) {
throw new RuntimeException();
}
}

public void commit() {
try {
connection.commit();
} catch (SQLException e) {
throw new RuntimeException();
}
}

public void rollback() {
try {
connection.rollback();
} catch (SQLException e) {
throw new RuntimeException();
}
}

@Override
public void close() {
DataSourceUtils.releaseConnection(dataSource);
}
}
}
59 changes: 4 additions & 55 deletions app/src/main/java/com/techcourse/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,61 +1,10 @@
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 java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class UserService {
public interface UserService {

private static final Logger log = LoggerFactory.getLogger(UserService.class);
private final UserDao userDao;
private final UserHistoryDao userHistoryDao;

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) {
DataSource dataSource = DataSourceConfig.getInstance();
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
TransactionSynchronizationManager.bindResource(dataSource, conn);
User user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
userHistoryDao.log(new UserHistory(user, createBy));
conn.commit();
} catch (Exception e) {
log.error(e.getMessage(), e);
if (conn != null) {
try {
TransactionSynchronizationManager.unbindResource(dataSource);
conn.rollback();
conn.close();
} catch (SQLException ex) {
log.error(ex.getMessage(), ex);
}
}
throw new DataAccessException();
}
}
User findById(final long id);
void insert(final User user);
void changePassword(final long id, final String newPassword, final String createBy);
Comment on lines +7 to +9

Choose a reason for hiding this comment

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

사소한 부분인데요.
final이 있는 게 있고, 없는 게 있네요.

여태 모디가 직접 작성한 코드에서는 final이 없었던 것으로 기억하는데,
final을 사용하시는 기준이 있으신가요?

Copy link
Author

Choose a reason for hiding this comment

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

이 부분은 주어진 뼈대 코드의 final인데 뼈대 코드를 완전히 수정하지 않아서 남아있네요 ㅎㅎ..
뼈대 코드에 final이 항상 붙어있어 다 없애는 방향으로 통일해야 하나 항상 고민이 되었어요. 다 지우다가 일부 놓친 적도 있고..

불변성의 장점은 물론 알고 있어 예전에는 IDE의 도움으로 final을 붙여주었었는데,
쓰다 보니 final이 붙음으로써 메서드의 파라미터들이나 각종 변수들이 너무 길어지는 점,
때로는 까먹고 final을 붙이지 않기도 하고 이후에 코드를 읽을 때 의도적으로 뺐었는지 불필요하게 다시 고민하게 되는 점 등..

자바 외의 언어를 썼다면 당연히 따랐겠지만 자바에서는 언제나 final 키워드를 붙이면서 오는 잔실수나 가독성 저하가 불변 키워드에서 오는 장점보다 더 크게 다가오더라구요. 또 캡슐화를 통한 불변 객체 구현이 아닌 메서드 내부에서의 파라미터와 지역 변수들의 final 키워드가 과연 유용한게 맞냐는 생각도 많이 했고요...ㅎㅎㅎ

Choose a reason for hiding this comment

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

인정합니다 ㅎㅎ
저도 사실 final 키워드 신경쓰는게 번거롭더라고요...
그래서 기존 코드가 아예 텅텅 빈 상태다! 하면 final 키워드를 붙이지 않고 작업하고,
final이 포함되어있으면 이를 써는 정도로만 유지하고 있어요.
아무튼 모디 의견에 백배 공감합니다.

}
7 changes: 5 additions & 2 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void setUp() {
@Test
void testChangePassword() {
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 @@ -46,7 +46,10 @@ void testChangePassword() {
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

This file was deleted.

71 changes: 35 additions & 36 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.springframework.jdbc.core;

import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -11,7 +13,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.IncorrectResultSizeDataAccessException;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class JdbcTemplate {

Expand All @@ -23,32 +25,49 @@ public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public int update(String sql, Object... args) throws DataAccessException {
public int update(String sql, Object... args) {
return execute(sql, (pstmt) -> {
prepareStatement(pstmt, args);
return pstmt.executeUpdate();
});
}

public <T> T queryForObject(String sql, Class<T> requiredType, Object... args)
throws DataAccessException {
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
return execute(sql, (pstmt) -> {
prepareStatement(pstmt, args);
try (ResultSet rs = pstmt.executeQuery()) {
int columnCount = rs.getMetaData().getColumnCount();
if (rs.first() && rs.isLast()) {
Object[] initArgs = new Object[columnCount];
for (int i = 1; i <= columnCount; i++) {
initArgs[i - 1] = rs.getObject(i);
}
return InstantiateUtil.instantiate(rs, requiredType, initArgs);
}
throw new IncorrectResultSizeDataAccessException();
return instantiate(rs, requiredType);
}
});
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
private <T> T instantiate(ResultSet rs, Class<T> requiredType) throws Exception {
int columnCount = rs.getMetaData().getColumnCount();
if (rs.first() && rs.isLast()) {
Object[] initArgs = new Object[columnCount];
for (int i = 1; i <= columnCount; i++) {
initArgs[i - 1] = rs.getObject(i);
}
return instantiate(rs, requiredType, initArgs);
}
throw new IncorrectResultSizeDataAccessException();
}

private <T> T instantiate(ResultSet rs, Class<T> requiredType, Object[] initArgs)
throws Exception {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();

Class<?>[] columnTypes = new Class[columnCount];
for (int i = 1; i <= columnCount; i++) {
int columnType = metaData.getColumnType(i);
columnTypes[i - 1] = ColumnTypes.convertToClass(columnType);
}
Constructor<?> constructor = requiredType.getDeclaredConstructor(columnTypes);
return requiredType.cast(constructor.newInstance(initArgs));
}

public <T> List<T> query(String sql, RowMapper<T> rowMapper) {
return execute(sql, (pstmt) -> {
try (ResultSet resultSet = pstmt.executeQuery()) {
List<T> results = new ArrayList<>();
Expand All @@ -67,34 +86,14 @@ private void prepareStatement(PreparedStatement pstmt, Object[] args) throws SQL
}

private <T> T execute(String sql, StatementExecution<PreparedStatement, T> function) {
Connection conn = null;
Connection conn;
try {
conn = getConnection();
conn = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = conn.prepareStatement(sql);
return function.apply(pstmt);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
try {
if (isConnectionManuallyInstantiated(conn)) {
conn.close();
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
}
}
}

private Connection getConnection() throws SQLException {
if (TransactionSynchronizationManager.getResource(dataSource) != null) {
return TransactionSynchronizationManager.getResource(dataSource);
}
return dataSource.getConnection();
}

private boolean isConnectionManuallyInstantiated(Connection conn) {
return conn != null && TransactionSynchronizationManager.getResource(dataSource) == null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd
}
}

public static void releaseConnection(Connection connection, DataSource dataSource) {
public static void releaseConnection(DataSource dataSource) {
try {
Connection connection = TransactionSynchronizationManager.unbindResource(dataSource);
connection.close();
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
Expand Down
Loading