From 5c96f2553458fa8cdd6807bba2876c0335c8ae9e Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 7 Oct 2023 15:03:31 +0900 Subject: [PATCH 01/18] =?UTF-8?q?test:=20(3=EB=8B=A8=EA=B3=84)=20Isolation?= =?UTF-8?q?=20Level=20=ED=95=99=EC=8A=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/transaction/stage1/Stage1Test.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index 8c29944d4e..243a183cb6 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -32,12 +32,12 @@ * + : 발생 * - : 발생하지 않음 * Read phenomena | Dirty reads | Non-repeatable reads | Phantom reads - * Isolation level | | | - * -----------------|-------------|----------------------|-------------- - * Read Uncommitted | | | - * Read Committed | | | - * Repeatable Read | | | - * Serializable | | | + * Isolation level | | | + * -----------------|--------------|-----------------------|-------------- + * Read Uncommitted | 🅾️ | 🅾️ | 🅾️ + * Read Committed | ❎ | 🅾️ | 🅾️ + * Repeatable Read | ❎ | ❎ | 🅾️ + * Serializable | ❎ | ❎ | ❎ */ class Stage1Test { @@ -56,12 +56,12 @@ private void setUp(final DataSource dataSource) { * + : 발생 * - : 발생하지 않음 * Read phenomena | Dirty reads - * Isolation level | + * Isolation level | 커밋되지 않은 데이터를 조회할 수 있음 * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | 🅾️️ + * Read Committed | ❎ + * Repeatable Read | ❎ + * Serializable | ❎ */ @Test void dirtyReading() throws SQLException { @@ -81,7 +81,7 @@ void dirtyReading() throws SQLException { final var subConnection = dataSource.getConnection(); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED; // 트랜잭션 격리 레벨을 설정한다. subConnection.setTransactionIsolation(isolationLevel); @@ -109,12 +109,12 @@ void dirtyReading() throws SQLException { * + : 발생 * - : 발생하지 않음 * Read phenomena | Non-repeatable reads - * Isolation level | + * Isolation level | 한 트랜잭션 내에서 같은 데이터를 조회했을 때 다른 결과가 나올 수 있음 * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | 🅾️ + * Read Committed | 🅾️ + * Repeatable Read | ❎ + * Serializable | ❎ */ @Test void noneRepeatable() throws SQLException { @@ -130,7 +130,7 @@ void noneRepeatable() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -151,7 +151,7 @@ void noneRepeatable() throws SQLException { userDao.update(subConnection, anotherUser); })).start(); - sleep(0.5); + sleep(0.5); //쓰레드 기다리기 위함 // 사용자A가 다시 gugu 객체를 조회했다. // 사용자B는 패스워드를 변경하고 아직 커밋하지 않았다. @@ -171,12 +171,12 @@ void noneRepeatable() throws SQLException { * + : 발생 * - : 발생하지 않음 * Read phenomena | Phantom reads - * Isolation level | + * Isolation level | 데이터의 추가 및 삭제가 일어나면 조회 결과가 달라질 수 있음 * -----------------|-------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | 🅾️ + * Read Committed | 🅾️ + * Repeatable Read | 🅾️ + * Serializable | ❎ */ @Test void phantomReading() throws SQLException { @@ -184,6 +184,7 @@ void phantomReading() throws SQLException { // testcontainer로 docker를 실행해서 mysql에 연결한다. final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) .withLogConsumer(new Slf4jLogConsumer(log)); + mysql.withUrlParam("allowMultiQueries", "true"); mysql.start(); setUp(createMySQLDataSource(mysql)); @@ -197,7 +198,7 @@ void phantomReading() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); From daef050a6ba4409b136a4b4fd691df58bb828a84 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 7 Oct 2023 16:34:24 +0900 Subject: [PATCH 02/18] =?UTF-8?q?test:=20Propagation=20=ED=95=99=EC=8A=B5?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/stage2/FirstUserService.java | 18 +-- .../transaction/stage2/SecondUserService.java | 3 +- .../java/transaction/stage2/Stage2Test.java | 106 ++++++++++++++---- 3 files changed, 95 insertions(+), 32 deletions(-) diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java index 9a1d415c18..a0d37b4bc6 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -46,19 +46,19 @@ public Set saveFirstTransactionWithRequired() { } @Transactional(propagation = Propagation.REQUIRED) - public Set saveFirstTransactionWithRequiredNew() { + public Set saveFirstTransactionWithRequiredNew(boolean throwException) { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); logActualTransactionActive(); - final var secondTransactionName = secondUserService.saveSecondTransactionWithRequiresNew(); + final var secondTransactionName = secondUserService.saveSecondTransactionWithRequiresNew(throwException); return of(firstTransactionName, secondTransactionName); } @Transactional(propagation = Propagation.REQUIRED) - public Set saveAndExceptionWithRequiredNew() { - secondUserService.saveSecondTransactionWithRequiresNew(); + public Set saveAndExceptionWithRequiredNew(boolean throwException) { + secondUserService.saveSecondTransactionWithRequiresNew(throwException); userRepository.save(User.createTest()); logActualTransactionActive(); @@ -66,7 +66,7 @@ public Set saveAndExceptionWithRequiredNew() { throw new RuntimeException(); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithSupports() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -77,7 +77,7 @@ public Set saveFirstTransactionWithSupports() { return of(firstTransactionName, secondTransactionName); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithMandatory() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -88,7 +88,7 @@ public Set saveFirstTransactionWithMandatory() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNotSupported() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -99,7 +99,7 @@ public Set saveFirstTransactionWithNotSupported() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNested() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -110,7 +110,7 @@ public Set saveFirstTransactionWithNested() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNever() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); diff --git a/study/src/test/java/transaction/stage2/SecondUserService.java b/study/src/test/java/transaction/stage2/SecondUserService.java index 0d240fe854..390f2893b4 100644 --- a/study/src/test/java/transaction/stage2/SecondUserService.java +++ b/study/src/test/java/transaction/stage2/SecondUserService.java @@ -26,9 +26,10 @@ public String saveSecondTransactionWithRequired() { } @Transactional(propagation = Propagation.REQUIRES_NEW) - public String saveSecondTransactionWithRequiresNew() { + public String saveSecondTransactionWithRequiresNew(boolean throwException) { userRepository.save(User.createTest()); logActualTransactionActive(); + if(!throwException) throw new RuntimeException(); return TransactionSynchronizationManager.getCurrentTransactionName(); } diff --git a/study/src/test/java/transaction/stage2/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java index e522bf4365..e4236e5bda 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.IllegalTransactionStateException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -36,8 +37,8 @@ void tearDown() { } /** - * 생성된 트랜잭션이 몇 개인가? - * 왜 그런 결과가 나왔을까? + * 생성된 트랜잭션이 몇 개인가? : propagetion이 required면 새로운 트랜잭션을 생성하지 않고 기존의 트랜잭션에 참여하기 때문에 1개의 트랜잭션만 생성된다. + * 왜 그런 결과가 나왔을까? : 트랜잭션의 이름은 메서드의 이름이 된다. */ @Test void testRequired() { @@ -45,71 +46,109 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** - * 생성된 트랜잭션이 몇 개인가? - * 왜 그런 결과가 나왔을까? + * 생성된 트랜잭션이 몇 개인가? : propagation이 required_new면 호출할 때마다 새로운 트랜잭션을 생성하기 때문에 해당 테스트에선 2개의 트랜잭션이 생성된다. + * 왜 그런 결과가 나왔을까? : 트랜잭션의 이름은 메서드의 이름이 되는데, 트랜잭션이 두개니까 이름도 두개가 된다. */ @Test void testRequiredNew() { - final var actual = firstUserService.saveFirstTransactionWithRequiredNew(); + final var actual = firstUserService.saveFirstTransactionWithRequiredNew(true); log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew", + "transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew"); } /** * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. * REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 지 확인해보자. + * :second 트랜잭션이 예외 발생으로 인해 rollback되면서 first 로직에 예외가 던져지는데 + * 여기서 예외에 대한 처리를 하지 않았기 때문에 rollback된다. 즉, 저장된 user는 없기 때문에 size가 0이다. */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); - assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) + assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew(false)) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); } /** * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + * 1. 주석이 없는 경우 + * first에서 트랜잭션을 생성하지 않고 second를 호출함. second에선 @Transactional의 속성으로 supports를 지정했는데 + * 이는 이미 존재하는 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 실행한다는 의미이다. + * 트랜잭션 없이 실행하지만 @Transactional 어노테이션이 붙은 이상 "논리 트랜잭션"이 만들어져 이름도 메서드로 지정되지만 + * 그 트랜잭션은 "물리 트랜잭션"이 아니라 활성화되진 않는다. + * 2. 주석이 있는 경우 + * first에서 트랜잭션을 생성하고 second를 호출함. second에선 @Transactional의 속성으로 supports를 지정했기 때문에 + * first에서 생성한 트랜잭션에 참여하게 된다. 따라서 first에서 생성한 트랜잭션의 이름이 second에서도 그대로 사용된다. + * */ @Test void testSupports() { final var actual = firstUserService.saveFirstTransactionWithSupports(); log.info("transactions : {}", actual); + // 주석이 없는 경우 +// assertThat(actual) +// .hasSize(1) +// .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithSupports"); + + // 주석이 있는 경우 assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. * SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. + * 1. 주석이 없을 때 + * first에서 트랜잭션을 생성하지 않고 second를 호출함. second에선 @Transactional의 속성으로 mandatory를 지정했는데 + * 이는 이미 존재하는 트랜잭션이 있으면 참여하고 없으면 예외를 발생시킨다는 의미이다. + * 예외가 발생했기 때문에 first에서도 예외가 발생하고 테스트가 실패한다. + * 2. 주석이 있을 때 + * first에서 트랜잭션을 생성하고 second를 호출함. second에선 @Transactional의 속성으로 mandatory를 지정했기 때문에 + * first에서 생성한 트랜잭션에 참여하게 된다. 따라서 first에서 생성한 트랜잭션의 이름이 second에서도 그대로 사용된다. */ @Test void testMandatory() { + // 주석이 없을 때 +// assertThatThrownBy(() -> firstUserService.saveFirstTransactionWithMandatory()) +// .isInstanceOf(IllegalTransactionStateException.class); + + // 주석이 있을 때 final var actual = firstUserService.saveFirstTransactionWithMandatory(); log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** * 아래 테스트는 몇 개의 물리적 트랜잭션이 동작할까? * FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석 처리하자. * 다시 테스트를 실행하면 몇 개의 물리적 트랜잭션이 동작할까? + * 1. FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석이 있을 때 + * first에서 트랜잭션을 생성하고 second를 호출함. second에선 @Transactional의 속성으로 not_supported를 지정했는데 + * 이는 이미 존재하는 트랜잭션이 있으면 일시 중단하고 없으면 트랜잭션 없이 실행한다는 의미이다. + * 따라서 first에서 생성한 트랜잭션을 일시 중단하고 second에서는 트랜잭션 없이 실행된다. + * 이때 second에선 @Transactional 어노테이션이 있기 때문에 이 자체로 "논리 트랜잭션"이 생성되지만 "물리 트랜잭션"은 생성되지 않는다. + * 2. FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석이 없을 때 + * first에서 트랜잭션없이 second를 호출함. first는 @Transactional 어노테이션이 없어 논리 및 물리 트랜잭션이 생성되지 않는다. + * second에선 @Transactional의 속성으로 not_supported를 지정했기 때문에 "논리 트랜잭션"이 생성되지만 "물리 트랜잭션"은 생성되지 않는다. * * 스프링 공식 문서에서 물리적 트랜잭션과 논리적 트랜잭션의 차이점이 무엇인지 찾아보자. */ @@ -118,14 +157,25 @@ void testNotSupported() { final var actual = firstUserService.saveFirstTransactionWithNotSupported(); log.info("transactions : {}", actual); + // FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석이 있을 때 +// assertThat(actual) +// .hasSize(2) +// .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported", +// "transaction.stage2.FirstUserService.saveFirstTransactionWithNotSupported"); + // FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석이 없을 때 assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported"); } /** * 아래 테스트는 왜 실패할까? + * 중첩 트랜잭션은 JDBC 3.0 이후 버전의 savepoint기능을 사용하는데, + * JPA를 사용하는 경우, 변경감지를 통해서 업데이트문을 최대한 지연해서 발행하는 방식을 사용하기 때문에 + * 중첩된 트랜잭션 경계를 설정할 수 없어 지원하지 않는다. * FirstUserService.saveFirstTransactionWithNested() 메서드의 @Transactional을 주석 처리하면 어떻게 될까? + * first에서 트랜잭션 없이 second를 호출함. first는 @Transactional 어노테이션이 없어 논리 및 물리 트랜잭션이 생성되지 않는다. + * second에선 @Transactional의 속성으로 nested를 지정했기 때문에 새로운 물리 트랜잭션을 생성한다. */ @Test void testNested() { @@ -133,20 +183,32 @@ void testNested() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNested"); } /** * 마찬가지로 @Transactional을 주석처리하면서 관찰해보자. + * 1. FirstUserService.saveFirstTransactionWithNever() 메서드의 @Transactional을 주석이 있을 때 + * first에서 트랜잭션을 시작하고 second를 호출함. second에선 @Transactional의 속성으로 never를 지정했는데 + * 이는 이미 존재하는 트랜잭션이 있으면 예외를 발생시키고 없으면 트랜잭션 없이 실행한다는 의미이다. + * 따라서 first에서 생성한 트랜잭션이 존재하기 때문에 예외가 발생한다. + * 2. FirstUserService.saveFirstTransactionWithNever() 메서드의 @Transactional을 주석이 없을 때 + * first에서 트랜잭션 없이 second를 호출함. second에선 @Transactional의 속성으로 never를 지정했기 때문에 + * second에선 @Transactional의 속성으로 never를 지정했기 때문에 "논리 트랜잭션"이 생성되지만 "물리 트랜잭션"은 생성되지 않는다. */ @Test void testNever() { + // FirstUserService.saveFirstTransactionWithNever() 메서드의 @Transactional을 주석이 있을 때 +// assertThatThrownBy(() -> firstUserService.saveFirstTransactionWithNever()) +// .isInstanceOf(IllegalTransactionStateException.class); + + // IllegalTransactionStateException final var actual = firstUserService.saveFirstTransactionWithNever(); log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNever"); } } From b80fa206ba0502cc0cb556dae0fcc6b8b83b1453 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 15:19:24 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20User=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 10 ++-- .../com/techcourse/dao/UserHistoryDao.java | 49 +++---------------- .../com/techcourse/service/UserService.java | 44 ++++++++++++++--- .../java/com/techcourse/dao/UserDaoTest.java | 23 ++++++--- .../techcourse/service/UserServiceTest.java | 34 +++++++------ .../jdbc/core/JdbcTemplate.java | 42 ++++++++++------ .../jdbc/core/PreparedStatementSetter.java | 8 +++ 7 files changed, 119 insertions(+), 91 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index b87de536f9..9977a1eb68 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -6,8 +6,8 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import java.sql.Connection; import java.util.List; -import java.util.Optional; public class UserDao { @@ -20,7 +20,7 @@ public class UserDao { rs.getString("email") ); - private JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; public UserDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; @@ -32,10 +32,10 @@ public void insert(User user) { jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - public void update(User user) { - String sql = "update users set account = ?, password = ? where email = ?"; + public void update(Connection connection, User user) { + String sql = "update users set account = ?, password = ?, email = ? where id = ?"; log.info("[LOG] update user"); - jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } public List findAll() { diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..0575756bb3 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,62 +1,25 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; -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(Connection connection, UserHistory userHistory) { 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) {} - } + log.info("[LOG] insert user into user_history"); + jdbcTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); } } diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fe00eb6ced..804a16120e 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,14 +1,20 @@ 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 javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + public class UserService { private final UserDao userDao; private final UserHistoryDao userHistoryDao; + private final DataSource dataSource = DataSourceConfig.getInstance(); public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { this.userDao = userDao; @@ -20,14 +26,38 @@ public User findById(final long id) { .orElseThrow(() -> new IllegalArgumentException("해당하는 유저가 없습니다.")); } - public void insert(final User user) { - userDao.insert(user); } - 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)); + public void insert(User user) throws SQLException { + Connection connection = null; + try { + connection = dataSource.getConnection(); + connection.setAutoCommit(false); + userDao.insert(connection, user); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new RuntimeException(e); + } finally { + close(connection); + } + } + + public void changePassword(long id, String newPassword, String createBy) throws SQLException { + Connection connection = null; + try { + connection = dataSource.getConnection(); + connection.setAutoCommit(false); + User user = findById(id); + user.changePassword(newPassword); + userDao.update(connection, user); + userHistoryDao.log(connection, new UserHistory(user, createBy)); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new RuntimeException(e); + } finally { + close(connection); + } } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 097bb7e536..e44f551e65 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -7,19 +7,28 @@ import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; +import javax.sql.DataSource; + +import java.sql.Connection; +import java.sql.SQLException; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class UserDaoTest { private UserDao userDao; + private Connection connection; @BeforeEach - void setup() { - DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + void setup() throws SQLException { + DataSource dataSource = DataSourceConfig.getInstance(); + DatabasePopulatorUtils.execute(dataSource); + connection = dataSource.getConnection(); - userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + userDao = new UserDao(new JdbcTemplate(dataSource)); + userDao.insert(connection, user); } @Test @@ -58,12 +67,12 @@ void insert() { @Test void update() { final var newPassword = "password99"; - final var user = userDao.findById(1L).get(); + final var user = userDao.findById(connection, 1L); user.changePassword(newPassword); - userDao.update(user); + userDao.update(connection, user); - final var actual = userDao.findById(1L).get(); + final var actual = userDao.findById(connection, 1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..30ba6435ee 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,41 +5,45 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -@Disabled class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @BeforeEach - void setUp() { - this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + void setUp() throws SQLException { + DataSource dataSource = DataSourceConfig.getInstance(); + DatabasePopulatorUtils.execute(dataSource); + this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); + Connection connection = dataSource.getConnection(); - DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(connection, user); } @Test - void testChangePassword() { - final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + void testChangePassword() throws SQLException { + var userHistoryDao = new UserHistoryDao(jdbcTemplate); + var userService = new UserService(userDao, userHistoryDao); - final var newPassword = "qqqqq"; - final var createBy = "gugu"; + var newPassword = "qqqqq"; + var createBy = "gugu"; userService.changePassword(1L, newPassword, createBy); - final var actual = userService.findById(1L); + var actual = userService.findById(1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 8f5d251a91..0e6253ba18 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,12 +11,11 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; public class JdbcTemplate { private static Logger log = LoggerFactory.getLogger(JdbcTemplate.class); - private DataSource dataSource; + private final DataSource dataSource; public JdbcTemplate(DataSource dataSource) { this.dataSource = dataSource; @@ -32,7 +31,18 @@ public List query(String sql, RowMapper rowMapper, Object... args) { results.add(rowMapper.run(resultSet)); } - return results; + public void update(Connection connection, String sql, Object... args) { + update(connection, sql, createPreparedStatementSetter(args)); + } + + public void update(String sql, Object... args) { + update(sql, createPreparedStatementSetter(args)); + } + + private List query(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + pss.setParameters(preparedStatement); + return mapResultSetToObject(rowMapper, preparedStatement); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e.getMessage(), e); @@ -48,16 +58,19 @@ public Optional queryForObject(String sql, RowMapper rowMapper, Object return Optional.empty(); } - return Optional.of(rowMapper.run(resultSet)); + private void update(Connection connection, String sql, PreparedStatementSetter pss) { + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + pss.setParameters(preparedStatement); + preparedStatement.executeUpdate(); } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e.getMessage(), e); + throw new DataAccessException(e); } } - public void update(String sql, Object... args) { + private void update(String sql, PreparedStatementSetter pss) { try (Connection connection = dataSource.getConnection(); - PreparedStatement preparedStatement = getPreparedStatement(connection, sql, args)) { + PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + pss.setParameters(preparedStatement); preparedStatement.executeUpdate(); } catch (SQLException e) { log.error(e.getMessage(), e); @@ -65,12 +78,13 @@ public void update(String sql, Object... args) { } } - private PreparedStatement getPreparedStatement(Connection connection, String sql, Object[] args) throws SQLException { - PreparedStatement preparedStatement = connection.prepareStatement(sql); - - for (int i = 0; i < args.length; i++) { - preparedStatement.setObject(i + 1, args[i]); - } + private PreparedStatementSetter createPreparedStatementSetter(Object... args) { + return psmt -> { + for (int i = 0; i < args.length; i++) { + psmt.setObject(i + 1, args[i]); + } + }; + } return preparedStatement; } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java new file mode 100644 index 0000000000..e9a749d32a --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java @@ -0,0 +1,8 @@ +package org.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface PreparedStatementSetter { + void setParameters(PreparedStatement pstmt) throws SQLException; +} From cc8cce413fa83a6585566afb31732b8bc20910cd Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 15:19:56 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=EC=9B=90=EC=9E=90=EC=84=B1=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=A5=20=EC=9C=84=ED=95=9C=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 16 ++++---- .../com/techcourse/service/UserService.java | 24 +++++++++-- .../java/com/techcourse/dao/UserDaoTest.java | 17 +++++--- .../service/MockUserHistoryDao.java | 4 +- .../techcourse/service/UserServiceTest.java | 12 +++--- .../jdbc/core/JdbcTemplate.java | 40 +++++++++++-------- 6 files changed, 73 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 9977a1eb68..8207f83bc8 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -26,10 +26,10 @@ public UserDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(User user) { + public void insert(Connection connection, User user) { String sql = "insert into users (account, password, email) values (?, ?, ?)"; log.info("[LOG] insert user into users"); - jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(Connection connection, User user) { @@ -38,21 +38,21 @@ public void update(Connection connection, User user) { jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } - public List findAll() { + public List findAll(Connection connection) { String sql = "select id, account, password, email from users"; log.info("[LOG] select all from users"); - return jdbcTemplate.query(sql, USER_ROW_MAPPER); + return jdbcTemplate.query(connection, sql, USER_ROW_MAPPER); } - public Optional findById(Long id) { + public User findById(Connection connection, Long id) { String sql = "select id, account, password, email from users where id = ?"; log.info("[LOG] select user by id"); - return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id); + return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); } - public Optional findByAccount(String account) { + public User findByAccount(Connection connection, String account) { String sql = "select id, account, password, email from users where account = ?"; log.info("[LOG] select user by account"); - return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, account); + return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, account); } } diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 804a16120e..9345ed3e3f 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -21,10 +21,18 @@ public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { this.userHistoryDao = userHistoryDao; } - public User findById(final long id) { - return userDao.findById(id) - .orElseThrow(() -> new IllegalArgumentException("해당하는 유저가 없습니다.")); - } + public User findById(long id) throws SQLException { + Connection connection = null; + try { + connection = dataSource.getConnection(); + connection.setAutoCommit(false); + return userDao.findById(connection, id); + } catch (SQLException e) { + connection.rollback(); + throw new RuntimeException(e); + } finally { + close(connection); + } } @@ -60,4 +68,12 @@ public void changePassword(long id, String newPassword, String createBy) throws close(connection); } } + + private void close(Connection connection){ + try { + connection.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index e44f551e65..1978a948ef 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -33,14 +33,14 @@ void setup() throws SQLException { @Test void findAll() { - final var users = userDao.findAll(); + final var users = userDao.findAll(connection); assertThat(users).isNotEmpty(); } @Test void findById() { - final var user = userDao.findById(1L).get(); + final var user = userDao.findById(connection, 1L); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -48,18 +48,25 @@ void findById() { @Test void findByAccount() { final var account = "gugu"; - final var user = userDao.findByAccount(account).get(); + final var user = userDao.findByAccount(connection, account); assertThat(user.getAccount()).isEqualTo(account); } + @Test + void findByWrongAccount() { + final var account = "gaga"; + assertThatThrownBy(() -> userDao.findByAccount(connection, account)) + .isInstanceOf(RuntimeException.class); + } + @Test void insert() { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); - userDao.insert(user); + userDao.insert(connection, user); - final var actual = userDao.findById(2L).get(); + final var actual = userDao.findById(connection, 2L); assertThat(actual.getAccount()).isEqualTo(account); } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..a9081eef0f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -5,6 +5,8 @@ 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) { @@ -12,7 +14,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final UserHistory userHistory) { + public void log(Connection connection, UserHistory userHistory) { throw new DataAccessException(); } } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 30ba6435ee..b8e7ed0b5b 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -49,18 +49,18 @@ void testChangePassword() throws SQLException { } @Test - void testTransactionRollback() { + void testTransactionRollback() throws SQLException { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + var userService = new UserService(userDao, userHistoryDao); - final var newPassword = "newPassword"; - final var createBy = "gugu"; + var newPassword = "newPassword"; + var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, () -> userService.changePassword(1L, newPassword, createBy)); - final var actual = userService.findById(1L); + var actual = userService.findById(1L); assertThat(actual.getPassword()).isNotEqualTo(newPassword); } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 0e6253ba18..9364de59a8 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -21,15 +21,13 @@ public JdbcTemplate(DataSource dataSource) { this.dataSource = dataSource; } - public List query(String sql, RowMapper rowMapper, Object... args) { - try (Connection connection = dataSource.getConnection(); - PreparedStatement preparedStatement = getPreparedStatement(connection, sql, args); - ResultSet resultSet = preparedStatement.executeQuery()) { + public List query(Connection connection, String sql, RowMapper rowMapper, Object... args) { + return query(connection, sql, rowMapper, createPreparedStatementSetter(args)); + } - List results = new ArrayList<>(); - while (resultSet.next()) { - results.add(rowMapper.run(resultSet)); - } + public T queryForObject(Connection connection, String sql, RowMapper rowMapper, Object... args) { + return queryForObject(connection, sql, rowMapper, createPreparedStatementSetter(args)); + } public void update(Connection connection, String sql, Object... args) { update(connection, sql, createPreparedStatementSetter(args)); @@ -49,14 +47,14 @@ private List query(Connection connection, String sql, RowMapper rowMap } } - public Optional queryForObject(String sql, RowMapper rowMapper, Object... args) { - try (Connection connection = dataSource.getConnection(); - PreparedStatement preparedStatement = getPreparedStatement(connection, sql, args); - ResultSet resultSet = preparedStatement.executeQuery()) { + private T queryForObject(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { + List list = query(connection, sql, rowMapper, pss); - if (!resultSet.next()) { - return Optional.empty(); - } + if (list.isEmpty()) { + throw new DataAccessException("해당하는 유저가 없습니다."); + } + return list.get(0); + } private void update(Connection connection, String sql, PreparedStatementSetter pss) { try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { @@ -86,6 +84,16 @@ private PreparedStatementSetter createPreparedStatementSetter(Object... args) { }; } - return preparedStatement; + private List mapResultSetToObject(RowMapper rowMapper, PreparedStatement preparedStatement) { + try (ResultSet resultSet = preparedStatement.executeQuery()) { + List list = new ArrayList<>(); + while (resultSet.next()) { + list.add(rowMapper.run(resultSet)); + } + return list; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e.getMessage(), e); + } } } From 2ccc69da85b6c99bf31d62bc7cefc8168b5dcedd Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 15:23:50 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=EA=B2=B0=EA=B3=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 9364de59a8..9e00f691e0 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -15,6 +15,7 @@ public class JdbcTemplate { private static Logger log = LoggerFactory.getLogger(JdbcTemplate.class); + private static final int SINGLE_SIZE = 1; private final DataSource dataSource; public JdbcTemplate(DataSource dataSource) { @@ -48,12 +49,9 @@ private List query(Connection connection, String sql, RowMapper rowMap } private T queryForObject(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { - List list = query(connection, sql, rowMapper, pss); - - if (list.isEmpty()) { - throw new DataAccessException("해당하는 유저가 없습니다."); - } - return list.get(0); + List result = query(connection, sql, rowMapper, pss); + validateResultSize(result); + return result.get(0); } private void update(Connection connection, String sql, PreparedStatementSetter pss) { @@ -96,4 +94,14 @@ private List mapResultSetToObject(RowMapper rowMapper, PreparedStateme throw new DataAccessException(e.getMessage(), e); } } + + private static void validateResultSize(List result) { + if (result.isEmpty()) { + throw new DataAccessException("해당하는 유저가 없습니다."); + } + + if(result.size() > SINGLE_SIZE) { + throw new DataAccessException("해당하는 유저가 2명 이상입니다."); + } + } } From 3acf179899fd54a805b4f892d1a2c4db4c342ba5 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 15:37:48 +0900 Subject: [PATCH 06/18] =?UTF-8?q?refactor:=20update=EC=8B=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EB=90=9C=20row=20=EA=B0=9C=EC=88=98=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserDao.java | 4 ++-- .../main/java/com/techcourse/service/UserService.java | 10 ++++++++++ .../org/springframework/jdbc/core/JdbcTemplate.java | 8 ++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 8207f83bc8..10c5ee6014 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -32,10 +32,10 @@ public void insert(Connection connection, User user) { jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); } - public void update(Connection connection, User user) { + public int update(Connection connection, User user) { String sql = "update users set account = ?, password = ?, email = ? where id = ?"; log.info("[LOG] update user"); - jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + return jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } public List findAll(Connection connection) { diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 9345ed3e3f..b6766734b4 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -5,6 +5,8 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import javax.sql.DataSource; import java.sql.Connection; @@ -58,6 +60,7 @@ public void changePassword(long id, String newPassword, String createBy) throws connection.setAutoCommit(false); User user = findById(id); user.changePassword(newPassword); + updateUser(connection, user); userDao.update(connection, user); userHistoryDao.log(connection, new UserHistory(user, createBy)); connection.commit(); @@ -69,6 +72,13 @@ public void changePassword(long id, String newPassword, String createBy) throws } } + private void updateUser(Connection connection, User user) { + int updateSize = userDao.update(connection, user); + if(updateSize != 1){ + throw new DataAccessException("갱신된 데이터의 개수가 올바르지 않습니다."); + } + } + private void close(Connection connection){ try { connection.close(); diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 9e00f691e0..7b14efd0c1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -30,8 +30,8 @@ public T queryForObject(Connection connection, String sql, RowMapper rowM return queryForObject(connection, sql, rowMapper, createPreparedStatementSetter(args)); } - public void update(Connection connection, String sql, Object... args) { - update(connection, sql, createPreparedStatementSetter(args)); + public int update(Connection connection, String sql, Object... args) { + return update(connection, sql, createPreparedStatementSetter(args)); } public void update(String sql, Object... args) { @@ -54,10 +54,10 @@ private T queryForObject(Connection connection, String sql, RowMapper row return result.get(0); } - private void update(Connection connection, String sql, PreparedStatementSetter pss) { + private int update(Connection connection, String sql, PreparedStatementSetter pss) { try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { pss.setParameters(preparedStatement); - preparedStatement.executeUpdate(); + return preparedStatement.executeUpdate(); } catch (SQLException e) { throw new DataAccessException(e); } From e6278dd16b307eccde5df6c93b1d7a8b3b55123c Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 16:28:03 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refactor:=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=ED=81=AC=EA=B8=B0=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/springframework/jdbc/core/JdbcTemplate.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 7b14efd0c1..420c1b248b 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -15,7 +15,6 @@ public class JdbcTemplate { private static Logger log = LoggerFactory.getLogger(JdbcTemplate.class); - private static final int SINGLE_SIZE = 1; private final DataSource dataSource; public JdbcTemplate(DataSource dataSource) { @@ -99,9 +98,5 @@ private static void validateResultSize(List result) { if (result.isEmpty()) { throw new DataAccessException("해당하는 유저가 없습니다."); } - - if(result.size() > SINGLE_SIZE) { - throw new DataAccessException("해당하는 유저가 2명 이상입니다."); - } } } From 47f248b3f71a8d9ef5912aaef80d66a5a3343547 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 16:57:39 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20(4=EB=8B=A8=EA=B3=84)=20Transacti?= =?UTF-8?q?on=20synchronization=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/UserService.java | 48 +++++++------------ .../jdbc/datasource/DataSourceUtils.java | 1 + .../TransactionSynchronizationManager.java | 32 +++++++++++-- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index b6766734b4..e1649d64ea 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -6,17 +6,20 @@ import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLTransactionRollbackException; public class UserService { private final UserDao userDao; private final UserHistoryDao userHistoryDao; private final DataSource dataSource = DataSourceConfig.getInstance(); + private static final int QUERY_SINGLE_SIZE = 1; public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { this.userDao = userDao; @@ -24,66 +27,51 @@ public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { } public User findById(long id) throws SQLException { - Connection connection = null; + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); try { - connection = dataSource.getConnection(); - connection.setAutoCommit(false); - return userDao.findById(connection, id); + User user = userDao.findById(connection, id); + connection.commit(); + return user; } catch (SQLException e) { connection.rollback(); - throw new RuntimeException(e); - } finally { - close(connection); + throw new SQLTransactionRollbackException(e.getMessage()); } - } public void insert(User user) throws SQLException { - Connection connection = null; + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); try { - connection = dataSource.getConnection(); - connection.setAutoCommit(false); userDao.insert(connection, user); connection.commit(); } catch (SQLException e) { connection.rollback(); - throw new RuntimeException(e); - } finally { - close(connection); + throw new SQLTransactionRollbackException(e.getMessage()); } } public void changePassword(long id, String newPassword, String createBy) throws SQLException { - Connection connection = null; + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); try { - connection = dataSource.getConnection(); - connection.setAutoCommit(false); User user = findById(id); user.changePassword(newPassword); updateUser(connection, user); - userDao.update(connection, user); userHistoryDao.log(connection, new UserHistory(user, createBy)); connection.commit(); } catch (SQLException e) { connection.rollback(); - throw new RuntimeException(e); + throw new SQLTransactionRollbackException(e.getMessage()); } finally { - close(connection); + DataSourceUtils.releaseConnection(connection, dataSource); } } private void updateUser(Connection connection, User user) { int updateSize = userDao.update(connection, user); - if(updateSize != 1){ + if (updateSize != QUERY_SINGLE_SIZE) { throw new DataAccessException("갱신된 데이터의 개수가 올바르지 않습니다."); } } - - private void close(Connection connection){ - try { - connection.close(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 3c40bfec52..be2f114ac1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -29,6 +29,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { + TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..3621c7ef1e 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,7 +1,11 @@ package org.springframework.transaction.support; +import org.springframework.jdbc.CannotGetJdbcConnectionException; + import javax.sql.DataSource; import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { @@ -11,13 +15,35 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() {} public static Connection getResource(DataSource key) { - return null; + Map map = getMap(); + Connection connection = map.get(key); + try { + if(connection == null || connection.isClosed()){ + connection = key.getConnection(); + map.put(key, connection); + } + return connection; + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection"); + } + } + + private static Map getMap() { + Map map = resources.get(); + if(map == null){ + map = new HashMap<>(); + resources.set(map); + } + return map; } public static void bindResource(DataSource key, Connection value) { + Map map = resources.get(); + map.put(key, value); } - public static Connection unbindResource(DataSource key) { - return null; + public static void unbindResource(DataSource key) { + Map map = resources.get(); + map.remove(key); } } From 99f454389f6dd56d54cb6b41f30d9a940abfd64b Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 17:19:17 +0900 Subject: [PATCH 09/18] =?UTF-8?q?refactor:=20DAO=EA=B0=80=20Connection=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=A0=84=EB=8B=AC=EB=B0=9B=EC=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 17 +++++++++++++++-- .../java/com/techcourse/dao/UserHistoryDao.java | 7 ++++++- .../com/techcourse/service/UserService.java | 13 ++++++------- .../java/com/techcourse/dao/UserDaoTest.java | 12 ++++++------ .../techcourse/service/MockUserHistoryDao.java | 4 +--- .../com/techcourse/service/UserServiceTest.java | 4 +--- .../springframework/jdbc/core/JdbcTemplate.java | 8 ++++---- 7 files changed, 39 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 10c5ee6014..3ea1fb2a05 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,11 +1,14 @@ package com.techcourse.dao; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.datasource.DataSourceUtils; +import javax.sql.DataSource; import java.sql.Connection; import java.util.List; @@ -21,12 +24,14 @@ public class UserDao { ); private final JdbcTemplate jdbcTemplate; + private final DataSource dataSource = DataSourceConfig.getInstance(); public UserDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(Connection connection, User user) { + public void insert(User user) { + Connection connection = DataSourceUtils.getConnection(dataSource); String sql = "insert into users (account, password, email) values (?, ?, ?)"; log.info("[LOG] insert user into users"); jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); @@ -38,13 +43,21 @@ public int update(Connection connection, User user) { return jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } + public int update(User user) { + Connection connection = DataSourceUtils.getConnection(dataSource); + String sql = "update users set account = ?, password = ?, email = ? where id = ?"; + log.info("[LOG] update user"); + return jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + } + public List findAll(Connection connection) { String sql = "select id, account, password, email from users"; log.info("[LOG] select all from users"); return jdbcTemplate.query(connection, sql, USER_ROW_MAPPER); } - public User findById(Connection connection, Long id) { + public User findById(Long id) { + Connection connection = DataSourceUtils.getConnection(dataSource); String sql = "select id, account, password, email from users where id = ?"; log.info("[LOG] select user by id"); return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 0575756bb3..ab28a532f1 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,10 +1,13 @@ package com.techcourse.dao; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceUtils; +import javax.sql.DataSource; import java.sql.Connection; public class UserHistoryDao { @@ -12,12 +15,14 @@ public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); private final JdbcTemplate jdbcTemplate; + private final DataSource dataSource = DataSourceConfig.getInstance(); public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(Connection connection, UserHistory userHistory) { + public void log(UserHistory userHistory) { + Connection connection = DataSourceUtils.getConnection(dataSource); final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; log.info("[LOG] insert user into user_history"); jdbcTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index e1649d64ea..3105464fcc 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -7,7 +7,6 @@ import com.techcourse.domain.UserHistory; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; import java.sql.Connection; @@ -30,7 +29,7 @@ public User findById(long id) throws SQLException { Connection connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); try { - User user = userDao.findById(connection, id); + User user = userDao.findById(id); connection.commit(); return user; } catch (SQLException e) { @@ -43,7 +42,7 @@ public void insert(User user) throws SQLException { Connection connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); try { - userDao.insert(connection, user); + userDao.insert(user); connection.commit(); } catch (SQLException e) { connection.rollback(); @@ -57,8 +56,8 @@ public void changePassword(long id, String newPassword, String createBy) throws try { User user = findById(id); user.changePassword(newPassword); - updateUser(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); + updateUser(user); + userHistoryDao.log(new UserHistory(user, createBy)); connection.commit(); } catch (SQLException e) { connection.rollback(); @@ -68,8 +67,8 @@ public void changePassword(long id, String newPassword, String createBy) throws } } - private void updateUser(Connection connection, User user) { - int updateSize = userDao.update(connection, user); + private void updateUser(User user) { + int updateSize = userDao.update(user); if (updateSize != QUERY_SINGLE_SIZE) { throw new DataAccessException("갱신된 데이터의 개수가 올바르지 않습니다."); } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 1978a948ef..b6e17416a8 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -28,7 +28,7 @@ void setup() throws SQLException { final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao = new UserDao(new JdbcTemplate(dataSource)); - userDao.insert(connection, user); + userDao.insert(user); } @Test @@ -40,7 +40,7 @@ void findAll() { @Test void findById() { - final var user = userDao.findById(connection, 1L); + final var user = userDao.findById(1L); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -64,9 +64,9 @@ void findByWrongAccount() { void insert() { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); - userDao.insert(connection, user); + userDao.insert(user); - final var actual = userDao.findById(connection, 2L); + final var actual = userDao.findById(2L); assertThat(actual.getAccount()).isEqualTo(account); } @@ -74,12 +74,12 @@ void insert() { @Test void update() { final var newPassword = "password99"; - final var user = userDao.findById(connection, 1L); + final var user = userDao.findById(1L); user.changePassword(newPassword); userDao.update(connection, user); - final var actual = userDao.findById(connection, 1L); + final var actual = userDao.findById(1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index a9081eef0f..0ab4810cc0 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -5,8 +5,6 @@ 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) { @@ -14,7 +12,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(Connection connection, UserHistory userHistory) { + public void log(UserHistory userHistory) { throw new DataAccessException(); } } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index b8e7ed0b5b..4e16d807d2 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -11,7 +11,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; -import java.sql.Connection; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; @@ -28,10 +27,9 @@ void setUp() throws SQLException { DatabasePopulatorUtils.execute(dataSource); this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); - Connection connection = dataSource.getConnection(); var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(connection, user); + userDao.insert(user); } @Test diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 420c1b248b..c9d06a3085 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -33,8 +33,8 @@ public int update(Connection connection, String sql, Object... args) { return update(connection, sql, createPreparedStatementSetter(args)); } - public void update(String sql, Object... args) { - update(sql, createPreparedStatementSetter(args)); + public int update(String sql, Object... args) { + return update(sql, createPreparedStatementSetter(args)); } private List query(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { @@ -62,11 +62,11 @@ private int update(Connection connection, String sql, PreparedStatementSetter ps } } - private void update(String sql, PreparedStatementSetter pss) { + private int update(String sql, PreparedStatementSetter pss) { try (Connection connection = dataSource.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql)) { pss.setParameters(preparedStatement); - preparedStatement.executeUpdate(); + return preparedStatement.executeUpdate(); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e.getMessage(), e); From 63febecf8c61f50a5f217ae31ca69c2363dbda19 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 17:55:25 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EC=83=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techcourse/service/AppUserService.java | 41 +++++++++++ .../com/techcourse/service/TxUserService.java | 64 +++++++++++++++++ .../com/techcourse/service/UserService.java | 70 ++----------------- .../techcourse/service/UserServiceTest.java | 19 ++--- 4 files changed, 120 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/AppUserService.java create mode 100644 app/src/main/java/com/techcourse/service/TxUserService.java diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java new file mode 100644 index 0000000000..5fbfced7e7 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,41 @@ +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; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + private static final int QUERY_SINGLE_SIZE = 1; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + public User findById(long id) { + return userDao.findById(id); + } + + public void insert(User user) { + userDao.insert(user); + } + + public void changePassword(long id, String newPassword, String createBy) { + User user = findById(id); + user.changePassword(newPassword); + updateUser(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } + + private void updateUser(User user) { + int updateSize = userDao.update(user); + if (updateSize != QUERY_SINGLE_SIZE) { + throw new DataAccessException("갱신된 데이터의 개수가 올바르지 않습니다."); + } + } +} diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..dd1be224ee --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,64 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import org.springframework.jdbc.datasource.DataSourceUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTransactionRollbackException; + +public class TxUserService implements UserService { + + private UserService userService; + private final DataSource dataSource = DataSourceConfig.getInstance(); + + public TxUserService(UserService userService) { + this.userService = userService; + } + + @Override + public User findById(long id) throws SQLException { + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + try { + User user = userService.findById(id); + connection.commit(); + return user; + } catch (SQLException e) { + connection.rollback(); + throw new SQLTransactionRollbackException(e.getMessage()); + } + } + + @Override + public void insert(User user) throws SQLException { + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + try { + userService.insert(user); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new SQLTransactionRollbackException(e.getMessage()); + } + } + + @Override + public void changePassword(long id, String newPassword, String createBy) throws SQLException { + Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + try { + User user = findById(id); + user.changePassword(newPassword); + userService.changePassword(id, newPassword, createBy); + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new SQLTransactionRollbackException(e.getMessage()); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 3105464fcc..762ab7e729 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,76 +1,14 @@ 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.dao.DataAccessException; -import org.springframework.jdbc.datasource.DataSourceUtils; -import javax.sql.DataSource; -import java.sql.Connection; import java.sql.SQLException; -import java.sql.SQLTransactionRollbackException; -public class UserService { +public interface UserService { - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - private final DataSource dataSource = DataSourceConfig.getInstance(); - private static final int QUERY_SINGLE_SIZE = 1; + User findById(long id) throws SQLException; - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } + void insert(User user) throws SQLException; - public User findById(long id) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - try { - User user = userDao.findById(id); - connection.commit(); - return user; - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } - } - - public void insert(User user) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - try { - userDao.insert(user); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } - } - - public void changePassword(long id, String newPassword, String createBy) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - try { - User user = findById(id); - user.changePassword(newPassword); - updateUser(user); - userHistoryDao.log(new UserHistory(user, createBy)); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } finally { - DataSourceUtils.releaseConnection(connection, dataSource); - } - } - - private void updateUser(User user) { - int updateSize = userDao.update(user); - if (updateSize != QUERY_SINGLE_SIZE) { - throw new DataAccessException("갱신된 데이터의 개수가 올바르지 않습니다."); - } - } + void changePassword(long id, String newPassword, String createBy) throws SQLException; } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 4e16d807d2..97574634f5 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -22,7 +22,7 @@ class UserServiceTest { private UserDao userDao; @BeforeEach - void setUp() throws SQLException { + void setUp() { DataSource dataSource = DataSourceConfig.getInstance(); DatabasePopulatorUtils.execute(dataSource); this.jdbcTemplate = new JdbcTemplate(dataSource); @@ -33,9 +33,9 @@ void setUp() throws SQLException { } @Test - void testChangePassword() throws SQLException { + void testChangePassword() { var userHistoryDao = new UserHistoryDao(jdbcTemplate); - var userService = new UserService(userDao, userHistoryDao); + var userService = new AppUserService(userDao, userHistoryDao); var newPassword = "qqqqq"; var createBy = "gugu"; @@ -49,16 +49,19 @@ void testChangePassword() throws SQLException { @Test void testTransactionRollback() throws SQLException { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - var userService = new UserService(userDao, userHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService); - var newPassword = "newPassword"; - var createBy = "gugu"; + final var newPassword = "newPassword"; + final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, () -> userService.changePassword(1L, newPassword, createBy)); - var actual = userService.findById(1L); + final var actual = userService.findById(1L); assertThat(actual.getPassword()).isNotEqualTo(newPassword); } From 4659c4a57ec1b0b29dd8503766e0c8d27c6d1a5e Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sun, 8 Oct 2023 23:27:14 +0900 Subject: [PATCH 11/18] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EA=B2=BD=EA=B3=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TxUserService.java | 19 +++++++++++++++---- .../techcourse/service/UserServiceTest.java | 7 ++++--- .../TransactionSynchronizationManager.java | 5 +++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index dd1be224ee..c5b574bbf2 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -3,6 +3,7 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; import java.sql.Connection; @@ -20,11 +21,16 @@ public TxUserService(UserService userService) { @Override public User findById(long id) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); + Connection connection = null; try { + if (TransactionSynchronizationManager.hasConnection(dataSource)) { + return userService.findById(id); + } + connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); User user = userService.findById(id); connection.commit(); + DataSourceUtils.releaseConnection(connection, dataSource); return user; } catch (SQLException e) { connection.rollback(); @@ -34,9 +40,14 @@ public User findById(long id) throws SQLException { @Override public void insert(User user) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); + Connection connection = null; try { + if (TransactionSynchronizationManager.hasConnection(dataSource)) { + userService.insert(user); + return; + } + connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); userService.insert(user); connection.commit(); } catch (SQLException e) { diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 97574634f5..5c3d96546f 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -20,10 +20,11 @@ class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; + private DataSource dataSource; @BeforeEach void setUp() { - DataSource dataSource = DataSourceConfig.getInstance(); + dataSource = DataSourceConfig.getInstance(); DatabasePopulatorUtils.execute(dataSource); this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); @@ -33,9 +34,9 @@ void setUp() { } @Test - void testChangePassword() { + void testChangePassword() throws SQLException { var userHistoryDao = new UserHistoryDao(jdbcTemplate); - var userService = new AppUserService(userDao, userHistoryDao); + var userService = new TxUserService(new AppUserService(userDao, userHistoryDao)); var newPassword = "qqqqq"; var createBy = "gugu"; diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 3621c7ef1e..bf6a63595d 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -37,6 +37,11 @@ private static Map getMap() { return map; } + public static boolean hasConnection(DataSource key) { + Map map = resources.get(); + return map != null && map.containsKey(key); + } + public static void bindResource(DataSource key, Connection value) { Map map = resources.get(); map.put(key, value); From 92a5284826bf79fe8093056e02f11f4e6fb546d8 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Mon, 9 Oct 2023 12:22:39 +0900 Subject: [PATCH 12/18] =?UTF-8?q?refactor:=20service=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/service/TxUserService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index c5b574bbf2..ce103f4fcc 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -61,8 +61,6 @@ public void changePassword(long id, String newPassword, String createBy) throws Connection connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); try { - User user = findById(id); - user.changePassword(newPassword); userService.changePassword(id, newPassword, createBy); connection.commit(); } catch (SQLException e) { From 80bea25d1b5d44c51a3004eb4ea85b66ba310dcf Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 08:38:17 +0900 Subject: [PATCH 13/18] =?UTF-8?q?refactor:=20ThreadLocal=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TransactionSynchronizationManager.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index bf6a63595d..9fda456c2f 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -10,15 +10,16 @@ public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { Map map = getMap(); Connection connection = map.get(key); try { - if(connection == null || connection.isClosed()){ + if (connection == null || connection.isClosed()) { connection = key.getConnection(); map.put(key, connection); } @@ -29,17 +30,12 @@ public static Connection getResource(DataSource key) { } private static Map getMap() { - Map map = resources.get(); - if(map == null){ - map = new HashMap<>(); - resources.set(map); - } - return map; + return resources.get(); } public static boolean hasConnection(DataSource key) { Map map = resources.get(); - return map != null && map.containsKey(key); + return map.containsKey(key); } public static void bindResource(DataSource key, Connection value) { From 441699410c17e4b05fcd9fdfef23d027820906f2 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 08:44:27 +0900 Subject: [PATCH 14/18] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=EC=9D=98=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springframework/jdbc/datasource/DataSourceUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index be2f114ac1..00cb517d17 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -29,8 +29,10 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { - TransactionSynchronizationManager.unbindResource(dataSource); - connection.close(); + if(!connection.getAutoCommit()){ + TransactionSynchronizationManager.unbindResource(dataSource); + connection.close(); + } } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } From f0d39b7eb40f87544802c1ff4f36a6912d1bdfca Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 08:53:10 +0900 Subject: [PATCH 15/18] =?UTF-8?q?refactor:=20connection=20=EB=B0=98?= =?UTF-8?q?=EB=B3=B5=EC=A0=84=EB=8B=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 28 +++++------------- .../com/techcourse/dao/UserHistoryDao.java | 9 +----- .../java/com/techcourse/dao/UserDaoTest.java | 15 ++++------ .../jdbc/core/JdbcTemplate.java | 29 ++++++------------- 4 files changed, 22 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 3ea1fb2a05..d3b3dfa0a9 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,15 +1,11 @@ package com.techcourse.dao; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.datasource.DataSourceUtils; -import javax.sql.DataSource; -import java.sql.Connection; import java.util.List; public class UserDao { @@ -24,48 +20,38 @@ public class UserDao { ); private final JdbcTemplate jdbcTemplate; - private final DataSource dataSource = DataSourceConfig.getInstance(); public UserDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void insert(User user) { - Connection connection = DataSourceUtils.getConnection(dataSource); String sql = "insert into users (account, password, email) values (?, ?, ?)"; log.info("[LOG] insert user into users"); - jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); - } - - public int update(Connection connection, User user) { - String sql = "update users set account = ?, password = ?, email = ? where id = ?"; - log.info("[LOG] update user"); - return jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public int update(User user) { - Connection connection = DataSourceUtils.getConnection(dataSource); String sql = "update users set account = ?, password = ?, email = ? where id = ?"; log.info("[LOG] update user"); - return jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + return jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } - public List findAll(Connection connection) { + public List findAll() { String sql = "select id, account, password, email from users"; log.info("[LOG] select all from users"); - return jdbcTemplate.query(connection, sql, USER_ROW_MAPPER); + return jdbcTemplate.query(sql, USER_ROW_MAPPER); } public User findById(Long id) { - Connection connection = DataSourceUtils.getConnection(dataSource); String sql = "select id, account, password, email from users where id = ?"; log.info("[LOG] select user by id"); - return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); + return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id); } - public User findByAccount(Connection connection, String account) { + public User findByAccount(String account) { String sql = "select id, account, password, email from users where account = ?"; log.info("[LOG] select user by account"); - return jdbcTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, account); + return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, account); } } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index ab28a532f1..00563a995c 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,30 +1,23 @@ package com.techcourse.dao; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.datasource.DataSourceUtils; - -import javax.sql.DataSource; -import java.sql.Connection; public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); private final JdbcTemplate jdbcTemplate; - private final DataSource dataSource = DataSourceConfig.getInstance(); public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void log(UserHistory userHistory) { - Connection connection = DataSourceUtils.getConnection(dataSource); final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; log.info("[LOG] insert user into user_history"); - jdbcTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); + jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index b6e17416a8..c08bbcb67e 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -9,22 +9,17 @@ import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class UserDaoTest { private UserDao userDao; - private Connection connection; @BeforeEach - void setup() throws SQLException { + void setup() { DataSource dataSource = DataSourceConfig.getInstance(); DatabasePopulatorUtils.execute(dataSource); - connection = dataSource.getConnection(); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao = new UserDao(new JdbcTemplate(dataSource)); @@ -33,7 +28,7 @@ void setup() throws SQLException { @Test void findAll() { - final var users = userDao.findAll(connection); + final var users = userDao.findAll(); assertThat(users).isNotEmpty(); } @@ -48,7 +43,7 @@ void findById() { @Test void findByAccount() { final var account = "gugu"; - final var user = userDao.findByAccount(connection, account); + final var user = userDao.findByAccount(account); assertThat(user.getAccount()).isEqualTo(account); } @@ -56,7 +51,7 @@ void findByAccount() { @Test void findByWrongAccount() { final var account = "gaga"; - assertThatThrownBy(() -> userDao.findByAccount(connection, account)) + assertThatThrownBy(() -> userDao.findByAccount(account)) .isInstanceOf(RuntimeException.class); } @@ -77,7 +72,7 @@ void update() { final var user = userDao.findById(1L); user.changePassword(newPassword); - userDao.update(connection, user); + userDao.update(user); final var actual = userDao.findById(1L); diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index c9d06a3085..240803bc6e 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -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; @@ -21,23 +22,20 @@ public JdbcTemplate(DataSource dataSource) { this.dataSource = dataSource; } - public List query(Connection connection, String sql, RowMapper rowMapper, Object... args) { - return query(connection, sql, rowMapper, createPreparedStatementSetter(args)); + public List query(String sql, RowMapper rowMapper, Object... args) { + return query(sql, rowMapper, createPreparedStatementSetter(args)); } - public T queryForObject(Connection connection, String sql, RowMapper rowMapper, Object... args) { - return queryForObject(connection, sql, rowMapper, createPreparedStatementSetter(args)); - } - - public int update(Connection connection, String sql, Object... args) { - return update(connection, sql, createPreparedStatementSetter(args)); + public T queryForObject(String sql, RowMapper rowMapper, Object... args) { + return queryForObject(sql, rowMapper, createPreparedStatementSetter(args)); } public int update(String sql, Object... args) { return update(sql, createPreparedStatementSetter(args)); } - private List query(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { + private List query(String sql, RowMapper rowMapper, PreparedStatementSetter pss) { + Connection connection = DataSourceUtils.getConnection(dataSource); try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { pss.setParameters(preparedStatement); return mapResultSetToObject(rowMapper, preparedStatement); @@ -47,21 +45,12 @@ private List query(Connection connection, String sql, RowMapper rowMap } } - private T queryForObject(Connection connection, String sql, RowMapper rowMapper, PreparedStatementSetter pss) { - List result = query(connection, sql, rowMapper, pss); + private T queryForObject(String sql, RowMapper rowMapper, PreparedStatementSetter pss) { + List result = query(sql, rowMapper, pss); validateResultSize(result); return result.get(0); } - private int update(Connection connection, String sql, PreparedStatementSetter pss) { - try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { - pss.setParameters(preparedStatement); - return preparedStatement.executeUpdate(); - } catch (SQLException e) { - throw new DataAccessException(e); - } - } - private int update(String sql, PreparedStatementSetter pss) { try (Connection connection = dataSource.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql)) { From 2ae1bc39a4646b4f7612ef44a68d63e263f79b36 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 08:59:44 +0900 Subject: [PATCH 16/18] =?UTF-8?q?refactor:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EB=B0=B0=EC=97=B4=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/dao/UserHistoryDao.java | 10 +++++++++- .../jdbc/core/JdbcTemplate.java | 18 +++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 00563a995c..ae2fdd3f09 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -18,6 +18,14 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { public void log(UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; log.info("[LOG] insert user into user_history"); - jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); + Object[] params = { + userHistory.getUserId(), + userHistory.getAccount(), + userHistory.getPassword(), + userHistory.getEmail(), + userHistory.getCreatedAt(), + userHistory.getCreateBy() + }; + jdbcTemplate.update(sql, params); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 240803bc6e..2d3ee7e16d 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -22,16 +22,16 @@ public JdbcTemplate(DataSource dataSource) { this.dataSource = dataSource; } - public List query(String sql, RowMapper rowMapper, Object... args) { - return query(sql, rowMapper, createPreparedStatementSetter(args)); + public List query(String sql, RowMapper rowMapper, Object... params) { + return query(sql, rowMapper, createPreparedStatementSetter(params)); } - public T queryForObject(String sql, RowMapper rowMapper, Object... args) { - return queryForObject(sql, rowMapper, createPreparedStatementSetter(args)); + public T queryForObject(String sql, RowMapper rowMapper, Object... params) { + return queryForObject(sql, rowMapper, createPreparedStatementSetter(params)); } - public int update(String sql, Object... args) { - return update(sql, createPreparedStatementSetter(args)); + public int update(String sql, Object... params) { + return update(sql, createPreparedStatementSetter(params)); } private List query(String sql, RowMapper rowMapper, PreparedStatementSetter pss) { @@ -62,10 +62,10 @@ private int update(String sql, PreparedStatementSetter pss) { } } - private PreparedStatementSetter createPreparedStatementSetter(Object... args) { + private PreparedStatementSetter createPreparedStatementSetter(Object... params) { return psmt -> { - for (int i = 0; i < args.length; i++) { - psmt.setObject(i + 1, args[i]); + for (int i = 0; i < params.length; i++) { + psmt.setObject(i + 1, params[i]); } }; } From 52f90a4f0ffd0503380c03b55e5406717cdc4475 Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 09:24:49 +0900 Subject: [PATCH 17/18] =?UTF-8?q?refactor:=20connection=20null=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TransactionSynchronizationManager.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 9fda456c2f..7ec7a245d8 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,10 +1,7 @@ package org.springframework.transaction.support; -import org.springframework.jdbc.CannotGetJdbcConnectionException; - import javax.sql.DataSource; import java.sql.Connection; -import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @@ -17,27 +14,13 @@ private TransactionSynchronizationManager() { public static Connection getResource(DataSource key) { Map map = getMap(); - Connection connection = map.get(key); - try { - if (connection == null || connection.isClosed()) { - connection = key.getConnection(); - map.put(key, connection); - } - return connection; - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection"); - } + return map.get(key); } private static Map getMap() { return resources.get(); } - public static boolean hasConnection(DataSource key) { - Map map = resources.get(); - return map.containsKey(key); - } - public static void bindResource(DataSource key, Connection value) { Map map = resources.get(); map.put(key, value); From 5f659c89f6829bd415b4d0ce5e4d0dc9c24987cc Mon Sep 17 00:00:00 2001 From: kiarakim Date: Sat, 14 Oct 2023 09:26:46 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor:=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=20=EC=BD=9C=EB=B0=B1=20=ED=8C=A8=ED=84=B4=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TxUserService.java | 67 +++++-------------- .../com/techcourse/service/UserService.java | 8 +-- .../techcourse/service/UserServiceTest.java | 31 +++++---- .../support/TransactionExecutor.java | 7 ++ .../support/TransactionTemplate.java | 60 +++++++++++++++++ 5 files changed, 100 insertions(+), 73 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index ce103f4fcc..41c97014d9 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -1,73 +1,36 @@ package com.techcourse.service; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; -import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.SQLTransactionRollbackException; +import org.springframework.transaction.support.TransactionTemplate; public class TxUserService implements UserService { - private UserService userService; - private final DataSource dataSource = DataSourceConfig.getInstance(); + private final TransactionTemplate transactionTemplate; + private final UserService userService; - public TxUserService(UserService userService) { + public TxUserService(TransactionTemplate transactionTemplate, UserService userService) { + this.transactionTemplate = transactionTemplate; this.userService = userService; } @Override - public User findById(long id) throws SQLException { - Connection connection = null; - try { - if (TransactionSynchronizationManager.hasConnection(dataSource)) { - return userService.findById(id); - } - connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - User user = userService.findById(id); - connection.commit(); - DataSourceUtils.releaseConnection(connection, dataSource); - return user; - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } + public User findById(long id) { + return userService.findById(id); } @Override - public void insert(User user) throws SQLException { - Connection connection = null; - try { - if (TransactionSynchronizationManager.hasConnection(dataSource)) { - userService.insert(user); - return; - } - connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); + public void insert(User user) { + transactionTemplate.execute(() -> { userService.insert(user); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } + return null; + }); } @Override - public void changePassword(long id, String newPassword, String createBy) throws SQLException { - Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - try { + public void changePassword(long id, String newPassword, String createBy) { + transactionTemplate.execute(() -> { userService.changePassword(id, newPassword, createBy); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - throw new SQLTransactionRollbackException(e.getMessage()); - } finally { - DataSourceUtils.releaseConnection(connection, dataSource); - } + return null; + }); } } diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 762ab7e729..a054542fd2 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -2,13 +2,11 @@ import com.techcourse.domain.User; -import java.sql.SQLException; - public interface UserService { - User findById(long id) throws SQLException; + User findById(long id); - void insert(User user) throws SQLException; + void insert(User user); - void changePassword(long id, String newPassword, String createBy) throws SQLException; + void changePassword(long id, String newPassword, String createBy); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 5c3d96546f..667b5d30ed 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -9,9 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; - -import javax.sql.DataSource; -import java.sql.SQLException; +import org.springframework.transaction.support.TransactionTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -20,23 +18,23 @@ class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; - private DataSource dataSource; @BeforeEach void setUp() { - dataSource = DataSourceConfig.getInstance(); - DatabasePopulatorUtils.execute(dataSource); - this.jdbcTemplate = new JdbcTemplate(dataSource); + this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); + DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } @Test - void testChangePassword() throws SQLException { + void testChangePassword() { var userHistoryDao = new UserHistoryDao(jdbcTemplate); - var userService = new TxUserService(new AppUserService(userDao, userHistoryDao)); + var transactionTemplate = new TransactionTemplate(DataSourceConfig.getInstance()); + var appUserService = new AppUserService(userDao, userHistoryDao); + var userService = new TxUserService(transactionTemplate, appUserService); var newPassword = "qqqqq"; var createBy = "gugu"; @@ -48,21 +46,22 @@ void testChangePassword() throws SQLException { } @Test - void testTransactionRollback() throws SQLException { + void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + var transactionTemplate = new TransactionTemplate(DataSourceConfig.getInstance()); // 애플리케이션 서비스 - final var appUserService = new AppUserService(userDao, userHistoryDao); + var appUserService = new AppUserService(userDao, userHistoryDao); // 트랜잭션 서비스 추상화 - final var userService = new TxUserService(appUserService); + var userService = new TxUserService(transactionTemplate, appUserService); - final var newPassword = "newPassword"; - final var createBy = "gugu"; + var newPassword = "newPassword"; + var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, () -> userService.changePassword(1L, newPassword, createBy)); - final var actual = userService.findById(1L); + var actual = userService.findById(1L); assertThat(actual.getPassword()).isNotEqualTo(newPassword); } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java new file mode 100644 index 0000000000..db9421c270 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionExecutor.java @@ -0,0 +1,7 @@ +package org.springframework.transaction.support; + +@FunctionalInterface +public interface TransactionExecutor { + + T execute(); +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java new file mode 100644 index 0000000000..f5894ad5a2 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,60 @@ +package org.springframework.transaction.support; + +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 TransactionTemplate { + + private final DataSource dataSource; + + public TransactionTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(TransactionExecutor executor) { + Connection connection = startTransaction(); + try { + T result = executor.execute(); + commit(connection); + return result; + } catch (DataAccessException e) { + rollback(connection); + throw e; + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + TransactionSynchronizationManager.unbindResource(dataSource); + } + } + + private Connection startTransaction() { + Connection connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DataAccessException(e.getMessage(), e); + } + return connection; + } + + private void commit(Connection connection) { + try { + connection.commit(); + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DataAccessException(e.getMessage(), e); + } + } + + private void rollback(Connection connection) { + try { + connection.rollback(); + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DataAccessException(e.getMessage(), e); + } + } +}