-
Notifications
You must be signed in to change notification settings - Fork 379
[3단계 - Transaction 적용하기] 우가(민보욱) 미션 제출합니다. #1159
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
Changes from 2 commits
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 |
|---|---|---|
| @@ -1,62 +1,33 @@ | ||
| package com.techcourse.dao; | ||
|
|
||
| import com.techcourse.domain.UserHistory; | ||
| import com.interface21.jdbc.core.JdbcTemplate; | ||
| import com.interface21.jdbc.core.PreparedStatementSetter; | ||
| import com.techcourse.domain.UserHistory; | ||
| import java.sql.Connection; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import javax.sql.DataSource; | ||
| import java.sql.Connection; | ||
| import java.sql.PreparedStatement; | ||
| import java.sql.SQLException; | ||
|
|
||
| public class UserHistoryDao { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); | ||
|
|
||
| private final DataSource dataSource; | ||
|
|
||
| public UserHistoryDao(final DataSource dataSource) { | ||
| this.dataSource = dataSource; | ||
| } | ||
| private final JdbcTemplate jdbcTemplate; | ||
|
|
||
| public UserHistoryDao(final JdbcTemplate jdbcTemplate) { | ||
| this.dataSource = null; | ||
| this.jdbcTemplate = jdbcTemplate; | ||
| } | ||
|
|
||
| public void log(final UserHistory userHistory) { | ||
| public void log(final UserHistory userHistory, final Connection connection) { | ||
| final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; | ||
|
|
||
| Connection conn = null; | ||
| PreparedStatement pstmt = null; | ||
| try { | ||
| conn = dataSource.getConnection(); | ||
| pstmt = conn.prepareStatement(sql); | ||
|
|
||
| log.debug("query : {}", sql); | ||
|
|
||
| pstmt.setLong(1, userHistory.getUserId()); | ||
| pstmt.setString(2, userHistory.getAccount()); | ||
| pstmt.setString(3, userHistory.getPassword()); | ||
| pstmt.setString(4, userHistory.getEmail()); | ||
| pstmt.setObject(5, userHistory.getCreatedAt()); | ||
| pstmt.setString(6, userHistory.getCreateBy()); | ||
| pstmt.executeUpdate(); | ||
| } catch (SQLException e) { | ||
| log.error(e.getMessage(), e); | ||
| throw new RuntimeException(e); | ||
| } finally { | ||
| try { | ||
| if (pstmt != null) { | ||
| pstmt.close(); | ||
| } | ||
| } catch (SQLException ignored) {} | ||
|
|
||
| try { | ||
| if (conn != null) { | ||
| conn.close(); | ||
| } | ||
| } catch (SQLException ignored) {} | ||
| } | ||
| PreparedStatementSetter pss = ps -> { | ||
| ps.setLong(1, userHistory.getUserId()); | ||
| ps.setString(2, userHistory.getAccount()); | ||
| ps.setString(3, userHistory.getPassword()); | ||
| ps.setString(4, userHistory.getEmail()); | ||
| ps.setObject(5, userHistory.getCreatedAt()); | ||
| ps.setString(6, userHistory.getCreateBy()); | ||
| }; | ||
|
|
||
| jdbcTemplate.update(connection, sql, pss); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| 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 org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
|
|
@@ -13,13 +15,14 @@ class UserDaoTest { | |
|
|
||
| private UserDao userDao; | ||
| private JdbcTemplate jdbcTemplate; | ||
| private Connection connection; | ||
|
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. Connection 리소스 누수 문제
몇 가지 질문을 드려보겠습니다:
💡 힌트: JUnit의 라이프사이클 애노테이션(@AfterEach 등)과 try-with-resources 구문을 고려해보세요. Also applies to: 24-24 🤖 Prompt for AI Agents |
||
|
|
||
| @BeforeEach | ||
| void setup() { | ||
| void setup() throws SQLException { | ||
| var dataSource = DataSourceConfig.getInstance(); | ||
| this.jdbcTemplate = new JdbcTemplate(dataSource); | ||
|
|
||
| jdbcTemplate.update("DROP TABLE users IF EXISTS"); | ||
| this.connection = dataSource.getConnection(); | ||
| jdbcTemplate.update(connection, "DROP TABLE users IF EXISTS"); | ||
bowook marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| DatabasePopulatorUtils.execute(dataSource); | ||
| userDao = new UserDao(jdbcTemplate); | ||
|
|
||
|
|
@@ -72,7 +75,7 @@ void update() { | |
| final var user = userDao.findByAccount("gugu").orElseThrow(); | ||
| user.changePassword(newPassword); | ||
|
|
||
| userDao.update(user); | ||
| userDao.update(user, connection); | ||
|
|
||
| assertThat(userDao.findByAccount("gugu")) | ||
| .isPresent() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,35 @@ | ||
| package com.techcourse.service; | ||
|
|
||
| import com.interface21.jdbc.core.JdbcTemplate; | ||
| import com.techcourse.config.DataSourceConfig; | ||
| import com.techcourse.dao.UserDao; | ||
| import com.techcourse.dao.UserHistoryDao; | ||
| import com.techcourse.domain.User; | ||
| import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; | ||
| import com.interface21.dao.DataAccessException; | ||
| import com.interface21.jdbc.core.JdbcTemplate; | ||
| import javax.sql.DataSource; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Disabled; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.SoftAssertions.assertSoftly; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
|
||
| @Disabled | ||
| class UserServiceTest { | ||
|
|
||
| private DataSource dataSource; | ||
| private JdbcTemplate jdbcTemplate; | ||
| private UserDao userDao; | ||
| private UserHistoryDao userHistoryDao; | ||
| private UserService userService; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); | ||
| this.dataSource = DataSourceConfig.getInstance(); | ||
| this.jdbcTemplate = new JdbcTemplate(dataSource); | ||
| this.userDao = new UserDao(jdbcTemplate); | ||
| this.userHistoryDao = new UserHistoryDao(jdbcTemplate); | ||
|
|
||
| this.userService = new UserService(userDao, userHistoryDao, dataSource); | ||
|
|
||
| DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); | ||
| final var user = new User("gugu", "password", "hkkang@woowahan.com"); | ||
|
|
@@ -32,32 +38,37 @@ void setUp() { | |
|
|
||
| @Test | ||
| void testChangePassword() { | ||
| final var userHistoryDao = new UserHistoryDao(jdbcTemplate); | ||
| final var userService = new UserService(userDao, userHistoryDao); | ||
|
|
||
| // given | ||
| final var newPassword = "qqqqq"; | ||
| final var createBy = "gugu"; | ||
| userService.changePassword(1L, newPassword, createBy); | ||
|
|
||
| // when | ||
| final var actual = userService.findById(1L); | ||
|
|
||
| // then | ||
| assertThat(actual.getPassword()).isEqualTo(newPassword); | ||
| } | ||
|
Comment on lines
49
to
50
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. 비밀번호 getter 의존은 보안·설계 상 취약합니다
행위 기반 검증(예: 비밀번호 일치 여부를 도메인/서비스가 판단)으로 전환하고, Password 같은 값 객체 도입을 고민해보셨나요? Also applies to: 67-69 🤖 Prompt for AI Agents |
||
|
|
||
| @Test | ||
| void testTransactionRollback() { | ||
| // 트랜잭션 롤백 테스트를 위해 mock으로 교체 | ||
| final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); | ||
| final var userService = new UserService(userDao, userHistoryDao); | ||
| // given | ||
| final var mockUserHistoryDao = new MockUserHistoryDao(jdbcTemplate); | ||
| final var userServiceWithMock = new UserService(userDao, mockUserHistoryDao, dataSource); | ||
|
|
||
| final var newPassword = "newPassword"; | ||
| final var createBy = "gugu"; | ||
| // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. | ||
| assertThrows(DataAccessException.class, | ||
| () -> userService.changePassword(1L, newPassword, createBy)); | ||
|
|
||
| assertThrows(RuntimeException.class, | ||
| () -> userServiceWithMock.changePassword(1L, newPassword, createBy)); | ||
|
|
||
| // when | ||
| final var actual = userService.findById(1L); | ||
bowook marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| assertThat(actual.getPassword()).isNotEqualTo(newPassword); | ||
| // then | ||
| assertSoftly(softAssertions -> { | ||
| softAssertions.assertThat(actual.getPassword()).isNotEqualTo(newPassword); | ||
| softAssertions.assertThat(actual.getPassword()).isNotEqualTo(newPassword); | ||
| }); | ||
| } | ||
| } | ||
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.
🛠️ Refactor suggestion | 🟠 Major
객체지향 생활 체조 원칙 9번 위반: Tell, Don't Ask
현재 코드는
UserHistory객체에게 데이터를 요청(Ask)하여 직접 처리하고 있습니다. 이는 캡슐화를 약화시키고UserHistory를 단순한 데이터 홀더로 만듭니다.🤔 생각해볼 질문들:
UserHistory객체 스스로가 PreparedStatement에 자신의 데이터를 설정하도록 할 수는 없을까요?💡 힌트:
"Tell, Don't Ask" 원칙에 따르면, 객체에게 데이터를 달라고 하지 말고, 객체에게 무언가를 하라고 말해야 합니다. UserHistory가 스스로 PreparedStatement 바인딩 책임을 가질 수 있는지 고민해보세요.
📖 이 원칙이 중요한 이유:
As per coding guidelines