-
Notifications
You must be signed in to change notification settings - Fork 300
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단계] 테오(최우성) 미션 제출합니다. #517
Changes from all commits
1427c29
25a8c99
9ba3021
8154747
448581d
e5da2ae
bedd241
65d3369
310eab6
f1bec48
3317e13
03f1992
b856a18
9c26212
7732af1
37e3783
51c1cb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(final long id) { | ||
return userDao.findById(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)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package com.techcourse.service; | ||
|
||
import org.springframework.dao.DataAccessException; | ||
import org.springframework.jdbc.datasource.DataSourceUtils; | ||
|
||
import javax.sql.DataSource; | ||
import java.sql.Connection; | ||
import java.sql.SQLException; | ||
|
||
public class Transaction implements AutoCloseable { | ||
|
||
private final DataSource dataSource; | ||
private final Connection connection; | ||
|
||
private Transaction(final DataSource dataSource, final Connection connection) { | ||
this.dataSource = dataSource; | ||
this.connection = connection; | ||
} | ||
|
||
public static Transaction start(final DataSource dataSource) { | ||
try { | ||
Connection connection = DataSourceUtils.getConnection(dataSource); | ||
connection.setAutoCommit(false); | ||
return new Transaction(dataSource, connection); | ||
} catch (SQLException e) { | ||
throw new DataAccessException(e); | ||
} | ||
} | ||
|
||
public void commit() throws SQLException { | ||
connection.commit(); | ||
} | ||
|
||
public void rollback() { | ||
try { | ||
connection.rollback(); | ||
} catch (SQLException e) { | ||
throw new DataAccessException(e); | ||
} | ||
} | ||
|
||
public void setReadOnly(final boolean readOnly) { | ||
try { | ||
connection.setReadOnly(readOnly); | ||
} catch (SQLException e) { | ||
throw new DataAccessException(e); | ||
} | ||
} | ||
Go-Jaecheol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Override | ||
public void close() { | ||
DataSourceUtils.releaseConnection(dataSource); | ||
} | ||
Go-Jaecheol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package com.techcourse.service; | ||
|
||
import com.techcourse.domain.User; | ||
import org.springframework.dao.DataAccessException; | ||
|
||
import javax.sql.DataSource; | ||
import java.sql.SQLException; | ||
import java.util.function.Supplier; | ||
|
||
public class TransactionUserService implements UserService { | ||
|
||
private final AppUserService appUserService; | ||
private final DataSource dataSource; | ||
|
||
public TransactionUserService(final AppUserService appUserService, final DataSource dataSource) { | ||
this.appUserService = appUserService; | ||
this.dataSource = dataSource; | ||
} | ||
|
||
@Override | ||
public User findById(final long id) { | ||
return new TransactionExecutor() | ||
.setDataSource(dataSource) | ||
.setReadOnlyTrue() | ||
.executeTransactionWithSupplier(() -> appUserService.findById(id)); | ||
} | ||
|
||
@Override | ||
public void insert(final User user) { | ||
new TransactionExecutor() | ||
.setDataSource(dataSource) | ||
.executeTransactionWithRunnable(() -> appUserService.insert(user)); | ||
} | ||
|
||
@Override | ||
public void changePassword(final long id, final String newPassword, final String createBy) { | ||
new TransactionExecutor() | ||
.setDataSource(dataSource) | ||
.executeTransactionWithRunnable(() -> appUserService.changePassword(id, newPassword, createBy)); | ||
} | ||
|
||
private static class TransactionExecutor { | ||
|
||
private DataSource dataSource; | ||
private boolean readOnly = false; | ||
|
||
Comment on lines
+42
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금은 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동감합니다 ㅎㅎ 지금은 사용처가 한 곳 뿐이라 응집도를 높이기 위해 이너 클래스를 사용했는데, |
||
private TransactionExecutor setReadOnlyTrue() { | ||
this.readOnly = true; | ||
return this; | ||
} | ||
|
||
private TransactionExecutor setDataSource(final DataSource dataSource) { | ||
this.dataSource = dataSource; | ||
return this; | ||
} | ||
|
||
private void executeTransactionWithRunnable(final Runnable runnable) { | ||
executeTransactionWithSupplier(() -> { | ||
runnable.run(); | ||
return null; | ||
}); | ||
} | ||
|
||
private <T> T executeTransactionWithSupplier(final Supplier<T> supplier) { | ||
Transaction transaction = Transaction.start(dataSource); | ||
transaction.setReadOnly(readOnly); | ||
try { | ||
T result = supplier.get(); | ||
transaction.commit(); | ||
return result; | ||
} catch (SQLException | RuntimeException e) { | ||
transaction.rollback(); | ||
throw new DataAccessException(e); | ||
} finally { | ||
transaction.close(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,12 @@ | ||
package com.techcourse.service; | ||
|
||
import com.techcourse.dao.UserDao; | ||
import com.techcourse.dao.UserHistoryDao; | ||
import com.techcourse.domain.User; | ||
import com.techcourse.domain.UserHistory; | ||
import org.springframework.dao.DataAccessException; | ||
|
||
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); | ||
|
||
private final DataSource dataSource; | ||
|
||
public UserService(UserDao userDao, UserHistoryDao userHistoryDao, DataSource dataSource) { | ||
this.userDao = userDao; | ||
this.userHistoryDao = userHistoryDao; | ||
this.dataSource = dataSource; | ||
} | ||
|
||
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) { | ||
Connection connection = null; | ||
try { | ||
connection = dataSource.getConnection(); | ||
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 | RuntimeException e) { | ||
tryRollback(connection); | ||
throw new DataAccessException(e); | ||
} finally { | ||
tryCloseConnection(connection); | ||
} | ||
} | ||
|
||
private void tryRollback(Connection connection) { | ||
try { | ||
if (connection != null) { | ||
connection.rollback(); | ||
} | ||
} catch (SQLException ex) { | ||
throw new DataAccessException(ex); | ||
} | ||
} | ||
|
||
private void tryCloseConnection(Connection connection) { | ||
try { | ||
if (connection != null) { | ||
connection.close(); | ||
} | ||
} catch (SQLException e) { | ||
throw new DataAccessException(e); | ||
} | ||
} | ||
void changePassword(final long id, final String newPassword, final String createdBy); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,15 @@ | |
- ThreadLocal을 통해 커넥션 객체 자체를 스레드 변수로 등록하고, 필요한 곳에서 사용하는 방법은 어떨까? | ||
- 문제는 해결되겠지만 그럼에도 Service에 트랜잭션 관련 코드가 남아 있다. | ||
- AOP를 사용해야 할 시점이 아닐까? | ||
|
||
|
||
### step 4 기록 | ||
- 트랜잭션 동기화 매니져는 왜 Map 형태로 구현이 되어 있을까? | ||
- ThreadLocal의 경우 하나의 스레드에 대한 지역변수처럼 작동한다. | ||
- 그렇다면 Map을 사용하는 이유는 하나의 서비스에서 여러 DataSource를 사용할 일을 대비하기 위함이 아닐까? | ||
|
||
- 스프링을 사용하지 않는 환경에서 Spring AOP 같은 기능은 어떻게 만들 수 있을까? | ||
- 동적으로 런타임에 프록시를 만들고 싶다. | ||
- 스프링의 경우 Context로 빈들을 관리하기 때문에 런타임 주입 시 프록시 객체로 주입할 수 있다. | ||
- 하지만 스프링을 사용하지 않는 경우에는 `new 객체();` 처럼 선언을 하니 런타임에 프록시로 구현체를 갈아끼울 수 없다. | ||
- 객체간 의존관계를 스프링이 설정한다는 점 덕분에 Spring AOP가 가능한게 아닐까? | ||
Comment on lines
+10
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 항상 제가 생각 못한 부분까지 깊게 고민하시는 군요.. 제가 리뷰이가 된 기분입니다 🤣 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 객체 분리를 따로 진행하지 않아서 크게 얘기할 거리가 없네요 🥲
객체 분리를 진행하지 않고 서비스단에서 바로 처리하도록 했습니다.
테오의 코드에서 보자면 TransactionExecutor에서 Transaction 객체를 따로 생성하지 않고, 상황에 따라
connection.setAutoCommit(false);
connection.commit();
등 connection을 다루면서 트랜잭션 처리를 했습니다!'이건 트랜잭션 처리를 위한 메서드야' 라고 책임을 명확히 해준다는 점에서 테오 코드처럼 객체를 분리하는 것도 좋아보이네요.
배워갑니다 🙇♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!
그런데 생각해보니 망고 방식처럼 서비스에서 Connection 객체를 그대로 사용하는 것도 좋아보이네요!
지금은 별다른 처리도 해주고 있지 않기 때문에 Connection을 그대로 Service에서 다루는게 비용 측면에서 합리적이라고 생각합니다.
스프링의 경우에는
PlatformTransactionManager
를 사용해서 아마 지금 저의Transaction
객체와 비슷한 역할을 수행할텐데, 조금 더 알아봐야겠네요! 🤔