From dc70d1ddf0778aa077348d8a98207876ece28523 Mon Sep 17 00:00:00 2001 From: kang-hyungu Date: Thu, 21 Sep 2023 14:11:58 +0900 Subject: [PATCH 01/52] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 37 +++ app/build.gradle | 49 ++-- .../AppWebApplicationInitializer.java | 10 +- .../com/techcourse/ManualHandlerMapping.java | 6 +- .../controller/LoginController.java | 10 +- .../controller/LogoutController.java | 2 +- .../controller/RegisterController.java | 10 +- .../techcourse/controller/UserController.java | 10 +- .../main/java/com/techcourse/dao/UserDao.java | 2 +- .../com/techcourse/dao/UserHistoryDao.java | 2 +- .../service/MockUserHistoryDao.java | 4 +- .../techcourse/service/UserServiceTest.java | 4 +- build.gradle | 50 ++++ jdbc/build.gradle | 28 +- .../dao/DataAccessException.java | 26 ++ .../CannotGetJdbcConnectionException.java | 18 ++ .../jdbc/core}/JdbcTemplate.java | 2 +- .../jdbc/datasource/DataSourceUtils.java | 37 +++ .../TransactionSynchronizationManager.java | 23 ++ .../java/nextstep/jdbc/JdbcTemplateTest.java | 2 +- mvc/build.gradle | 33 +-- .../stereotype}/Controller.java | 2 +- .../springframework/util/ReflectionUtils.java | 38 +++ .../org/springframework/http}/MediaType.java | 2 +- .../SpringServletContainerInitializer.java} | 12 +- .../web/WebApplicationInitializer.java | 2 +- .../web/bind}/annotation/PathVariable.java | 2 +- .../web/bind}/annotation/RequestMapping.java | 4 +- .../web/bind/annotation}/RequestMethod.java | 2 +- .../web/bind}/annotation/RequestParam.java | 2 +- .../web/servlet}/ModelAndView.java | 2 +- .../springframework/web/servlet}/View.java | 2 +- .../web/servlet}/mvc/DispatcherServlet.java | 4 +- .../web/servlet}/mvc/HandlerAdapter.java | 4 +- .../servlet}/mvc/HandlerAdapterRegistry.java | 2 +- .../web/servlet}/mvc/HandlerExecutor.java | 4 +- .../web/servlet}/mvc/HandlerMapping.java | 2 +- .../servlet}/mvc/HandlerMappingRegistry.java | 2 +- .../web/servlet/mvc}/asis/Controller.java | 2 +- .../mvc}/asis/ControllerHandlerAdapter.java | 8 +- .../servlet/mvc}/asis/ForwardController.java | 2 +- .../mvc}/tobe/AnnotationHandlerMapping.java | 8 +- .../servlet/mvc}/tobe/ControllerScanner.java | 4 +- .../servlet/mvc}/tobe/HandlerExecution.java | 4 +- .../tobe/HandlerExecutionHandlerAdapter.java | 6 +- .../web/servlet/mvc}/tobe/HandlerKey.java | 4 +- .../web/servlet}/view/JsonView.java | 5 +- .../web/servlet}/view/JspView.java | 3 +- ...akarta.servlet.ServletContainerInitializer | 2 +- mvc/src/test/java/samples/TestController.java | 10 +- .../tobe/AnnotationHandlerMappingTest.java | 2 +- settings.gradle | 2 +- sonar.gradle | 11 + study/build.gradle | 40 +++ study/src/main/java/aop/App.java | 12 + .../main/java/aop}/DataAccessException.java | 3 +- study/src/main/java/aop/Transactional.java | 10 + .../java/aop/config/DataSourceConfig.java | 20 ++ study/src/main/java/aop/domain/User.java | 56 ++++ .../src/main/java/aop/domain/UserHistory.java | 59 ++++ .../src/main/java/aop/repository/UserDao.java | 51 ++++ .../java/aop/repository/UserHistoryDao.java | 27 ++ .../main/java/aop/service/AppUserService.java | 36 +++ .../main/java/aop/service/TxUserService.java | 48 ++++ .../main/java/aop/service/UserService.java | 12 + study/src/main/java/connectionpool/App.java | 12 + .../java/connectionpool/DataSourceConfig.java | 34 +++ study/src/main/java/transaction/App.java | 12 + .../java/transaction/ConsumerWrapper.java | 24 ++ .../transaction/DatabasePopulatorUtils.java | 46 +++ .../java/transaction/FunctionWrapper.java | 24 ++ .../java/transaction/RunnableWrapper.java | 22 ++ .../java/transaction/ThrowingConsumer.java | 6 + .../java/transaction/ThrowingFunction.java | 6 + .../java/transaction/ThrowingRunnable.java | 6 + study/src/main/resources/application.yml | 17 ++ study/src/main/resources/schema.sql | 16 ++ .../src/test/java/aop/StubUserHistoryDao.java | 19 ++ .../src/test/java/aop/stage0/Stage0Test.java | 73 +++++ .../java/aop/stage0/TransactionHandler.java | 15 + .../src/test/java/aop/stage1/Stage1Test.java | 68 +++++ .../java/aop/stage1/TransactionAdvice.java | 15 + .../java/aop/stage1/TransactionAdvisor.java | 27 ++ .../java/aop/stage1/TransactionPointcut.java | 19 ++ .../src/test/java/aop/stage1/UserService.java | 36 +++ study/src/test/java/aop/stage2/AopConfig.java | 8 + .../src/test/java/aop/stage2/Stage2Test.java | 57 ++++ .../src/test/java/aop/stage2/UserService.java | 42 +++ .../PoolingVsNoPoolingTest.java | 118 ++++++++ .../connectionpool/stage0/Stage0Test.java | 62 ++++ .../connectionpool/stage1/Stage1Test.java | 82 ++++++ .../connectionpool/stage2/Stage2Test.java | 84 ++++++ .../java/transaction/stage1/Stage1Test.java | 265 ++++++++++++++++++ .../test/java/transaction/stage1/User.java | 56 ++++ .../test/java/transaction/stage1/UserDao.java | 66 +++++ .../stage1/jdbc/DataAccessException.java | 25 ++ .../transaction/stage1/jdbc/JdbcTemplate.java | 106 +++++++ .../transaction/stage1/jdbc/KeyHolder.java | 21 ++ .../stage1/jdbc/PreparedStatementCreator.java | 9 + .../stage1/jdbc/PreparedStatementSetter.java | 8 + .../transaction/stage1/jdbc/RowMapper.java | 9 + .../transaction/stage2/FirstUserService.java | 136 +++++++++ .../transaction/stage2/SecondUserService.java | 76 +++++ .../java/transaction/stage2/Stage2Test.java | 152 ++++++++++ .../test/java/transaction/stage2/User.java | 63 +++++ .../transaction/stage2/UserRepository.java | 6 + 106 files changed, 2699 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 build.gradle create mode 100644 jdbc/src/main/java/org/springframework/dao/DataAccessException.java create mode 100644 jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java rename jdbc/src/main/java/{nextstep/jdbc => org/springframework/jdbc/core}/JdbcTemplate.java (89%) create mode 100644 jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java rename mvc/src/main/java/{nextstep/web/annotation => context/org/springframework/stereotype}/Controller.java (87%) create mode 100644 mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java rename mvc/src/main/java/{nextstep/web/support => web/org/springframework/http}/MediaType.java (76%) rename mvc/src/main/java/{nextstep/web/NextstepServletContainerInitializer.java => web/org/springframework/web/SpringServletContainerInitializer.java} (76%) rename mvc/src/main/java/{nextstep => web/org/springframework}/web/WebApplicationInitializer.java (84%) rename mvc/src/main/java/{nextstep/web => web/org/springframework/web/bind}/annotation/PathVariable.java (82%) rename mvc/src/main/java/{nextstep/web => web/org/springframework/web/bind}/annotation/RequestMapping.java (82%) rename mvc/src/main/java/{nextstep/web/support => web/org/springframework/web/bind/annotation}/RequestMethod.java (62%) rename mvc/src/main/java/{nextstep/web => web/org/springframework/web/bind}/annotation/RequestParam.java (82%) rename mvc/src/main/java/{nextstep/mvc/view => webmvc/org/springframework/web/servlet}/ModelAndView.java (93%) rename mvc/src/main/java/{nextstep/mvc/view => webmvc/org/springframework/web/servlet}/View.java (85%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/DispatcherServlet.java (95%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/HandlerAdapter.java (73%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/HandlerAdapterRegistry.java (91%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/HandlerExecutor.java (85%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/HandlerMapping.java (76%) rename mvc/src/main/java/{nextstep => webmvc/org/springframework/web/servlet}/mvc/HandlerMappingRegistry.java (92%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/asis/Controller.java (80%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/asis/ControllerHandlerAdapter.java (70%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/asis/ForwardController.java (89%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/AnnotationHandlerMapping.java (92%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/ControllerScanner.java (91%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/HandlerExecution.java (90%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/HandlerExecutionHandlerAdapter.java (74%) rename mvc/src/main/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/HandlerKey.java (86%) rename mvc/src/main/java/{nextstep/mvc => webmvc/org/springframework/web/servlet}/view/JsonView.java (89%) rename mvc/src/main/java/{nextstep/mvc => webmvc/org/springframework/web/servlet}/view/JspView.java (92%) rename mvc/src/test/java/{nextstep/mvc/controller => webmvc/org/springframework/web/servlet/mvc}/tobe/AnnotationHandlerMappingTest.java (96%) create mode 100644 sonar.gradle create mode 100644 study/build.gradle create mode 100644 study/src/main/java/aop/App.java rename {jdbc/src/main/java/nextstep/jdbc => study/src/main/java/aop}/DataAccessException.java (96%) create mode 100644 study/src/main/java/aop/Transactional.java create mode 100644 study/src/main/java/aop/config/DataSourceConfig.java create mode 100644 study/src/main/java/aop/domain/User.java create mode 100644 study/src/main/java/aop/domain/UserHistory.java create mode 100644 study/src/main/java/aop/repository/UserDao.java create mode 100644 study/src/main/java/aop/repository/UserHistoryDao.java create mode 100644 study/src/main/java/aop/service/AppUserService.java create mode 100644 study/src/main/java/aop/service/TxUserService.java create mode 100644 study/src/main/java/aop/service/UserService.java create mode 100644 study/src/main/java/connectionpool/App.java create mode 100644 study/src/main/java/connectionpool/DataSourceConfig.java create mode 100644 study/src/main/java/transaction/App.java create mode 100644 study/src/main/java/transaction/ConsumerWrapper.java create mode 100644 study/src/main/java/transaction/DatabasePopulatorUtils.java create mode 100644 study/src/main/java/transaction/FunctionWrapper.java create mode 100644 study/src/main/java/transaction/RunnableWrapper.java create mode 100644 study/src/main/java/transaction/ThrowingConsumer.java create mode 100644 study/src/main/java/transaction/ThrowingFunction.java create mode 100644 study/src/main/java/transaction/ThrowingRunnable.java create mode 100644 study/src/main/resources/application.yml create mode 100644 study/src/main/resources/schema.sql create mode 100644 study/src/test/java/aop/StubUserHistoryDao.java create mode 100644 study/src/test/java/aop/stage0/Stage0Test.java create mode 100644 study/src/test/java/aop/stage0/TransactionHandler.java create mode 100644 study/src/test/java/aop/stage1/Stage1Test.java create mode 100644 study/src/test/java/aop/stage1/TransactionAdvice.java create mode 100644 study/src/test/java/aop/stage1/TransactionAdvisor.java create mode 100644 study/src/test/java/aop/stage1/TransactionPointcut.java create mode 100644 study/src/test/java/aop/stage1/UserService.java create mode 100644 study/src/test/java/aop/stage2/AopConfig.java create mode 100644 study/src/test/java/aop/stage2/Stage2Test.java create mode 100644 study/src/test/java/aop/stage2/UserService.java create mode 100644 study/src/test/java/connectionpool/PoolingVsNoPoolingTest.java create mode 100644 study/src/test/java/connectionpool/stage0/Stage0Test.java create mode 100644 study/src/test/java/connectionpool/stage1/Stage1Test.java create mode 100644 study/src/test/java/connectionpool/stage2/Stage2Test.java create mode 100644 study/src/test/java/transaction/stage1/Stage1Test.java create mode 100644 study/src/test/java/transaction/stage1/User.java create mode 100644 study/src/test/java/transaction/stage1/UserDao.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/DataAccessException.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/KeyHolder.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/PreparedStatementCreator.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/PreparedStatementSetter.java create mode 100644 study/src/test/java/transaction/stage1/jdbc/RowMapper.java create mode 100644 study/src/test/java/transaction/stage2/FirstUserService.java create mode 100644 study/src/test/java/transaction/stage2/SecondUserService.java create mode 100644 study/src/test/java/transaction/stage2/Stage2Test.java create mode 100644 study/src/test/java/transaction/stage2/User.java create mode 100644 study/src/test/java/transaction/stage2/UserRepository.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..179fbb276b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: SonarCloud +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: 'corretto' # Alternative distribution options are available + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew clean build codeCoverageReport sonar --info -x :study:build diff --git a/app/build.gradle b/app/build.gradle index a94011f4db..00444e6f91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,11 @@ plugins { - id 'java' - id 'idea' + id "java" + id "idea" + id "jacoco" } -group 'org.example' -version '1.0-SNAPSHOT' +group "org.example" +version "1.0-SNAPSHOT" sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -14,27 +15,23 @@ repositories { } dependencies { - implementation project(':mvc') - implementation project(':jdbc') - - implementation 'org.springframework:spring-tx:5.3.23' - implementation 'org.springframework:spring-jdbc:5.3.23' - - implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.0-M16' - implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:10.1.0-M16' - implementation 'ch.qos.logback:logback-classic:1.2.10' - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' - implementation 'com.h2database:h2:2.1.214' - - testImplementation 'org.assertj:assertj-core:3.22.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testImplementation 'org.mockito:mockito-core:3.+' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' -} - -test { - useJUnitPlatform() + implementation project(":mvc") + implementation project(":jdbc") + + implementation "org.springframework:spring-tx:5.3.23" + implementation "org.springframework:spring-jdbc:5.3.23" + + implementation "org.apache.tomcat.embed:tomcat-embed-core:10.1.13" + implementation "org.apache.tomcat.embed:tomcat-embed-jasper:10.1.13" + implementation "ch.qos.logback:logback-classic:1.2.12" + implementation "org.apache.commons:commons-lang3:3.13.0" + implementation "com.fasterxml.jackson.core:jackson-databind:2.15.2" + implementation "com.h2database:h2:2.2.220" + + testImplementation "org.assertj:assertj-core:3.24.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "org.mockito:mockito-core:5.4.0" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } idea { @@ -46,6 +43,6 @@ idea { sourceSets { main { - java.destinationDirectory.set(file('src/main/webapp/WEB-INF/classes')) + java.destinationDirectory.set(file("src/main/webapp/WEB-INF/classes")) } } diff --git a/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java b/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java index a0ea9347c1..024510e26c 100644 --- a/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java +++ b/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java @@ -1,11 +1,11 @@ package com.techcourse; import jakarta.servlet.ServletContext; -import nextstep.mvc.DispatcherServlet; -import nextstep.mvc.controller.asis.ControllerHandlerAdapter; -import nextstep.mvc.controller.tobe.AnnotationHandlerMapping; -import nextstep.mvc.controller.tobe.HandlerExecutionHandlerAdapter; -import nextstep.web.WebApplicationInitializer; +import webmvc.org.springframework.web.servlet.mvc.DispatcherServlet; +import webmvc.org.springframework.web.servlet.mvc.asis.ControllerHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerExecutionHandlerAdapter; +import web.org.springframework.web.WebApplicationInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java index 3d14ca6be8..6119278f93 100644 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ b/app/src/main/java/com/techcourse/ManualHandlerMapping.java @@ -2,9 +2,9 @@ import com.techcourse.controller.*; import jakarta.servlet.http.HttpServletRequest; -import nextstep.mvc.HandlerMapping; -import nextstep.mvc.controller.asis.Controller; -import nextstep.mvc.controller.asis.ForwardController; +import webmvc.org.springframework.web.servlet.mvc.HandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/com/techcourse/controller/LoginController.java b/app/src/main/java/com/techcourse/controller/LoginController.java index 492e246fd2..2f1d2d7e25 100644 --- a/app/src/main/java/com/techcourse/controller/LoginController.java +++ b/app/src/main/java/com/techcourse/controller/LoginController.java @@ -4,11 +4,11 @@ import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.JspView; -import nextstep.mvc.view.ModelAndView; -import nextstep.web.annotation.Controller; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; +import webmvc.org.springframework.web.servlet.view.JspView; +import webmvc.org.springframework.web.servlet.ModelAndView; +import context.org.springframework.stereotype.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/com/techcourse/controller/LogoutController.java b/app/src/main/java/com/techcourse/controller/LogoutController.java index 9d1f099a98..4642fd9450 100644 --- a/app/src/main/java/com/techcourse/controller/LogoutController.java +++ b/app/src/main/java/com/techcourse/controller/LogoutController.java @@ -2,7 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; public class LogoutController implements Controller { diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index dcbbbc64e6..8c794c2d26 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -4,11 +4,11 @@ import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.JspView; -import nextstep.mvc.view.ModelAndView; -import nextstep.web.annotation.Controller; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; +import webmvc.org.springframework.web.servlet.view.JspView; +import webmvc.org.springframework.web.servlet.ModelAndView; +import context.org.springframework.stereotype.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; @Controller public class RegisterController { diff --git a/app/src/main/java/com/techcourse/controller/UserController.java b/app/src/main/java/com/techcourse/controller/UserController.java index 478c90eb06..d17bbcf0bb 100644 --- a/app/src/main/java/com/techcourse/controller/UserController.java +++ b/app/src/main/java/com/techcourse/controller/UserController.java @@ -3,11 +3,11 @@ import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.JsonView; -import nextstep.mvc.view.ModelAndView; -import nextstep.web.annotation.Controller; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; +import webmvc.org.springframework.web.servlet.view.JsonView; +import webmvc.org.springframework.web.servlet.ModelAndView; +import context.org.springframework.stereotype.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index ee17c159c2..d14c545f34 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,7 +1,7 @@ package com.techcourse.dao; import com.techcourse.domain.User; -import nextstep.jdbc.JdbcTemplate; +import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index a506912cf4..edb4338caa 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,7 +1,7 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import nextstep.jdbc.JdbcTemplate; +import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index da3f2cee4b..2ee12b195f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,8 +2,8 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import nextstep.jdbc.DataAccessException; -import nextstep.jdbc.JdbcTemplate; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; public class MockUserHistoryDao extends UserHistoryDao { diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index a22c3c7a82..255a0ebfe7 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,8 +5,8 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import nextstep.jdbc.DataAccessException; -import nextstep.jdbc.JdbcTemplate; +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; diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..fd90bc151f --- /dev/null +++ b/build.gradle @@ -0,0 +1,50 @@ +plugins { + id "java" + id "jacoco" + id "org.sonarqube" version "4.2.1.3168" +} + +subprojects { + apply plugin: 'org.sonarqube' + sonar { + properties { + property 'sonar.coverage.jacoco.xmlReportPaths', "$projectDir.parentFile.path/build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml" + } + } + + tasks.withType(Test).configureEach { + maxParallelForks 3 + useJUnitPlatform() + } +} + +apply from: "$project.rootDir/sonar.gradle" + +// $ ./gradlew test codeCoverageReport +tasks.register("codeCoverageReport", JacocoReport) { + // If a subproject applies the 'jacoco' plugin, add the result it to the report + subprojects { subproject -> + subproject.plugins.withType(JacocoPlugin).configureEach { + subproject.tasks.matching({ t -> t.extensions.findByType(JacocoTaskExtension) }).configureEach { testTask -> + //the jacoco extension may be disabled for some projects + if (testTask.extensions.getByType(JacocoTaskExtension).isEnabled()) { + sourceSets subproject.sourceSets.main + executionData(testTask) + } else { + logger.warn('Jacoco extension is disabled for test task \'{}\' in project \'{}\'. this test task will be excluded from jacoco report.',testTask.getName(),subproject.getName()) + } + } + + subproject.tasks.matching({ t -> t.extensions.findByType(JacocoTaskExtension) }).forEach { + rootProject.tasks.codeCoverageReport.dependsOn(it) + } + } + } + + // enable the different report types (html, xml, csv) + reports { + xml.required = true + html.required = true + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} diff --git a/jdbc/build.gradle b/jdbc/build.gradle index c5f680359c..83f293f626 100644 --- a/jdbc/build.gradle +++ b/jdbc/build.gradle @@ -1,26 +1,22 @@ plugins { - id 'java' + id "java" + id "jacoco" } +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 + repositories { mavenCentral() } dependencies { - implementation 'org.springframework:spring-tx:5.3.23' - implementation 'org.springframework:spring-jdbc:5.3.23' - - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'ch.qos.logback:logback-classic:1.2.10' - - implementation 'org.reflections:reflections:0.10.2' - - testImplementation 'org.assertj:assertj-core:3.22.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testImplementation 'org.mockito:mockito-core:3.+' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' -} + implementation "org.reflections:reflections:0.10.2" + implementation "org.apache.commons:commons-lang3:3.13.0" + implementation "ch.qos.logback:logback-classic:1.2.12" -test { - useJUnitPlatform() + testImplementation "org.assertj:assertj-core:3.24.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "org.mockito:mockito-core:5.4.0" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } diff --git a/jdbc/src/main/java/org/springframework/dao/DataAccessException.java b/jdbc/src/main/java/org/springframework/dao/DataAccessException.java new file mode 100644 index 0000000000..8f81f3ede9 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/dao/DataAccessException.java @@ -0,0 +1,26 @@ +package org.springframework.dao; + +public class DataAccessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public DataAccessException() { + super(); + } + + public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public DataAccessException(String message, Throwable cause) { + super(message, cause); + } + + public DataAccessException(String message) { + super(message); + } + + public DataAccessException(Throwable cause) { + super(cause); + } +} diff --git a/jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java b/jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java new file mode 100644 index 0000000000..c5c3bcd010 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java @@ -0,0 +1,18 @@ +package org.springframework.jdbc; + +import java.sql.SQLException; + +public class CannotGetJdbcConnectionException extends RuntimeException { + + public CannotGetJdbcConnectionException(String msg) { + super(msg); + } + + public CannotGetJdbcConnectionException(String msg, SQLException ex) { + super(msg, ex); + } + + public CannotGetJdbcConnectionException(String msg, IllegalStateException ex) { + super(msg, ex); + } +} diff --git a/jdbc/src/main/java/nextstep/jdbc/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java similarity index 89% rename from jdbc/src/main/java/nextstep/jdbc/JdbcTemplate.java rename to jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 613a588a93..52a0d30a17 100644 --- a/jdbc/src/main/java/nextstep/jdbc/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,4 +1,4 @@ -package nextstep.jdbc; +package org.springframework.jdbc.core; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java new file mode 100644 index 0000000000..3c40bfec52 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -0,0 +1,37 @@ +package org.springframework.jdbc.datasource; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +// 4단계 미션에서 사용할 것 +public abstract class DataSourceUtils { + + private DataSourceUtils() {} + + public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { + Connection connection = TransactionSynchronizationManager.getResource(dataSource); + if (connection != null) { + return connection; + } + + try { + connection = dataSource.getConnection(); + TransactionSynchronizationManager.bindResource(dataSource, connection); + return connection; + } catch (SQLException ex) { + throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); + } + } + + public static void releaseConnection(Connection connection, DataSource dataSource) { + try { + 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 new file mode 100644 index 0000000000..715557fc66 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -0,0 +1,23 @@ +package org.springframework.transaction.support; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.Map; + +public abstract class TransactionSynchronizationManager { + + private static final ThreadLocal> resources = new ThreadLocal<>(); + + private TransactionSynchronizationManager() {} + + public static Connection getResource(DataSource key) { + return null; + } + + public static void bindResource(DataSource key, Connection value) { + } + + public static Connection unbindResource(DataSource key) { + return null; + } +} diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 040f689480..d777c8becf 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -2,4 +2,4 @@ class JdbcTemplateTest { -} \ No newline at end of file +} diff --git a/mvc/build.gradle b/mvc/build.gradle index 7c7b100327..8cdd011952 100644 --- a/mvc/build.gradle +++ b/mvc/build.gradle @@ -1,5 +1,6 @@ plugins { - id 'java' + id "java" + id "jacoco" } sourceCompatibility = JavaVersion.VERSION_11 @@ -10,25 +11,21 @@ repositories { } dependencies { - implementation 'jakarta.servlet:jakarta.servlet-api:5.0.0' - implementation 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.3' - implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0' - implementation 'jakarta.annotation:jakarta.annotation-api:2.0.0' - annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.0.0' + implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" + implementation "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" + implementation "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0" + implementation "jakarta.annotation:jakarta.annotation-api:2.0.0" + annotationProcessor "jakarta.annotation:jakarta.annotation-api:2.0.0" - implementation 'org.reflections:reflections:0.10.2' + implementation "org.reflections:reflections:0.10.2" - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' + implementation "com.fasterxml.jackson.core:jackson-databind:2.15.2" - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'ch.qos.logback:logback-classic:1.2.10' + implementation "org.apache.commons:commons-lang3:3.13.0" + implementation "ch.qos.logback:logback-classic:1.2.12" - testImplementation 'org.assertj:assertj-core:3.22.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testImplementation 'org.mockito:mockito-core:3.+' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' -} - -test { - useJUnitPlatform() + testImplementation "org.assertj:assertj-core:3.24.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "org.mockito:mockito-core:5.4.0" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } diff --git a/mvc/src/main/java/nextstep/web/annotation/Controller.java b/mvc/src/main/java/context/org/springframework/stereotype/Controller.java similarity index 87% rename from mvc/src/main/java/nextstep/web/annotation/Controller.java rename to mvc/src/main/java/context/org/springframework/stereotype/Controller.java index cb264235b4..ef7379b54a 100644 --- a/mvc/src/main/java/nextstep/web/annotation/Controller.java +++ b/mvc/src/main/java/context/org/springframework/stereotype/Controller.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package context.org.springframework.stereotype; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java new file mode 100644 index 0000000000..e89c1743f3 --- /dev/null +++ b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java @@ -0,0 +1,38 @@ +package core.org.springframework.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; + +public abstract class ReflectionUtils { + + /** + * Obtain an accessible constructor for the given class and parameters. + * @param clazz the clazz to check + * @param parameterTypes the parameter types of the desired constructor + * @return the constructor reference + * @throws NoSuchMethodException if no such constructor exists + * @since 5.0 + */ + public static Constructor accessibleConstructor(Class clazz, Class... parameterTypes) + throws NoSuchMethodException { + + Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); + makeAccessible(ctor); + return ctor; + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts. + * @param ctor the constructor to make accessible + * @see Constructor#setAccessible + */ + @SuppressWarnings("deprecation") + public static void makeAccessible(Constructor ctor) { + if ((!Modifier.isPublic(ctor.getModifiers()) || + !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { + ctor.setAccessible(true); + } + } +} diff --git a/mvc/src/main/java/nextstep/web/support/MediaType.java b/mvc/src/main/java/web/org/springframework/http/MediaType.java similarity index 76% rename from mvc/src/main/java/nextstep/web/support/MediaType.java rename to mvc/src/main/java/web/org/springframework/http/MediaType.java index f881e02174..3a31f51d33 100644 --- a/mvc/src/main/java/nextstep/web/support/MediaType.java +++ b/mvc/src/main/java/web/org/springframework/http/MediaType.java @@ -1,4 +1,4 @@ -package nextstep.web.support; +package web.org.springframework.http; public class MediaType { public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8"; diff --git a/mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java b/mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java similarity index 76% rename from mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java rename to mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java index 3e79b34b6a..b0e900ad6e 100644 --- a/mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java +++ b/mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java @@ -1,5 +1,6 @@ -package nextstep.web; +package web.org.springframework.web; +import core.org.springframework.util.ReflectionUtils; import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; @@ -10,7 +11,7 @@ import java.util.Set; @HandlesTypes(WebApplicationInitializer.class) -public class NextstepServletContainerInitializer implements ServletContainerInitializer { +public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) @@ -20,9 +21,10 @@ public void onStartup(Set> webAppInitializerClasses, ServletContext ser if (webAppInitializerClasses != null) { for (Class waiClass : webAppInitializerClasses) { try { - initializers.add((WebApplicationInitializer) waiClass.getDeclaredConstructor().newInstance()); - } catch (Throwable e) { - throw new ServletException("Failed to instantiate WebApplicationInitializer class", e); + initializers.add((WebApplicationInitializer) + ReflectionUtils.accessibleConstructor(waiClass).newInstance()); + } catch (Throwable ex) { + throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } diff --git a/mvc/src/main/java/nextstep/web/WebApplicationInitializer.java b/mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java similarity index 84% rename from mvc/src/main/java/nextstep/web/WebApplicationInitializer.java rename to mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java index 60e6a89543..4b9b7d893d 100644 --- a/mvc/src/main/java/nextstep/web/WebApplicationInitializer.java +++ b/mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java @@ -1,4 +1,4 @@ -package nextstep.web; +package web.org.springframework.web; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; diff --git a/mvc/src/main/java/nextstep/web/annotation/PathVariable.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/PathVariable.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java index 4f2a9b50a5..ceefd548e1 100644 --- a/mvc/src/main/java/nextstep/web/annotation/PathVariable.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.*; diff --git a/mvc/src/main/java/nextstep/web/annotation/RequestMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/RequestMapping.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java index bb8c9e6e7b..6a09dfa0c4 100644 --- a/mvc/src/main/java/nextstep/web/annotation/RequestMapping.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,6 +1,4 @@ -package nextstep.web.annotation; - -import nextstep.web.support.RequestMethod; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/mvc/src/main/java/nextstep/web/support/RequestMethod.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java similarity index 62% rename from mvc/src/main/java/nextstep/web/support/RequestMethod.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java index 1f37f21d5a..1dd958bd23 100644 --- a/mvc/src/main/java/nextstep/web/support/RequestMethod.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java @@ -1,4 +1,4 @@ -package nextstep.web.support; +package web.org.springframework.web.bind.annotation; public enum RequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE diff --git a/mvc/src/main/java/nextstep/web/annotation/RequestParam.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/RequestParam.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java index 2813247c4a..5b3be4cedc 100644 --- a/mvc/src/main/java/nextstep/web/annotation/RequestParam.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.*; diff --git a/mvc/src/main/java/nextstep/mvc/view/ModelAndView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java similarity index 93% rename from mvc/src/main/java/nextstep/mvc/view/ModelAndView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java index cb172084b3..ff8e24553f 100644 --- a/mvc/src/main/java/nextstep/mvc/view/ModelAndView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java @@ -1,4 +1,4 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet; import java.util.Collections; import java.util.HashMap; diff --git a/mvc/src/main/java/nextstep/mvc/view/View.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java similarity index 85% rename from mvc/src/main/java/nextstep/mvc/view/View.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java index f8cd1dba1a..ec335db0a0 100644 --- a/mvc/src/main/java/nextstep/mvc/view/View.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java @@ -1,4 +1,4 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/DispatcherServlet.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/DispatcherServlet.java similarity index 95% rename from mvc/src/main/java/nextstep/mvc/DispatcherServlet.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/DispatcherServlet.java index e68ca5f32c..c8d74cf60b 100644 --- a/mvc/src/main/java/nextstep/mvc/DispatcherServlet.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/DispatcherServlet.java @@ -1,10 +1,10 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.ModelAndView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mvc/src/main/java/nextstep/mvc/HandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java similarity index 73% rename from mvc/src/main/java/nextstep/mvc/HandlerAdapter.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java index ebecdd2e1f..4475d5a206 100644 --- a/mvc/src/main/java/nextstep/mvc/HandlerAdapter.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapter.java @@ -1,6 +1,6 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/HandlerAdapterRegistry.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapterRegistry.java similarity index 91% rename from mvc/src/main/java/nextstep/mvc/HandlerAdapterRegistry.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapterRegistry.java index cbf1a9810a..bd960168db 100644 --- a/mvc/src/main/java/nextstep/mvc/HandlerAdapterRegistry.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdapterRegistry.java @@ -1,4 +1,4 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; import java.util.ArrayList; import java.util.List; diff --git a/mvc/src/main/java/nextstep/mvc/HandlerExecutor.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerExecutor.java similarity index 85% rename from mvc/src/main/java/nextstep/mvc/HandlerExecutor.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerExecutor.java index abd7cba029..2ac3daddaf 100644 --- a/mvc/src/main/java/nextstep/mvc/HandlerExecutor.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerExecutor.java @@ -1,8 +1,8 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.ModelAndView; public class HandlerExecutor { diff --git a/mvc/src/main/java/nextstep/mvc/HandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java similarity index 76% rename from mvc/src/main/java/nextstep/mvc/HandlerMapping.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java index 9c1f61f239..667715f716 100644 --- a/mvc/src/main/java/nextstep/mvc/HandlerMapping.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java @@ -1,4 +1,4 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/nextstep/mvc/HandlerMappingRegistry.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java similarity index 92% rename from mvc/src/main/java/nextstep/mvc/HandlerMappingRegistry.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java index 608fa6e86f..edc8430332 100644 --- a/mvc/src/main/java/nextstep/mvc/HandlerMappingRegistry.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java @@ -1,4 +1,4 @@ -package nextstep.mvc; +package webmvc.org.springframework.web.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; diff --git a/mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java similarity index 80% rename from mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java index b0edabf3fb..bdd1fde780 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java @@ -1,4 +1,4 @@ -package nextstep.mvc.controller.asis; +package webmvc.org.springframework.web.servlet.mvc.asis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/controller/asis/ControllerHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ControllerHandlerAdapter.java similarity index 70% rename from mvc/src/main/java/nextstep/mvc/controller/asis/ControllerHandlerAdapter.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ControllerHandlerAdapter.java index 9faf37411f..6c02851b91 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/asis/ControllerHandlerAdapter.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ControllerHandlerAdapter.java @@ -1,10 +1,10 @@ -package nextstep.mvc.controller.asis; +package webmvc.org.springframework.web.servlet.mvc.asis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.HandlerAdapter; -import nextstep.mvc.view.JspView; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter; +import webmvc.org.springframework.web.servlet.view.JspView; +import webmvc.org.springframework.web.servlet.ModelAndView; public class ControllerHandlerAdapter implements HandlerAdapter { diff --git a/mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java similarity index 89% rename from mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java index ed0f08d940..cd8f1ef371 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java @@ -1,4 +1,4 @@ -package nextstep.mvc.controller.asis; +package webmvc.org.springframework.web.servlet.mvc.asis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java similarity index 92% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java index af90da57b1..5505b3eb2b 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -1,9 +1,9 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; import jakarta.servlet.http.HttpServletRequest; -import nextstep.mvc.HandlerMapping; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; +import webmvc.org.springframework.web.servlet.mvc.HandlerMapping; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; import org.reflections.ReflectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/ControllerScanner.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java similarity index 91% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/ControllerScanner.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java index d96ff1f849..4ced894a3d 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/ControllerScanner.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java @@ -1,6 +1,6 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; -import nextstep.web.annotation.Controller; +import context.org.springframework.stereotype.Controller; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java similarity index 90% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java index 2d4bceb977..22fa2dd98c 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java @@ -1,8 +1,8 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.ModelAndView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecutionHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdapter.java similarity index 74% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecutionHandlerAdapter.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdapter.java index 8d10ec2324..071cb6dcf0 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecutionHandlerAdapter.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdapter.java @@ -1,9 +1,9 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.HandlerAdapter; -import nextstep.mvc.view.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter; +import webmvc.org.springframework.web.servlet.ModelAndView; public class HandlerExecutionHandlerAdapter implements HandlerAdapter { diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java similarity index 86% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java index 66f14a9deb..ce758bbdc1 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java @@ -1,6 +1,6 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; -import nextstep.web.support.RequestMethod; +import web.org.springframework.web.bind.annotation.RequestMethod; import java.util.Objects; diff --git a/mvc/src/main/java/nextstep/mvc/view/JsonView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java similarity index 89% rename from mvc/src/main/java/nextstep/mvc/view/JsonView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java index b24ab650fd..63c5684044 100644 --- a/mvc/src/main/java/nextstep/mvc/view/JsonView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java @@ -1,10 +1,11 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet.view; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.web.support.MediaType; +import web.org.springframework.http.MediaType; +import webmvc.org.springframework.web.servlet.View; import java.io.IOException; import java.util.Map; diff --git a/mvc/src/main/java/nextstep/mvc/view/JspView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java similarity index 92% rename from mvc/src/main/java/nextstep/mvc/view/JspView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java index 63c65c9f6b..c61ee1846c 100644 --- a/mvc/src/main/java/nextstep/mvc/view/JspView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java @@ -1,9 +1,10 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet.view; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.View; import java.util.Map; import java.util.Objects; diff --git a/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer index b9002355ee..d98fc63525 100644 --- a/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer +++ b/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -1 +1 @@ -nextstep.web.NextstepServletContainerInitializer \ No newline at end of file +web.org.springframework.web.SpringServletContainerInitializer diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 49d81be351..fbed040d78 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -2,11 +2,11 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.JspView; -import nextstep.mvc.view.ModelAndView; -import nextstep.web.annotation.Controller; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; +import webmvc.org.springframework.web.servlet.view.JspView; +import webmvc.org.springframework.web.servlet.ModelAndView; +import context.org.springframework.stereotype.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java similarity index 96% rename from mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java rename to mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 110a45bab1..af8907e236 100644 --- a/mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,4 +1,4 @@ -package nextstep.mvc.controller.tobe; +package webmvc.org.springframework.web.servlet.mvc.tobe; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/settings.gradle b/settings.gradle index f19833b340..df1d532345 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'jwp-dashboard-jdbc' - include 'jdbc' include 'mvc' include 'app' +include 'study' diff --git a/sonar.gradle b/sonar.gradle new file mode 100644 index 0000000000..249f388452 --- /dev/null +++ b/sonar.gradle @@ -0,0 +1,11 @@ +apply plugin: "org.sonarqube" + +sonar { + properties { + property "sonar.projectKey", "woowacourse_jwp-dashboard-jdbc" + property "sonar.organization", "woowacourse" + property "sonar.host.url", "https://sonarcloud.io" + + property "sonar.exclusions", "study/**" + } +} diff --git a/study/build.gradle b/study/build.gradle new file mode 100644 index 0000000000..c7aff0460e --- /dev/null +++ b/study/build.gradle @@ -0,0 +1,40 @@ +plugins { + id "org.springframework.boot" version "2.7.4" + id "io.spring.dependency-management" version "1.0.11.RELEASE" + id "java" +} + +group "nextstep" +version "1.0-SNAPSHOT" + +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.springframework.boot:spring-boot-starter-web:2.7.14" + implementation "org.springframework.boot:spring-boot-starter-jdbc:2.7.14" + implementation "org.springframework.boot:spring-boot-starter-data-jpa:2.7.14" + + implementation "com.h2database:h2:2.2.220" + implementation "org.testcontainers:mysql:1.17.3" + + implementation "org.reflections:reflections:0.10.2" + implementation "ch.qos.logback:logback-classic:1.2.12" + implementation "org.apache.commons:commons-lang3:3.13.0" + + testImplementation "org.springframework.boot:spring-boot-starter-test:2.7.14" + testImplementation "org.assertj:assertj-core:3.24.2" + testImplementation "org.mockito:mockito-core:5.4.0" + testImplementation "org.junit.jupiter:junit-jupiter-engine:5.7.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "mysql:mysql-connector-java:8.0.30" +} + +test { + maxParallelForks 3 + useJUnitPlatform() +} diff --git a/study/src/main/java/aop/App.java b/study/src/main/java/aop/App.java new file mode 100644 index 0000000000..9af82bd63f --- /dev/null +++ b/study/src/main/java/aop/App.java @@ -0,0 +1,12 @@ +package aop; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/jdbc/src/main/java/nextstep/jdbc/DataAccessException.java b/study/src/main/java/aop/DataAccessException.java similarity index 96% rename from jdbc/src/main/java/nextstep/jdbc/DataAccessException.java rename to study/src/main/java/aop/DataAccessException.java index ac0e0a3a3b..9fd069c493 100644 --- a/jdbc/src/main/java/nextstep/jdbc/DataAccessException.java +++ b/study/src/main/java/aop/DataAccessException.java @@ -1,7 +1,6 @@ -package nextstep.jdbc; +package aop; public class DataAccessException extends RuntimeException { - private static final long serialVersionUID = 1L; public DataAccessException() { diff --git a/study/src/main/java/aop/Transactional.java b/study/src/main/java/aop/Transactional.java new file mode 100644 index 0000000000..c9b91810c7 --- /dev/null +++ b/study/src/main/java/aop/Transactional.java @@ -0,0 +1,10 @@ +package aop; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Transactional { +} diff --git a/study/src/main/java/aop/config/DataSourceConfig.java b/study/src/main/java/aop/config/DataSourceConfig.java new file mode 100644 index 0000000000..b27245b932 --- /dev/null +++ b/study/src/main/java/aop/config/DataSourceConfig.java @@ -0,0 +1,20 @@ +package aop.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import javax.sql.DataSource; + +@Configuration +public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("classpath:schema.sql") + .build(); + } +} diff --git a/study/src/main/java/aop/domain/User.java b/study/src/main/java/aop/domain/User.java new file mode 100644 index 0000000000..fb8f4d80c6 --- /dev/null +++ b/study/src/main/java/aop/domain/User.java @@ -0,0 +1,56 @@ +package aop.domain; + +public class User { + + private Long id; + private final String account; + private String password; + private final String email; + + public User(long id, String account, String password, String email) { + this.id = id; + this.account = account; + this.password = password; + this.email = email; + } + + public User(String account, String password, String email) { + this.account = account; + this.password = password; + this.email = email; + } + + public boolean checkPassword(String password) { + return this.password.equals(password); + } + + public void changePassword(String password) { + this.password = password; + } + + public String getAccount() { + return account; + } + + public long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/study/src/main/java/aop/domain/UserHistory.java b/study/src/main/java/aop/domain/UserHistory.java new file mode 100644 index 0000000000..13403450ae --- /dev/null +++ b/study/src/main/java/aop/domain/UserHistory.java @@ -0,0 +1,59 @@ +package aop.domain; + +import java.time.LocalDateTime; + +public class UserHistory { + + private Long id; + + private final long userId; + private final String account; + private final String password; + private final String email; + + private final LocalDateTime createdAt; + + private final String createBy; + + public UserHistory(final User user, final String createBy) { + this(null, user.getId(), user.getAccount(), user.getPassword(), user.getEmail(), createBy); + } + + public UserHistory(final Long id, final long userId, final String account, final String password, final String email, final String createBy) { + this.id = id; + this.userId = userId; + this.account = account; + this.password = password; + this.email = email; + this.createdAt = LocalDateTime.now(); + this.createBy = createBy; + } + + public Long getId() { + return id; + } + + public long getUserId() { + return userId; + } + + public String getAccount() { + return account; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public String getCreateBy() { + return createBy; + } +} diff --git a/study/src/main/java/aop/repository/UserDao.java b/study/src/main/java/aop/repository/UserDao.java new file mode 100644 index 0000000000..c9463f7323 --- /dev/null +++ b/study/src/main/java/aop/repository/UserDao.java @@ -0,0 +1,51 @@ +package aop.repository; + +import aop.domain.User; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class UserDao { + + private final JdbcTemplate jdbcTemplate; + + public UserDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void insert(final User user) { + final var sql = "insert into users (account, password, email) values (?, ?, ?)"; + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + } + + public void update(final User user) { + final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + } + + public List findAll() { + final var sql = "select id, account, password, email from users"; + return jdbcTemplate.query(sql, createRowMapper()); + } + + public User findById(final Long id) { + final var sql = "select id, account, password, email from users where id = ?"; + return jdbcTemplate.queryForObject(sql, createRowMapper(), id); + } + + public User findByAccount(final String account) { + final var sql = "select id, account, password, email from users where account = ?"; + return jdbcTemplate.queryForObject(sql, createRowMapper(), account); + } + + private static RowMapper createRowMapper() { + return (final var rs, final var i) -> new User( + rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")); + } +} diff --git a/study/src/main/java/aop/repository/UserHistoryDao.java b/study/src/main/java/aop/repository/UserHistoryDao.java new file mode 100644 index 0000000000..7c5a28d050 --- /dev/null +++ b/study/src/main/java/aop/repository/UserHistoryDao.java @@ -0,0 +1,27 @@ +package aop.repository; + +import aop.domain.UserHistory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class UserHistoryDao { + + private final JdbcTemplate jdbcTemplate; + + public UserHistoryDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void log(final UserHistory userHistory) { + final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; + jdbcTemplate.update(sql, + userHistory.getUserId(), + userHistory.getAccount(), + userHistory.getPassword(), + userHistory.getEmail(), + userHistory.getCreatedAt(), + userHistory.getCreateBy() + ); + } +} diff --git a/study/src/main/java/aop/service/AppUserService.java b/study/src/main/java/aop/service/AppUserService.java new file mode 100644 index 0000000000..cfa4de3b4a --- /dev/null +++ b/study/src/main/java/aop/service/AppUserService.java @@ -0,0 +1,36 @@ +package aop.service; + +import aop.Transactional; +import aop.domain.User; +import aop.domain.UserHistory; +import aop.repository.UserDao; +import aop.repository.UserHistoryDao; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Transactional + public User findById(final long id) { + return userDao.findById(id); + } + + @Transactional + public void insert(final User user) { + userDao.insert(user); + } + + @Transactional + 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)); + } +} diff --git a/study/src/main/java/aop/service/TxUserService.java b/study/src/main/java/aop/service/TxUserService.java new file mode 100644 index 0000000000..f3c65ddf6e --- /dev/null +++ b/study/src/main/java/aop/service/TxUserService.java @@ -0,0 +1,48 @@ +package aop.service; + +import aop.DataAccessException; +import aop.domain.User; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +public class TxUserService implements UserService { + + private final PlatformTransactionManager transactionManager; + private final UserService userService; + + public TxUserService(final PlatformTransactionManager transactionManager, final UserService userService) { + this.transactionManager = transactionManager; + this.userService = userService; + } + + @Override + public User findById(final long id) { + return userService.findById(id); + } + + @Override + public void insert(final User user) { + userService.insert(user); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + /* ===== 트랜잭션 영역 ===== */ + final var transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); + + try { + /* ===== 트랜잭션 영역 ===== */ + + /* ===== 애플리케이션 영역 ===== */ + userService.changePassword(id, newPassword, createBy); + /* ===== 애플리케이션 영역 ===== */ + + /* ===== 트랜잭션 영역 ===== */ + } catch (RuntimeException e) { + transactionManager.rollback(transactionStatus); + throw new DataAccessException(e); + } + transactionManager.commit(transactionStatus); + /* ===== 트랜잭션 영역 ===== */ + } +} diff --git a/study/src/main/java/aop/service/UserService.java b/study/src/main/java/aop/service/UserService.java new file mode 100644 index 0000000000..8ce24e21b4 --- /dev/null +++ b/study/src/main/java/aop/service/UserService.java @@ -0,0 +1,12 @@ +package aop.service; + + +import aop.domain.User; + +public interface UserService { + + User findById(final long id); + void insert(final User user); + + void changePassword(final long id, final String newPassword, final String createBy); +} diff --git a/study/src/main/java/connectionpool/App.java b/study/src/main/java/connectionpool/App.java new file mode 100644 index 0000000000..3deb1fde16 --- /dev/null +++ b/study/src/main/java/connectionpool/App.java @@ -0,0 +1,12 @@ +package connectionpool; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/study/src/main/java/connectionpool/DataSourceConfig.java b/study/src/main/java/connectionpool/DataSourceConfig.java new file mode 100644 index 0000000000..1d2c3e6b7d --- /dev/null +++ b/study/src/main/java/connectionpool/DataSourceConfig.java @@ -0,0 +1,34 @@ +package connectionpool; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +@Configuration +public class DataSourceConfig { + + public static final int MAXIMUM_POOL_SIZE = 5; + private static final String H2_URL = "jdbc:h2:./test;DB_CLOSE_DELAY=-1"; + private static final String USER = "sa"; + private static final String PASSWORD = ""; + + + @Bean + public DataSource hikariDataSource() { + final var hikariConfig = new HikariConfig(); + hikariConfig.setPoolName("gugu"); + hikariConfig.setJdbcUrl(H2_URL); + hikariConfig.setUsername(USER); + hikariConfig.setPassword(PASSWORD); + hikariConfig.setMaximumPoolSize(MAXIMUM_POOL_SIZE); + hikariConfig.setConnectionTestQuery("VALUES 1"); + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + return new HikariDataSource(hikariConfig); + } +} diff --git a/study/src/main/java/transaction/App.java b/study/src/main/java/transaction/App.java new file mode 100644 index 0000000000..49b07ffd2e --- /dev/null +++ b/study/src/main/java/transaction/App.java @@ -0,0 +1,12 @@ +package transaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/study/src/main/java/transaction/ConsumerWrapper.java b/study/src/main/java/transaction/ConsumerWrapper.java new file mode 100644 index 0000000000..be9d1b9678 --- /dev/null +++ b/study/src/main/java/transaction/ConsumerWrapper.java @@ -0,0 +1,24 @@ +package transaction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Consumer; + +public final class ConsumerWrapper { + + private static final Logger log = LoggerFactory.getLogger(ConsumerWrapper.class); + + public static Consumer accept(ThrowingConsumer consumer) { + return i -> { + try { + consumer.accept(i); + } catch (Exception e) { + log.error(e.getMessage(), e.getCause()); + throw new RuntimeException(e); + } + }; + } + + private ConsumerWrapper() {} +} diff --git a/study/src/main/java/transaction/DatabasePopulatorUtils.java b/study/src/main/java/transaction/DatabasePopulatorUtils.java new file mode 100644 index 0000000000..84195470d9 --- /dev/null +++ b/study/src/main/java/transaction/DatabasePopulatorUtils.java @@ -0,0 +1,46 @@ +package transaction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public class DatabasePopulatorUtils { + + private static final Logger log = LoggerFactory.getLogger(DatabasePopulatorUtils.class); + + public static void execute(final DataSource dataSource) { + Connection connection = null; + Statement statement = null; + try { + final var url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql"); + final var file = new File(url.getFile()); + final var sql = Files.readString(file.toPath()); + connection = dataSource.getConnection(); + statement = connection.createStatement(); + statement.execute(sql); + } catch (NullPointerException | IOException | SQLException e) { + log.error(e.getMessage(), e.getCause()); + } finally { + try { + if (statement != null) { + statement.close(); + } + } catch (SQLException ignored) {} + + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException ignored) {} + } + } + + private DatabasePopulatorUtils() {} +} diff --git a/study/src/main/java/transaction/FunctionWrapper.java b/study/src/main/java/transaction/FunctionWrapper.java new file mode 100644 index 0000000000..7e1e35fff7 --- /dev/null +++ b/study/src/main/java/transaction/FunctionWrapper.java @@ -0,0 +1,24 @@ +package transaction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Function; + +public class FunctionWrapper { + + private static final Logger log = LoggerFactory.getLogger(FunctionWrapper.class); + + public static Function apply(ThrowingFunction function) { + return i -> { + try { + return function.apply(i); + } catch (Exception e) { + log.error(e.getMessage(), e.getCause()); + throw new RuntimeException(e); + } + }; + } + + private FunctionWrapper() {} +} diff --git a/study/src/main/java/transaction/RunnableWrapper.java b/study/src/main/java/transaction/RunnableWrapper.java new file mode 100644 index 0000000000..67515998d2 --- /dev/null +++ b/study/src/main/java/transaction/RunnableWrapper.java @@ -0,0 +1,22 @@ +package transaction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RunnableWrapper { + + private static final Logger log = LoggerFactory.getLogger(RunnableWrapper.class); + + public static Runnable accept(ThrowingRunnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Exception e) { + log.error(e.getMessage(), e.getCause()); + throw new RuntimeException(e); + } + }; + } + + private RunnableWrapper() {} +} diff --git a/study/src/main/java/transaction/ThrowingConsumer.java b/study/src/main/java/transaction/ThrowingConsumer.java new file mode 100644 index 0000000000..655f65e01d --- /dev/null +++ b/study/src/main/java/transaction/ThrowingConsumer.java @@ -0,0 +1,6 @@ +package transaction; + +@FunctionalInterface +public interface ThrowingConsumer { + void accept(T t) throws E; +} diff --git a/study/src/main/java/transaction/ThrowingFunction.java b/study/src/main/java/transaction/ThrowingFunction.java new file mode 100644 index 0000000000..466bed0279 --- /dev/null +++ b/study/src/main/java/transaction/ThrowingFunction.java @@ -0,0 +1,6 @@ +package transaction; + +@FunctionalInterface +public interface ThrowingFunction { + R apply(T t) throws E; +} diff --git a/study/src/main/java/transaction/ThrowingRunnable.java b/study/src/main/java/transaction/ThrowingRunnable.java new file mode 100644 index 0000000000..b121bac040 --- /dev/null +++ b/study/src/main/java/transaction/ThrowingRunnable.java @@ -0,0 +1,6 @@ +package transaction; + +@FunctionalInterface +public interface ThrowingRunnable { + void run() throws E; +} diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml new file mode 100644 index 0000000000..84c31c77b5 --- /dev/null +++ b/study/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + jpa: + show-sql: true + generate-ddl: true + hibernate: + ddl-auto: create-drop # 주의! 로컬 테스트에서만 사용한다. + datasource: + hikari: + jdbc-url: jdbc:h2:./test;DB_CLOSE_DELAY=-1;MODE=MYSQL; # TRACE_LEVEL_SYSTEM_OUT=3; # h2에서 출력하는 트랜잭션 로그 확인할 때 사용 + username: sa + password: + +# 스프링에서 출력하는 트랜잭션 로그를 직접 보고 싶으면 아래 주석 해제 +#logging: +# level: +# org.springframework.transaction.interceptor: TRACE +# org.springframework.transaction.support: DEBUG diff --git a/study/src/main/resources/schema.sql b/study/src/main/resources/schema.sql new file mode 100644 index 0000000000..bf9c44ac95 --- /dev/null +++ b/study/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + account VARCHAR(100) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL +) ENGINE=INNODB; + +CREATE TABLE IF NOT EXISTS user_history ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + account VARCHAR(100) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + created_at DATETIME NOT NULL, + created_by VARCHAR(100) NOT NULL +) ENGINE=INNODB; diff --git a/study/src/test/java/aop/StubUserHistoryDao.java b/study/src/test/java/aop/StubUserHistoryDao.java new file mode 100644 index 0000000000..c74f14a29c --- /dev/null +++ b/study/src/test/java/aop/StubUserHistoryDao.java @@ -0,0 +1,19 @@ +package aop; + +import aop.domain.UserHistory; +import aop.repository.UserHistoryDao; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class StubUserHistoryDao extends UserHistoryDao { + + public StubUserHistoryDao(final JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + public void log(final UserHistory userHistory) { + throw new DataAccessException(); + } +} diff --git a/study/src/test/java/aop/stage0/Stage0Test.java b/study/src/test/java/aop/stage0/Stage0Test.java new file mode 100644 index 0000000000..079cc6b5a0 --- /dev/null +++ b/study/src/test/java/aop/stage0/Stage0Test.java @@ -0,0 +1,73 @@ +package aop.stage0; + +import aop.DataAccessException; +import aop.StubUserHistoryDao; +import aop.domain.User; +import aop.repository.UserDao; +import aop.repository.UserHistoryDao; +import aop.service.AppUserService; +import aop.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class Stage0Test { + + private static final Logger log = LoggerFactory.getLogger(Stage0Test.class); + + @Autowired + private UserDao userDao; + + @Autowired + private UserHistoryDao userHistoryDao; + + @Autowired + private StubUserHistoryDao stubUserHistoryDao; + + @Autowired + private PlatformTransactionManager platformTransactionManager; + + @BeforeEach + void setUp() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + } + + @Test + void testChangePassword() { + final var appUserService = new AppUserService(userDao, userHistoryDao); + final UserService userService = null; + + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isEqualTo(newPassword); + } + + @Test + void testTransactionRollback() { + final var appUserService = new AppUserService(userDao, stubUserHistoryDao); + final UserService userService = null; + + final var newPassword = "newPassword"; + final var createBy = "gugu"; + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isNotEqualTo(newPassword); + } +} diff --git a/study/src/test/java/aop/stage0/TransactionHandler.java b/study/src/test/java/aop/stage0/TransactionHandler.java new file mode 100644 index 0000000000..2aa8445aba --- /dev/null +++ b/study/src/test/java/aop/stage0/TransactionHandler.java @@ -0,0 +1,15 @@ +package aop.stage0; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +public class TransactionHandler implements InvocationHandler { + + /** + * @Transactional 어노테이션이 존재하는 메서드만 트랜잭션 기능을 적용하도록 만들어보자. + */ + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + return null; + } +} diff --git a/study/src/test/java/aop/stage1/Stage1Test.java b/study/src/test/java/aop/stage1/Stage1Test.java new file mode 100644 index 0000000000..113b2e7d03 --- /dev/null +++ b/study/src/test/java/aop/stage1/Stage1Test.java @@ -0,0 +1,68 @@ +package aop.stage1; + +import aop.DataAccessException; +import aop.StubUserHistoryDao; +import aop.domain.User; +import aop.repository.UserDao; +import aop.repository.UserHistoryDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class Stage1Test { + + private static final Logger log = LoggerFactory.getLogger(Stage1Test.class); + + @Autowired + private UserDao userDao; + + @Autowired + private UserHistoryDao userHistoryDao; + + @Autowired + private StubUserHistoryDao stubUserHistoryDao; + + @Autowired + private PlatformTransactionManager platformTransactionManager; + + @BeforeEach + void setUp() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userDao.insert(user); + } + + @Test + void testChangePassword() { + final UserService userService = null; + + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isEqualTo(newPassword); + } + + @Test + void testTransactionRollback() { + final UserService userService = null; + + final var newPassword = "newPassword"; + final var createBy = "gugu"; + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isNotEqualTo(newPassword); + } +} diff --git a/study/src/test/java/aop/stage1/TransactionAdvice.java b/study/src/test/java/aop/stage1/TransactionAdvice.java new file mode 100644 index 0000000000..03a03a84e5 --- /dev/null +++ b/study/src/test/java/aop/stage1/TransactionAdvice.java @@ -0,0 +1,15 @@ +package aop.stage1; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +/** + * 어드바이스(advice). 부가기능을 담고 있는 클래스 + */ +public class TransactionAdvice implements MethodInterceptor { + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + return null; + } +} diff --git a/study/src/test/java/aop/stage1/TransactionAdvisor.java b/study/src/test/java/aop/stage1/TransactionAdvisor.java new file mode 100644 index 0000000000..7abc27516c --- /dev/null +++ b/study/src/test/java/aop/stage1/TransactionAdvisor.java @@ -0,0 +1,27 @@ +package aop.stage1; + +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; + +/** + * 어드바이저(advisor). 포인트컷과 어드바이스를 하나씩 갖고 있는 객체. + * AOP의 애스팩트(aspect)에 해당되는 클래스다. + */ +public class TransactionAdvisor implements PointcutAdvisor { + + @Override + public Pointcut getPointcut() { + return null; + } + + @Override + public Advice getAdvice() { + return null; + } + + @Override + public boolean isPerInstance() { + return false; + } +} diff --git a/study/src/test/java/aop/stage1/TransactionPointcut.java b/study/src/test/java/aop/stage1/TransactionPointcut.java new file mode 100644 index 0000000000..29ff854890 --- /dev/null +++ b/study/src/test/java/aop/stage1/TransactionPointcut.java @@ -0,0 +1,19 @@ +package aop.stage1; + +import org.springframework.aop.support.StaticMethodMatcherPointcut; + +import java.lang.reflect.Method; + +/** + * 포인트컷(pointcut). 어드바이스를 적용할 조인 포인트를 선별하는 클래스. + * TransactionPointcut 클래스는 메서드를 대상으로 조인 포인트를 찾는다. + * + * 조인 포인트(join point). 어드바이스가 적용될 위치 + */ +public class TransactionPointcut extends StaticMethodMatcherPointcut { + + @Override + public boolean matches(final Method method, final Class targetClass) { + return false; + } +} diff --git a/study/src/test/java/aop/stage1/UserService.java b/study/src/test/java/aop/stage1/UserService.java new file mode 100644 index 0000000000..5a12b12f63 --- /dev/null +++ b/study/src/test/java/aop/stage1/UserService.java @@ -0,0 +1,36 @@ +package aop.stage1; + +import aop.Transactional; +import aop.domain.User; +import aop.domain.UserHistory; +import aop.repository.UserDao; +import aop.repository.UserHistoryDao; + +public class UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Transactional + public User findById(final long id) { + return userDao.findById(id); + } + + @Transactional + public void insert(final User user) { + userDao.insert(user); + } + + @Transactional + 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)); + } +} diff --git a/study/src/test/java/aop/stage2/AopConfig.java b/study/src/test/java/aop/stage2/AopConfig.java new file mode 100644 index 0000000000..0a6e7f124e --- /dev/null +++ b/study/src/test/java/aop/stage2/AopConfig.java @@ -0,0 +1,8 @@ +package aop.stage2; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AopConfig { + +} diff --git a/study/src/test/java/aop/stage2/Stage2Test.java b/study/src/test/java/aop/stage2/Stage2Test.java new file mode 100644 index 0000000000..ae46641880 --- /dev/null +++ b/study/src/test/java/aop/stage2/Stage2Test.java @@ -0,0 +1,57 @@ +package aop.stage2; + +import aop.DataAccessException; +import aop.StubUserHistoryDao; +import aop.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class Stage2Test { + + private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); + + @Autowired + private UserService userService; + + @Autowired + private StubUserHistoryDao stubUserHistoryDao; + + @BeforeEach + void setUp() { + final var user = new User("gugu", "password", "hkkang@woowahan.com"); + userService.insert(user); + } + + @Test + void testChangePassword() { + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isEqualTo(newPassword); + } + + @Test + void testTransactionRollback() { + userService.setUserHistoryDao(stubUserHistoryDao); + + final var newPassword = "newPassword"; + final var createBy = "gugu"; + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); + + final var actual = userService.findById(1L); + + assertThat(actual.getPassword()).isNotEqualTo(newPassword); + } +} diff --git a/study/src/test/java/aop/stage2/UserService.java b/study/src/test/java/aop/stage2/UserService.java new file mode 100644 index 0000000000..d76731659d --- /dev/null +++ b/study/src/test/java/aop/stage2/UserService.java @@ -0,0 +1,42 @@ +package aop.stage2; + +import aop.Transactional; +import aop.domain.User; +import aop.domain.UserHistory; +import aop.repository.UserDao; +import aop.repository.UserHistoryDao; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + private final UserDao userDao; + private UserHistoryDao userHistoryDao; + + public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Transactional + public User findById(final long id) { + return userDao.findById(id); + } + + @Transactional + public void insert(final User user) { + userDao.insert(user); + } + + @Transactional + 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 setUserHistoryDao(final UserHistoryDao userHistoryDao) { + this.userHistoryDao = userHistoryDao; + } +} diff --git a/study/src/test/java/connectionpool/PoolingVsNoPoolingTest.java b/study/src/test/java/connectionpool/PoolingVsNoPoolingTest.java new file mode 100644 index 0000000000..20942cca11 --- /dev/null +++ b/study/src/test/java/connectionpool/PoolingVsNoPoolingTest.java @@ -0,0 +1,118 @@ +package connectionpool; + +import com.mysql.cj.jdbc.MysqlDataSource; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.ClockSource; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.utility.DockerImageName; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * pooling을 사용한 경우와 사용하지 않은 경우 트래픽이 얼마나 차이나는지 확인해보자. + * + * network bandwidth capture + * 터미널에 iftop를 설치하고 아래 명령어를 실행한 상태에서 테스트를 실행하자. + * $ sudo iftop -i lo0 -nf "host localhost" + * windows 사용자라면 wsl2를 사용하거나 다른 모니터링 툴을 찾아보자. + */ +class PoolingVsNoPoolingTest { + + private final Logger log = LoggerFactory.getLogger(PoolingVsNoPoolingTest.class); + + private static final int COUNT = 1000; + + private static MySQLContainer container; + + @BeforeAll + static void beforeAll() throws SQLException { + // TestContainer로 임시 MySQL을 실행한다. + container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) + .withDatabaseName("test"); + container.start(); + + final var dataSource = createMysqlDataSource(); + + // 테스트에 사용할 users 테이블을 생성하고 데이터를 추가한다. + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(true); + try (Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS users;"); + stmt.execute("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL) ENGINE=INNODB;"); + stmt.executeUpdate("INSERT INTO users (email) VALUES ('hkkang@woowahan.com')"); + conn.setAutoCommit(false); + } + } + } + + @AfterAll + static void afterAll() { + container.stop(); + } + + @Test + void noPoling() throws SQLException { + final var dataSource = createMysqlDataSource(); + + long start = ClockSource.currentTime(); + connect(dataSource); + long end = ClockSource.currentTime(); + + // 테스트 결과를 확인한다. + log.info("Elapsed runtime: {}", ClockSource.elapsedDisplayString(start, end)); + } + + @Test + void pooling() throws SQLException { + final var config = new HikariConfig(); + config.setJdbcUrl(container.getJdbcUrl()); + config.setUsername(container.getUsername()); + config.setPassword(container.getPassword()); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(1000); + config.setAutoCommit(false); + config.setReadOnly(false); + final var hikariDataSource = new HikariDataSource(config); + + long start = ClockSource.currentTime(); + connect(hikariDataSource); + long end = ClockSource.currentTime(); + + // 테스트 결과를 확인한다. + log.info("Elapsed runtime: {}", ClockSource.elapsedDisplayString(start, end)); + } + + private static void connect(DataSource dataSource) throws SQLException { + // COUNT만큼 DB 연결을 수행한다. + for (int i = 0; i < COUNT; i++) { + try (Connection connection = dataSource.getConnection()) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { + if (rs.next()) { + rs.getString(1).hashCode(); + } + } + } + } + } + + private static MysqlDataSource createMysqlDataSource() throws SQLException { + final var dataSource = new MysqlDataSource(); + dataSource.setUrl(container.getJdbcUrl()); + dataSource.setUser(container.getUsername()); + dataSource.setPassword(container.getPassword()); + dataSource.setConnectTimeout(1000); + return dataSource; + } +} diff --git a/study/src/test/java/connectionpool/stage0/Stage0Test.java b/study/src/test/java/connectionpool/stage0/Stage0Test.java new file mode 100644 index 0000000000..7eb3d7d87d --- /dev/null +++ b/study/src/test/java/connectionpool/stage0/Stage0Test.java @@ -0,0 +1,62 @@ +package connectionpool.stage0; + +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +class Stage0Test { + + private static final String H2_URL = "jdbc:h2:./test"; + private static final String USER = "sa"; + private static final String PASSWORD = ""; + + /** + * DriverManager + * JDBC 드라이버를 관리하는 가장 기본적인 방법. + * 커넥션 풀, 분산 트랜잭션을 지원하지 않아서 잘 사용하지 않는다. + * + * JDBC 4.0 이전에는 Class.forName 메서드를 사용하여 JDBC 드라이버를 직접 등록해야 했다. + * JDBC 4.0 부터 DriverManager가 적절한 JDBC 드라이버를 찾는다. + * + * Autoloading of JDBC drivers + * https://docs.oracle.com/javadb/10.8.3.0/ref/rrefjdbc4_0summary.html + */ + @Test + void driverManager() throws SQLException { + // Class.forName("org.h2.Driver"); // JDBC 4.0 부터 생략 가능 + // DriverManager 클래스를 활용하여 static 변수의 정보를 활용하여 h2 db에 연결한다. + try (final Connection connection = DriverManager.getConnection(H2_URL, USER, PASSWORD)) { + assertThat(connection.isValid(1)).isTrue(); + } + } + + /** + * DataSource + * 데이터베이스, 파일 같은 물리적 데이터 소스에 연결할 때 사용하는 인터페이스. + * 구현체는 각 vendor에서 제공한다. + * 테스트 코드의 JdbcDataSource 클래스는 h2에서 제공하는 클래스다. + * + * DirverManager가 아닌 DataSource를 사용하는 이유 + * - 애플리케이션 코드를 직접 수정하지 않고 properties로 디비 연결을 변경할 수 있다. + * - 커넥션 풀링(Connection pooling) 또는 분산 트랜잭션은 DataSource를 통해서 사용 가능하다. + * + * Using a DataSource Object to Make a Connection + * https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/javax/sql/package-summary.html + */ + @Test + void dataSource() throws SQLException { + final JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL(H2_URL); + dataSource.setUser(USER); + dataSource.setPassword(PASSWORD); + + try (final var connection = dataSource.getConnection()) { + assertThat(connection.isValid(1)).isTrue(); + } + } +} diff --git a/study/src/test/java/connectionpool/stage1/Stage1Test.java b/study/src/test/java/connectionpool/stage1/Stage1Test.java new file mode 100644 index 0000000000..087485ff58 --- /dev/null +++ b/study/src/test/java/connectionpool/stage1/Stage1Test.java @@ -0,0 +1,82 @@ +package connectionpool.stage1; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.h2.jdbcx.JdbcConnectionPool; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +class Stage1Test { + + private static final String H2_URL = "jdbc:h2:./test;DB_CLOSE_DELAY=-1"; + private static final String USER = "sa"; + private static final String PASSWORD = ""; + + /** + * 커넥션 풀링(Connection Pooling)이란? + * DataSource 객체를 통해 미리 커넥션(Connection)을 만들어 두는 것을 의미한다. + * 새로운 커넥션을 생성하는 것은 많은 비용이 들기에 미리 커넥션을 만들어두면 성능상 이점이 있다. + * 커넥션 풀링에 미리 만들어둔 커넥션은 재사용 가능하다. + * + * h2에서 제공하는 JdbcConnectionPool를 다뤄보며 커넥션 풀에 대한 감을 잡아보자. + * + * Connection Pooling and Statement Pooling + * https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/javax/sql/package-summary.html + */ + @Test + void testJdbcConnectionPool() throws SQLException { + final JdbcConnectionPool jdbcConnectionPool = JdbcConnectionPool.create(H2_URL, USER, PASSWORD); + + assertThat(jdbcConnectionPool.getActiveConnections()).isZero(); + try (final var connection = jdbcConnectionPool.getConnection()) { + assertThat(connection.isValid(1)).isTrue(); + assertThat(jdbcConnectionPool.getActiveConnections()).isEqualTo(1); + } + assertThat(jdbcConnectionPool.getActiveConnections()).isZero(); + + jdbcConnectionPool.dispose(); + } + + /** + * Spring Boot 2.0 부터 HikariCP를 기본 데이터 소스로 채택하고 있다. + * https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#data.sql.datasource.connection-pool + * Supported Connection Pools + * We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it. + * + * HikariCP 공식 문서를 참고하여 HikariCP를 설정해보자. + * https://github.com/brettwooldridge/HikariCP#rocket-initialization + * + * HikariCP 필수 설정 + * https://github.com/brettwooldridge/HikariCP#essentials + * + * HikariCP의 pool size는 몇으로 설정하는게 좋을까? + * https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + * + * HikariCP를 사용할 때 적용하면 좋은 MySQL 설정 + * https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration + */ + @Test + void testHikariCP() { + final var hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(H2_URL); + hikariConfig.setUsername(USER); + hikariConfig.setPassword(PASSWORD); + hikariConfig.setMaximumPoolSize(5); + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + final var dataSource = new HikariDataSource(hikariConfig); + final var properties = dataSource.getDataSourceProperties(); + + assertThat(dataSource.getMaximumPoolSize()).isEqualTo(5); + assertThat(properties.getProperty("cachePrepStmts")).isEqualTo("true"); + assertThat(properties.getProperty("prepStmtCacheSize")).isEqualTo("250"); + assertThat(properties.getProperty("prepStmtCacheSqlLimit")).isEqualTo("2048"); + + dataSource.close(); + } +} diff --git a/study/src/test/java/connectionpool/stage2/Stage2Test.java b/study/src/test/java/connectionpool/stage2/Stage2Test.java new file mode 100644 index 0000000000..2ed6b075b5 --- /dev/null +++ b/study/src/test/java/connectionpool/stage2/Stage2Test.java @@ -0,0 +1,84 @@ +package connectionpool.stage2; + +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.pool.HikariPool; +import connectionpool.DataSourceConfig; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.sql.DataSource; +import java.lang.reflect.Field; +import java.sql.Connection; + +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class Stage2Test { + + private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); + + /** + * spring boot에서 설정 파일인 application.yml를 사용하여 DataSource를 설정할 수 있다. + * 하지만 DataSource를 여러 개 사용하거나 세부 설정을 하려면 빈을 직접 생성하는 방법을 사용한다. + * DataSourceConfig 클래스를 찾아서 어떻게 빈으로 직접 생성하는지 확인해보자. + * 그리고 아래 DataSource가 직접 생성한 빈으로 주입 받았는지 getPoolName() 메서드로 확인해보자. + */ + @Autowired + private DataSource dataSource; + + @Test + void test() throws InterruptedException { + final var hikariDataSource = (HikariDataSource) dataSource; + final var hikariPool = getPool((HikariDataSource) dataSource); + + // 설정한 커넥션 풀 최대값보다 더 많은 스레드를 생성해서 동시에 디비에 접근을 시도하면 어떻게 될까? + final var threads = new Thread[20]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(getConnection()); + } + + for (final var thread : threads) { + thread.start(); + } + + for (final var thread : threads) { + thread.join(); + } + + // 동시에 많은 요청이 몰려도 최대 풀 사이즈를 유지한다. + assertThat(hikariPool.getTotalConnections()).isEqualTo(DataSourceConfig.MAXIMUM_POOL_SIZE); + + // DataSourceConfig 클래스에서 직접 생성한 커넥션 풀. + assertThat(hikariDataSource.getPoolName()).isEqualTo("gugu"); + } + + // 데이터베이스에 연결만 하는 메서드. 커넥션 풀에 몇 개의 연결이 생기는지 확인하는 용도. + private Runnable getConnection() { + return () -> { + try { + log.info("Before acquire "); + try (Connection ignored = dataSource.getConnection()) { + log.info("After acquire "); + quietlySleep(500); // Thread.sleep(500)과 동일한 기능 + } + } catch (Exception e) { + } + }; + } + + // 학습 테스트를 위해 HikariPool을 추출 + public static HikariPool getPool(final HikariDataSource hikariDataSource) + { + try { + Field field = hikariDataSource.getClass().getDeclaredField("pool"); + field.setAccessible(true); + return (HikariPool) field.get(hikariDataSource); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java new file mode 100644 index 0000000000..8c29944d4e --- /dev/null +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -0,0 +1,265 @@ +package transaction.stage1; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; +import transaction.DatabasePopulatorUtils; +import transaction.RunnableWrapper; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 격리 레벨(Isolation Level)에 따라 여러 사용자가 동시에 db에 접근했을 때 어떤 문제가 발생하는지 확인해보자. + * ❗phantom reads는 docker를 실행한 상태에서 테스트를 실행한다. + * ❗phantom reads는 MySQL로 확인한다. H2 데이터베이스에서는 발생하지 않는다. + * + * 참고 링크 + * https://en.wikipedia.org/wiki/Isolation_(database_systems) + * + * 각 테스트에서 어떤 현상이 발생하는지 직접 경험해보고 아래 표를 채워보자. + * + : 발생 + * - : 발생하지 않음 + * Read phenomena | Dirty reads | Non-repeatable reads | Phantom reads + * Isolation level | | | + * -----------------|-------------|----------------------|-------------- + * Read Uncommitted | | | + * Read Committed | | | + * Repeatable Read | | | + * Serializable | | | + */ +class Stage1Test { + + private static final Logger log = LoggerFactory.getLogger(Stage1Test.class); + private DataSource dataSource; + private UserDao userDao; + + private void setUp(final DataSource dataSource) { + this.dataSource = dataSource; + DatabasePopulatorUtils.execute(dataSource); + this.userDao = new UserDao(dataSource); + } + + /** + * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + * + : 발생 + * - : 발생하지 않음 + * Read phenomena | Dirty reads + * Isolation level | + * -----------------|------------- + * Read Uncommitted | + * Read Committed | + * Repeatable Read | + * Serializable | + */ + @Test + void dirtyReading() throws SQLException { + setUp(createH2DataSource()); + + // db에 새로운 연결(사용자A)을 받아와서 + final var connection = dataSource.getConnection(); + + // 트랜잭션을 시작한다. + connection.setAutoCommit(false); + + // db에 데이터를 추가하고 커밋하기 전에 + userDao.insert(connection, new User("gugu", "password", "hkkang@woowahan.com")); + + new Thread(RunnableWrapper.accept(() -> { + // db에 connection(사용자A)이 아닌 새로운 연결인 subConnection(사용자B)을 받아온다. + final var subConnection = dataSource.getConnection(); + + // 적절한 격리 레벨을 찾는다. + final int isolationLevel = Connection.TRANSACTION_NONE; + + // 트랜잭션 격리 레벨을 설정한다. + subConnection.setTransactionIsolation(isolationLevel); + + // ❗️gugu 객체는 connection에서 아직 커밋하지 않은 상태다. + // 격리 레벨에 따라 커밋하지 않은 gugu 객체를 조회할 수 있다. + // 사용자B가 사용자A가 커밋하지 않은 데이터를 조회하는게 적절할까? + final var actual = userDao.findByAccount(subConnection, "gugu"); + + // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. + // 어떤 격리 레벨일 때 다른 연결의 커밋 전 데이터를 조회할 수 있을지 찾아보자. + // 다른 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. + log.info("isolation level : {}, user : {}", isolationLevel, actual); + assertThat(actual).isNull(); + })).start(); + + sleep(0.5); + + // 롤백하면 사용자A의 user 데이터를 저장하지 않았는데 사용자B는 user 데이터가 있다고 인지한 상황이 된다. + connection.rollback(); + } + + /** + * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + * + : 발생 + * - : 발생하지 않음 + * Read phenomena | Non-repeatable reads + * Isolation level | + * -----------------|--------------------- + * Read Uncommitted | + * Read Committed | + * Repeatable Read | + * Serializable | + */ + @Test + void noneRepeatable() throws SQLException { + setUp(createH2DataSource()); + + // 테스트 전에 필요한 데이터를 추가한다. + userDao.insert(dataSource.getConnection(), new User("gugu", "password", "hkkang@woowahan.com")); + + // db에 새로운 연결(사용자A)을 받아와서 + final var connection = dataSource.getConnection(); + + // 트랜잭션을 시작한다. + connection.setAutoCommit(false); + + // 적절한 격리 레벨을 찾는다. + final int isolationLevel = Connection.TRANSACTION_NONE; + + // 트랜잭션 격리 레벨을 설정한다. + connection.setTransactionIsolation(isolationLevel); + + // 사용자A가 gugu 객체를 조회했다. + final var user = userDao.findByAccount(connection, "gugu"); + log.info("user : {}", user); + + new Thread(RunnableWrapper.accept(() -> { + // 사용자B가 새로 연결하여 + final var subConnection = dataSource.getConnection(); + + // 사용자A가 조회한 gugu 객체를 사용자B가 다시 조회했다. + final var anotherUser = userDao.findByAccount(subConnection, "gugu"); + + // ❗사용자B가 gugu 객체의 비밀번호를 변경했다.(subConnection은 auto commit 상태) + anotherUser.changePassword("qqqq"); + userDao.update(subConnection, anotherUser); + })).start(); + + sleep(0.5); + + // 사용자A가 다시 gugu 객체를 조회했다. + // 사용자B는 패스워드를 변경하고 아직 커밋하지 않았다. + final var actual = userDao.findByAccount(connection, "gugu"); + + // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. + // 각 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. + log.info("isolation level : {}, user : {}", isolationLevel, actual); + assertThat(actual.getPassword()).isEqualTo("password"); + + connection.rollback(); + } + + /** + * phantom read는 h2에서 발생하지 않는다. mysql로 확인해보자. + * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + * + : 발생 + * - : 발생하지 않음 + * Read phenomena | Phantom reads + * Isolation level | + * -----------------|-------------- + * Read Uncommitted | + * Read Committed | + * Repeatable Read | + * Serializable | + */ + @Test + void phantomReading() throws SQLException { + + // testcontainer로 docker를 실행해서 mysql에 연결한다. + final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) + .withLogConsumer(new Slf4jLogConsumer(log)); + mysql.start(); + setUp(createMySQLDataSource(mysql)); + + // 테스트 전에 필요한 데이터를 추가한다. + userDao.insert(dataSource.getConnection(), new User("gugu", "password", "hkkang@woowahan.com")); + + // db에 새로운 연결(사용자A)을 받아와서 + final var connection = dataSource.getConnection(); + + // 트랜잭션을 시작한다. + connection.setAutoCommit(false); + + // 적절한 격리 레벨을 찾는다. + final int isolationLevel = Connection.TRANSACTION_NONE; + + // 트랜잭션 격리 레벨을 설정한다. + connection.setTransactionIsolation(isolationLevel); + + // 사용자A가 id로 범위를 조회했다. + userDao.findGreaterThan(connection, 1); + + new Thread(RunnableWrapper.accept(() -> { + // 사용자B가 새로 연결하여 + final var subConnection = dataSource.getConnection(); + + // 트랜잭션 시작 + subConnection.setAutoCommit(false); + + // 새로운 user 객체를 저장했다. + // id는 2로 저장된다. + userDao.insert(subConnection, new User("bird", "password", "bird@woowahan.com")); + + subConnection.commit(); + })).start(); + + sleep(0.5); + + // MySQL에서 팬텀 읽기를 시연하려면 update를 실행해야 한다. + // http://stackoverflow.com/questions/42794425/unable-to-produce-a-phantom-read/42796969#42796969 + userDao.updatePasswordGreaterThan(connection, "qqqq", 1); + + // 사용자A가 다시 id로 범위를 조회했다. + final var actual = userDao.findGreaterThan(connection, 1); + + // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. + // 각 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. + log.info("isolation level : {}, user : {}", isolationLevel, actual); + assertThat(actual).hasSize(1); + + connection.rollback(); + mysql.close(); + } + + private static DataSource createMySQLDataSource(final JdbcDatabaseContainer container) { + final var config = new HikariConfig(); + config.setJdbcUrl(container.getJdbcUrl()); + config.setUsername(container.getUsername()); + config.setPassword(container.getPassword()); + config.setDriverClassName(container.getDriverClassName()); + return new HikariDataSource(config); + } + + private static DataSource createH2DataSource() { + final var jdbcDataSource = new JdbcDataSource(); + // h2 로그를 확인하고 싶을 때 사용 +// jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); + jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL;"); + jdbcDataSource.setUser("sa"); + jdbcDataSource.setPassword(""); + return jdbcDataSource; + } + + private void sleep(double seconds) { + try { + TimeUnit.MILLISECONDS.sleep((long) (seconds * 1000)); + } catch (InterruptedException ignored) { + } + } +} diff --git a/study/src/test/java/transaction/stage1/User.java b/study/src/test/java/transaction/stage1/User.java new file mode 100644 index 0000000000..c2036720db --- /dev/null +++ b/study/src/test/java/transaction/stage1/User.java @@ -0,0 +1,56 @@ +package transaction.stage1; + +public class User { + + private Long id; + private final String account; + private String password; + private final String email; + + public User(long id, String account, String password, String email) { + this.id = id; + this.account = account; + this.password = password; + this.email = email; + } + + public User(String account, String password, String email) { + this.account = account; + this.password = password; + this.email = email; + } + + public boolean checkPassword(String password) { + return this.password.equals(password); + } + + public void changePassword(String password) { + this.password = password; + } + + public String getAccount() { + return account; + } + + public long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/study/src/test/java/transaction/stage1/UserDao.java b/study/src/test/java/transaction/stage1/UserDao.java new file mode 100644 index 0000000000..dad7915cba --- /dev/null +++ b/study/src/test/java/transaction/stage1/UserDao.java @@ -0,0 +1,66 @@ +package transaction.stage1; + +import transaction.stage1.jdbc.JdbcTemplate; +import transaction.stage1.jdbc.RowMapper; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.List; + +public class UserDao { + + // spring jdbc가 아닌 직접 구현한 JdbcTemplate을 사용한다. + private final JdbcTemplate jdbcTemplate; + + public UserDao(final DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void insert(final Connection connection, final User user) { + final var sql = "insert into users (account, password, email) values (?, ?, ?)"; + jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); + } + + public void update(final Connection connection, final User user) { + final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; + jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); + } + + public void updatePasswordGreaterThan(final Connection connection, final String password, final long id) { + final var sql = "update users set password = ? where id >= ?"; + jdbcTemplate.update(connection, sql, password, id); + } + + public User findById(final Connection connection, final Long id) { + final var sql = "select id, account, password, email from users where id = ?"; + return jdbcTemplate.queryForObject(connection, sql, createRowMapper(), id); + } + + public User findByAccount(final Connection connection, final String account) { + final var sql = "select id, account, password, email from users where account = ?"; + return jdbcTemplate.queryForObject(connection, sql, createRowMapper(), account); + } + + public List findGreaterThan(final Connection connection, final long id) { + final var sql = "select id, account, password, email from users where id >= ?"; + return jdbcTemplate.query(connection, sql, createRowMapper(), id); + } + + public List findByAccountGreaterThan(final Connection connection, final String account) { + final var sql = "select id, account, password, email from users where account >= ?"; + return jdbcTemplate.query(connection, sql, createRowMapper(), account); + } + + public List findAll(final Connection connection) { + final var sql = "select id, account, password, email from users"; + return jdbcTemplate.query(connection, sql, createRowMapper()); + } + + private static RowMapper createRowMapper() { + return (final var rs) -> new User( + rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")); + } +} diff --git a/study/src/test/java/transaction/stage1/jdbc/DataAccessException.java b/study/src/test/java/transaction/stage1/jdbc/DataAccessException.java new file mode 100644 index 0000000000..aa427fd7fb --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/DataAccessException.java @@ -0,0 +1,25 @@ +package transaction.stage1.jdbc; + +public class DataAccessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DataAccessException() { + super(); + } + + public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public DataAccessException(String message, Throwable cause) { + super(message, cause); + } + + public DataAccessException(String message) { + super(message); + } + + public DataAccessException(Throwable cause) { + super(cause); + } +} diff --git a/study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java b/study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java new file mode 100644 index 0000000000..9d64be5a99 --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java @@ -0,0 +1,106 @@ +package transaction.stage1.jdbc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class JdbcTemplate { + private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); + private final DataSource dataSource; + + public JdbcTemplate(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public void update(final Connection connection, final String sql, final PreparedStatementSetter pss) throws DataAccessException { + try (final var pstmt = connection.prepareStatement(sql)) { + pss.setParameters(pstmt); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + public void update(final Connection connection, final String sql, final Object... parameters) { + update(connection, sql, createPreparedStatementSetter(parameters)); + } + + public void update(final String sql, final PreparedStatementSetter pss) throws DataAccessException { + try (final var conn = dataSource.getConnection(); final var pstmt = conn.prepareStatement(sql)) { + pss.setParameters(pstmt); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + public void update(final String sql, final Object... parameters) { + update(sql, createPreparedStatementSetter(parameters)); + } + + public void update(final PreparedStatementCreator psc, final KeyHolder holder) { + try (final var conn = dataSource.getConnection(); final var ps = psc.createPreparedStatement(conn)) { + ps.executeUpdate(); + final var rs = ps.getGeneratedKeys(); + if (rs.next()) { + long generatedKey = rs.getLong(1); + log.debug("Generated Key:{}", generatedKey); + holder.setId(generatedKey); + } + rs.close(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + public T queryForObject(final Connection connection, final String sql, final RowMapper rm, final PreparedStatementSetter pss) { + final var list = query(connection, sql, rm, pss); + if (list.isEmpty()) { + return null; + } + return list.get(0); + } + + public T queryForObject(final Connection connection, final String sql, final RowMapper rm, final Object... parameters) { + return queryForObject(connection, sql, rm, createPreparedStatementSetter(parameters)); + } + + public List query(final Connection connection, final String sql, final RowMapper rm, final PreparedStatementSetter pss) throws DataAccessException { + try (final var pstmt = connection.prepareStatement(sql)) { + pss.setParameters(pstmt); + return mapResultSetToObject(rm, pstmt); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private List mapResultSetToObject(final RowMapper rm, final PreparedStatement pstmt) { + try (final var rs = pstmt.executeQuery()) { + final var list = new ArrayList(); + while (rs.next()) { + list.add(rm.mapRow(rs)); + } + return list; + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + public List query(final Connection connection, final String sql, final RowMapper rm, final Object... parameters) { + return query(connection, sql, rm, createPreparedStatementSetter(parameters)); + } + + private PreparedStatementSetter createPreparedStatementSetter(final Object... parameters) { + return pstmt -> { + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + }; + } +} diff --git a/study/src/test/java/transaction/stage1/jdbc/KeyHolder.java b/study/src/test/java/transaction/stage1/jdbc/KeyHolder.java new file mode 100644 index 0000000000..04718bc3ef --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/KeyHolder.java @@ -0,0 +1,21 @@ +package transaction.stage1.jdbc; + +public class KeyHolder { + + private long id; + + public void setId(long id) { + this.id = id; + } + + public long getId() { + return id; + } + + @Override + public String toString() { + return "KeyHolder{" + + "id=" + id + + '}'; + } +} diff --git a/study/src/test/java/transaction/stage1/jdbc/PreparedStatementCreator.java b/study/src/test/java/transaction/stage1/jdbc/PreparedStatementCreator.java new file mode 100644 index 0000000000..6a7b625f1a --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/PreparedStatementCreator.java @@ -0,0 +1,9 @@ +package transaction.stage1.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface PreparedStatementCreator { + PreparedStatement createPreparedStatement(final Connection con) throws SQLException; +} diff --git a/study/src/test/java/transaction/stage1/jdbc/PreparedStatementSetter.java b/study/src/test/java/transaction/stage1/jdbc/PreparedStatementSetter.java new file mode 100644 index 0000000000..7b94832e83 --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/PreparedStatementSetter.java @@ -0,0 +1,8 @@ +package transaction.stage1.jdbc; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface PreparedStatementSetter { + void setParameters(final PreparedStatement pstmt) throws SQLException; +} diff --git a/study/src/test/java/transaction/stage1/jdbc/RowMapper.java b/study/src/test/java/transaction/stage1/jdbc/RowMapper.java new file mode 100644 index 0000000000..5fbcd1d441 --- /dev/null +++ b/study/src/test/java/transaction/stage1/jdbc/RowMapper.java @@ -0,0 +1,9 @@ +package transaction.stage1.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + T mapRow(final ResultSet rs) throws SQLException; +} diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java new file mode 100644 index 0000000000..9a1d415c18 --- /dev/null +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -0,0 +1,136 @@ +package transaction.stage2; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class FirstUserService { + + private static final Logger log = LoggerFactory.getLogger(FirstUserService.class); + + private final UserRepository userRepository; + private final SecondUserService secondUserService; + + @Autowired + public FirstUserService(final UserRepository userRepository, + final SecondUserService secondUserService) { + this.userRepository = userRepository; + this.secondUserService = secondUserService; + } + + @Transactional(readOnly = true, propagation = Propagation.REQUIRED) + public List findAll() { + return userRepository.findAll(); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithRequired() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithRequired(); + + return of(firstTransactionName, secondTransactionName); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithRequiredNew() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithRequiresNew(); + + return of(firstTransactionName, secondTransactionName); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveAndExceptionWithRequiredNew() { + secondUserService.saveSecondTransactionWithRequiresNew(); + + userRepository.save(User.createTest()); + logActualTransactionActive(); + + throw new RuntimeException(); + } + +// @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithSupports() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithSupports(); + + return of(firstTransactionName, secondTransactionName); + } + +// @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithMandatory() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithMandatory(); + + return of(firstTransactionName, secondTransactionName); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithNotSupported() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithNotSupported(); + + return of(firstTransactionName, secondTransactionName); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithNested() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithNested(); + + return of(firstTransactionName, secondTransactionName); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set saveFirstTransactionWithNever() { + final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + userRepository.save(User.createTest()); + logActualTransactionActive(); + + final var secondTransactionName = secondUserService.saveSecondTransactionWithNever(); + + return of(firstTransactionName, secondTransactionName); + } + + private Set of(final String firstTransactionName, final String secondTransactionName) { + return Stream.of(firstTransactionName, secondTransactionName) + .filter(transactionName -> !Objects.isNull(transactionName)) + .collect(Collectors.toSet()); + } + + private void logActualTransactionActive() { + final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); + final var emoji = actualTransactionActive ? "✅" : "❌"; + log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); + } +} diff --git a/study/src/test/java/transaction/stage2/SecondUserService.java b/study/src/test/java/transaction/stage2/SecondUserService.java new file mode 100644 index 0000000000..0d240fe854 --- /dev/null +++ b/study/src/test/java/transaction/stage2/SecondUserService.java @@ -0,0 +1,76 @@ +package transaction.stage2; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +@Service +public class SecondUserService { + + private static final Logger log = LoggerFactory.getLogger(SecondUserService.class); + + private final UserRepository userRepository; + + public SecondUserService(final UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Transactional(propagation = Propagation.REQUIRED) + public String saveSecondTransactionWithRequired() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public String saveSecondTransactionWithRequiresNew() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public String saveSecondTransactionWithSupports() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.MANDATORY) + public String saveSecondTransactionWithMandatory() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public String saveSecondTransactionWithNotSupported() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.NESTED) + public String saveSecondTransactionWithNested() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + @Transactional(propagation = Propagation.NEVER) + public String saveSecondTransactionWithNever() { + userRepository.save(User.createTest()); + logActualTransactionActive(); + return TransactionSynchronizationManager.getCurrentTransactionName(); + } + + private void logActualTransactionActive() { + final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); + final var emoji = actualTransactionActive ? "✅" : "❌"; + log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); + } +} diff --git a/study/src/test/java/transaction/stage2/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java new file mode 100644 index 0000000000..e522bf4365 --- /dev/null +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -0,0 +1,152 @@ +package transaction.stage2; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * 트랜잭션 전파(Transaction Propagation)란? + * 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. + * + * FirstUserService 클래스의 메서드를 실행할 때 첫 번째 트랜잭션이 생성된다. + * SecondUserService 클래스의 메서드를 실행할 때 두 번째 트랜잭션이 어떻게 되는지 관찰해보자. + * + * https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class Stage2Test { + + private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); + + @Autowired + private FirstUserService firstUserService; + + @Autowired + private UserRepository userRepository; + + @AfterEach + void tearDown() { + userRepository.deleteAll(); + } + + /** + * 생성된 트랜잭션이 몇 개인가? + * 왜 그런 결과가 나왔을까? + */ + @Test + void testRequired() { + final var actual = firstUserService.saveFirstTransactionWithRequired(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * 생성된 트랜잭션이 몇 개인가? + * 왜 그런 결과가 나왔을까? + */ + @Test + void testRequiredNew() { + final var actual = firstUserService.saveFirstTransactionWithRequiredNew(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. + * REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 지 확인해보자. + */ + @Test + void testRequiredNewWithRollback() { + assertThat(firstUserService.findAll()).hasSize(-1); + + assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) + .isInstanceOf(RuntimeException.class); + + assertThat(firstUserService.findAll()).hasSize(-1); + } + + /** + * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. + * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + */ + @Test + void testSupports() { + final var actual = firstUserService.saveFirstTransactionWithSupports(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. + * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + * SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. + */ + @Test + void testMandatory() { + final var actual = firstUserService.saveFirstTransactionWithMandatory(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * 아래 테스트는 몇 개의 물리적 트랜잭션이 동작할까? + * FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석 처리하자. + * 다시 테스트를 실행하면 몇 개의 물리적 트랜잭션이 동작할까? + * + * 스프링 공식 문서에서 물리적 트랜잭션과 논리적 트랜잭션의 차이점이 무엇인지 찾아보자. + */ + @Test + void testNotSupported() { + final var actual = firstUserService.saveFirstTransactionWithNotSupported(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * 아래 테스트는 왜 실패할까? + * FirstUserService.saveFirstTransactionWithNested() 메서드의 @Transactional을 주석 처리하면 어떻게 될까? + */ + @Test + void testNested() { + final var actual = firstUserService.saveFirstTransactionWithNested(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } + + /** + * 마찬가지로 @Transactional을 주석처리하면서 관찰해보자. + */ + @Test + void testNever() { + final var actual = firstUserService.saveFirstTransactionWithNever(); + + log.info("transactions : {}", actual); + assertThat(actual) + .hasSize(0) + .containsExactly(""); + } +} diff --git a/study/src/test/java/transaction/stage2/User.java b/study/src/test/java/transaction/stage2/User.java new file mode 100644 index 0000000000..60d113b723 --- /dev/null +++ b/study/src/test/java/transaction/stage2/User.java @@ -0,0 +1,63 @@ +package transaction.stage2; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String account; + private String password; + private String email; + + protected User() {} + + public User(String account, String password, String email) { + this.account = account; + this.password = password; + this.email = email; + } + + public boolean checkPassword(String password) { + return this.password.equals(password); + } + + public void changePassword(String password) { + this.password = password; + } + + public static User createTest() { + return new User("gugu", "password", "hkkang@woowahan.com"); + } + + public Long getId() { + return id; + } + + public String getAccount() { + return account; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/study/src/test/java/transaction/stage2/UserRepository.java b/study/src/test/java/transaction/stage2/UserRepository.java new file mode 100644 index 0000000000..61e329a3b0 --- /dev/null +++ b/study/src/test/java/transaction/stage2/UserRepository.java @@ -0,0 +1,6 @@ +package transaction.stage2; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} From 7182a499bf14dc4566488f68adc81748276f3ece Mon Sep 17 00:00:00 2001 From: kang-hyungu Date: Thu, 21 Sep 2023 14:11:58 +0900 Subject: [PATCH 02/52] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 672d1adecf..1bcba66f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,5 @@ Temporary Items tomcat.* tomcat.*/** + +**/WEB-INF/classes/** From 06379e5dfc39fca37fa096cc17bb04273303e594 Mon Sep 17 00:00:00 2001 From: kang-hyungu Date: Thu, 21 Sep 2023 14:11:58 +0900 Subject: [PATCH 03/52] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/main/java/aop/config/DataSourceConfig.java | 1 + study/src/main/resources/application.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/study/src/main/java/aop/config/DataSourceConfig.java b/study/src/main/java/aop/config/DataSourceConfig.java index b27245b932..d64fc2ca2b 100644 --- a/study/src/main/java/aop/config/DataSourceConfig.java +++ b/study/src/main/java/aop/config/DataSourceConfig.java @@ -14,6 +14,7 @@ public class DataSourceConfig { public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) + .setName("test;DB_CLOSE_DELAY=-1;MODE=MYSQL;") .addScript("classpath:schema.sql") .build(); } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 84c31c77b5..b9451e1013 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -1,5 +1,6 @@ spring: jpa: + open-in-view: false show-sql: true generate-ddl: true hibernate: From ae100c4210437534e8e3ede61419b5b945ef8fca Mon Sep 17 00:00:00 2001 From: swonny Date: Tue, 3 Oct 2023 19:02:40 +0900 Subject: [PATCH 04/52] =?UTF-8?q?feat:=20jdbc=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=AA=A8=EB=93=A0=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=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 | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index d14c545f34..5e5b2f17e2 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,15 +1,16 @@ package com.techcourse.dao; import com.techcourse.domain.User; -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.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; public class UserDao { @@ -49,18 +50,51 @@ public void insert(final User user) { if (pstmt != null) { pstmt.close(); } - } catch (SQLException ignored) {} + } catch (SQLException ignored) { + } try { if (conn != null) { conn.close(); } - } catch (SQLException ignored) {} + } catch (SQLException ignored) { + } } } public void update(final User user) { - // todo + final var sql = "update users set (account, password, email) = (?, ?, ?)"; + + Connection conn = null; + PreparedStatement pstmt = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + + log.debug("query : {}", sql); + + pstmt.setString(1, user.getAccount()); + pstmt.setString(2, user.getPassword()); + pstmt.setString(3, user.getEmail()); + 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) { + } + } } public List findAll() { @@ -98,19 +132,22 @@ public User findById(final Long id) { if (rs != null) { rs.close(); } - } catch (SQLException ignored) {} + } catch (SQLException ignored) { + } try { if (pstmt != null) { pstmt.close(); } - } catch (SQLException ignored) {} + } catch (SQLException ignored) { + } try { if (conn != null) { conn.close(); } - } catch (SQLException ignored) {} + } catch (SQLException ignored) { + } } } From 42708f0d7d885f08dbc30966a46fc6c3004ebaad Mon Sep 17 00:00:00 2001 From: swonny Date: Tue, 3 Oct 2023 19:03:40 +0900 Subject: [PATCH 05/52] =?UTF-8?q?feat:=20jdbc=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=AA=A8=EB=93=A0=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=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 | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 5e5b2f17e2..3aa9b0acb3 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -98,8 +98,55 @@ public void update(final User user) { } public List findAll() { - // todo - return null; + final var sql = "select id, account, password, email from users"; + + Connection conn = null; + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + final List users = new ArrayList<>(); + while (rs.next()) { + users.add( + new User( + rs.getLong(1), + rs.getString(2), + rs.getString(3), + rs.getString(4) + ) + ); + } + return users; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException ignored) { + } + + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException ignored) { + } + + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException ignored) { + } + } } public User findById(final Long id) { From 2463faf0802fca8c1d6f8543f823d609abf65152 Mon Sep 17 00:00:00 2001 From: swonny Date: Tue, 3 Oct 2023 19:04:01 +0900 Subject: [PATCH 06/52] =?UTF-8?q?feat:=20jdbc=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EC=9C=A0=EC=A0=80=20account=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=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 | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 3aa9b0acb3..eae5adb72b 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -199,7 +199,51 @@ public User findById(final Long id) { } public User findByAccount(final String account) { - // todo - return null; + final var sql = "select id, account, password, email from users where account = ?"; + + Connection conn = null; + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + pstmt.setString(1, account); + rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + if (rs.next()) { + return new User( + rs.getLong(1), + rs.getString(2), + rs.getString(3), + rs.getString(4)); + } + return null; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException ignored) { + } + + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException ignored) { + } + + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException ignored) { + } + } } } From e48fbcd7101c7e08afa4697d9d47e1c720290e5f Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 08:26:42 +0900 Subject: [PATCH 07/52] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B0=9D=EC=B2=B4=20RowMapper=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 10 ++++ .../jdbc/core/JdbcTemplate.java | 47 +++++++++++++++++++ .../springframework/jdbc/core/RowMapper.java | 10 ++++ 3 files changed, 67 insertions(+) create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index eae5adb72b..176f0b5513 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -17,14 +17,24 @@ public class UserDao { private static final Logger log = LoggerFactory.getLogger(UserDao.class); + private final RowMapper userRowMapper = resultSet -> new User( + resultSet.getLong("id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email") + ); + private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; public UserDao(final DataSource dataSource) { this.dataSource = dataSource; + jdbcTemplate = null; } public UserDao(final JdbcTemplate jdbcTemplate) { this.dataSource = null; + this.jdbcTemplate = jdbcTemplate; } public void insert(final User user) { 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 52a0d30a17..ac4d167af9 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -4,6 +4,10 @@ import org.slf4j.LoggerFactory; import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; public class JdbcTemplate { @@ -14,4 +18,47 @@ public class JdbcTemplate { public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } + + public T queryForObject(final String sql, final RowMapper rowMapper, final long id) { + Connection conn = null; + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + pstmt.setLong(1, id); + rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + if (rs.next()) { + return rowMapper.mapRow(rs); + } + return null; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException ignored) { + } + + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException ignored) { + } + + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException ignored) { + } + } + } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java new file mode 100644 index 0000000000..02165135cd --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,10 @@ +package org.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + + T mapRow(final ResultSet resultSet) throws SQLException; +} From aa39a33b61a4429ecce95b128cf92591b3985d51 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 08:37:01 +0900 Subject: [PATCH 08/52] =?UTF-8?q?refactor:=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 176f0b5513..5a0a857f05 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import javax.sql.DataSource; import java.sql.Connection; @@ -161,51 +162,8 @@ public List findAll() { public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - pstmt.setLong(1, id); - rs = pstmt.executeQuery(); - - log.debug("query : {}", sql); - - if (rs.next()) { - return new User( - rs.getLong(1), - rs.getString(2), - rs.getString(3), - rs.getString(4)); - } - return null; - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) { - } - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) { - } - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) { - } - } + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id); + return user; } public User findByAccount(final String account) { From 95aaf0c374cdb6265a68c13c6118aedbb96f9b42 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 08:47:19 +0900 Subject: [PATCH 09/52] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=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 --- .../main/java/com/techcourse/dao/UserDao.java | 49 ++----------------- .../jdbc/core/JdbcTemplate.java | 4 +- 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 5a0a857f05..46c39e30ae 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -30,7 +30,7 @@ public class UserDao { public UserDao(final DataSource dataSource) { this.dataSource = dataSource; - jdbcTemplate = null; + jdbcTemplate = new JdbcTemplate(dataSource); } public UserDao(final JdbcTemplate jdbcTemplate) { @@ -168,50 +168,7 @@ public User findById(final Long id) { public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - pstmt.setString(1, account); - rs = pstmt.executeQuery(); - - log.debug("query : {}", sql); - - if (rs.next()) { - return new User( - rs.getLong(1), - rs.getString(2), - rs.getString(3), - rs.getString(4)); - } - return null; - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) { - } - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) { - } - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) { - } - } + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account); + return user; } } 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 ac4d167af9..c6c7fbe733 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -19,14 +19,14 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public T queryForObject(final String sql, final RowMapper rowMapper, final long id) { + public T queryForObject(final String sql, final RowMapper rowMapper, final P parameter) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = dataSource.getConnection(); pstmt = conn.prepareStatement(sql); - pstmt.setLong(1, id); + pstmt.setObject(1, parameter); rs = pstmt.executeQuery(); log.debug("query : {}", sql); From 8ff2b3a5282b59f646829adc7924160e9043a4dc Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 08:55:08 +0900 Subject: [PATCH 10/52] =?UTF-8?q?feat:=20=EB=B3=B5=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20findAll=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 52 +------------------ .../jdbc/core/JdbcTemplate.java | 46 ++++++++++++++++ 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 46c39e30ae..cc2bab799a 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -9,9 +9,7 @@ import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; public class UserDao { @@ -110,54 +108,8 @@ public void update(final User user) { public List findAll() { final var sql = "select id, account, password, email from users"; - - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - rs = pstmt.executeQuery(); - - log.debug("query : {}", sql); - - final List users = new ArrayList<>(); - while (rs.next()) { - users.add( - new User( - rs.getLong(1), - rs.getString(2), - rs.getString(3), - rs.getString(4) - ) - ); - } - return users; - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) { - } - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) { - } - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) { - } - } + final List users = jdbcTemplate.query(sql, userRowMapper); + return users; } public User findById(final Long id) { 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 c6c7fbe733..52276b6c14 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -8,6 +8,8 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; public class JdbcTemplate { @@ -61,4 +63,48 @@ public T queryForObject(final String sql, final RowMapper rowMapper, f } } } + + public List query(final String sql, final RowMapper rowMapper) { + Connection conn = null; + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + final List objects = new ArrayList<>(); + while (rs.next()) { + final T object = rowMapper.mapRow(rs); + objects.add(object); + } + return objects; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException ignored) { + } + + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException ignored) { + } + + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException ignored) { + } + } + } } From 4f04737237f404f709ea6d8bc2b2f03f684ec80c Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:12:44 +0900 Subject: [PATCH 11/52] =?UTF-8?q?feat:=20update=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 33 ++---------------- .../jdbc/core/JdbcTemplate.java | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index cc2bab799a..9c58ea7601 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -73,36 +73,9 @@ public void insert(final User user) { public void update(final User user) { final var sql = "update users set (account, password, email) = (?, ?, ?)"; - - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setString(1, user.getAccount()); - pstmt.setString(2, user.getPassword()); - pstmt.setString(3, user.getEmail()); - 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) { - } + final int updatedRows = jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + if (updatedRows < 1) { + throw new RuntimeException("수정된 데이터가 없습니다."); } } 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 52276b6c14..e8d395d8de 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -107,4 +107,38 @@ public List query(final String sql, final RowMapper rowMapper) { } } } + + public int update(final String sql, final Object... parameters) { + Connection conn = null; + PreparedStatement pstmt = null; + try { + conn = dataSource.getConnection(); + pstmt = conn.prepareStatement(sql); + + log.debug("query : {}", sql); + + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + pstmt.setObject(parameterIndex + 1, parameters[parameterIndex]); + } + + return 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) { + } + } + } } From f2e785ee7019341882dd5be905e46e2ae1ccb90c Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:15:45 +0900 Subject: [PATCH 12/52] =?UTF-8?q?feat:=20insert=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 9c58ea7601..4612fb8073 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -7,9 +7,6 @@ import org.springframework.jdbc.core.RowMapper; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; import java.util.List; public class UserDao { @@ -38,36 +35,9 @@ public UserDao(final JdbcTemplate jdbcTemplate) { public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setString(1, user.getAccount()); - pstmt.setString(2, user.getPassword()); - pstmt.setString(3, user.getEmail()); - 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) { - } + final int updatedRows = jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + if (updatedRows < 1) { + throw new RuntimeException("저장된 데이터가 없습니다."); } } From edf550d8940edb85609a4dcfb47f1bdc50165dd9 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:23:55 +0900 Subject: [PATCH 13/52] =?UTF-8?q?refactor:=20=EB=8B=A8=EC=9D=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20try-with-resources=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 39 ++++--------------- 1 file changed, 7 insertions(+), 32 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 e8d395d8de..dbc73a2765 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -22,45 +22,20 @@ public JdbcTemplate(final DataSource dataSource) { } public T queryForObject(final String sql, final RowMapper rowMapper, final P parameter) { - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - pstmt.setObject(1, parameter); - rs = pstmt.executeQuery(); - + try (final Connection connection = dataSource.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + ) { log.debug("query : {}", sql); - if (rs.next()) { - return rowMapper.mapRow(rs); + final ResultSet resultSet = preparedStatement.executeQuery(); + if (resultSet.next()) { + return rowMapper.mapRow(resultSet); } return null; } catch (SQLException e) { log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) { - } - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) { - } - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) { - } + throw new RuntimeException(e); } } From 6d9a536db4af4b5321cb92123942911a57279eda Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:26:25 +0900 Subject: [PATCH 14/52] =?UTF-8?q?refactor:=20=EB=8B=A8=EC=9D=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=95=84=EB=8B=8C=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= 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, 4 insertions(+), 1 deletion(-) 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 dbc73a2765..f67ea766e6 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -31,7 +31,10 @@ public T queryForObject(final String sql, final RowMapper rowMapper, f if (resultSet.next()) { return rowMapper.mapRow(resultSet); } - return null; + if (!resultSet.isLast()) { + throw new RuntimeException("단일 데이터가 아닙니다."); + } + throw new RuntimeException("찾는 데이터가 존재하지 않습니다."); } catch (SQLException e) { log.error(e.getMessage(), e); From 1a99a87df96dc3a80edc330c4370ace75c0ccbfd Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:27:41 +0900 Subject: [PATCH 15/52] =?UTF-8?q?fix:=20=EB=8B=A8=EC=9D=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=88=84=EB=9D=BD=EB=90=9C=20=EB=A1=9C=EC=A7=81=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/org/springframework/jdbc/core/JdbcTemplate.java | 3 +++ 1 file changed, 3 insertions(+) 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 f67ea766e6..de8113e01f 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -14,6 +14,7 @@ public class JdbcTemplate { private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); + private static final int FIRST_PARAMETER_INDEX = 1; private final DataSource dataSource; @@ -27,6 +28,8 @@ public T queryForObject(final String sql, final RowMapper rowMapper, f ) { log.debug("query : {}", sql); + preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameter); + final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return rowMapper.mapRow(resultSet); From 8f7b50b87ac8afb9a50ef3846e04154d2bb3cf74 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:31:42 +0900 Subject: [PATCH 16/52] =?UTF-8?q?refactor:=20=EB=B3=B5=EC=88=98=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20try-with-res?= =?UTF-8?q?ources=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 37 +++---------------- 1 file changed, 6 insertions(+), 31 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 de8113e01f..e8fbd22672 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -46,46 +46,21 @@ public T queryForObject(final String sql, final RowMapper rowMapper, f } public List query(final String sql, final RowMapper rowMapper) { - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - rs = pstmt.executeQuery(); + try (final Connection connection = dataSource.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + ) { + final ResultSet resultSet = preparedStatement.executeQuery(); log.debug("query : {}", sql); final List objects = new ArrayList<>(); - while (rs.next()) { - final T object = rowMapper.mapRow(rs); + while (resultSet.next()) { + final T object = rowMapper.mapRow(resultSet); objects.add(object); } return objects; } catch (SQLException e) { - log.error(e.getMessage(), e); throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) { - } - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) { - } - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) { - } } } From efdff4e8fd6b09aac20762ea47f022b3c82e4f17 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:32:57 +0900 Subject: [PATCH 17/52] =?UTF-8?q?refactor:=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20try-with-resources=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=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 --- .../jdbc/core/JdbcTemplate.java | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 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 e8fbd22672..2ee59aae8f 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -65,36 +65,18 @@ public List query(final String sql, final RowMapper rowMapper) { } public int update(final String sql, final Object... parameters) { - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - + try (final Connection connection = dataSource.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + ) { log.debug("query : {}", sql); for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { - pstmt.setObject(parameterIndex + 1, parameters[parameterIndex]); + preparedStatement.setObject(parameterIndex + 1, parameters[parameterIndex]); } - return pstmt.executeUpdate(); + return preparedStatement.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) { - } } } } From 1d0d0bb3b5acd88ee21e30ab70442a9c77ce227d Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:33:22 +0900 Subject: [PATCH 18/52] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/springframework/jdbc/core/JdbcTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2ee59aae8f..3b619d6f28 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -64,7 +64,7 @@ public List query(final String sql, final RowMapper rowMapper) { } } - public int update(final String sql, final Object... parameters) { + public int update(final String sql, final Object... parameters) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); ) { From a4bc31d3ba7c34e363af96c61cd03a94070a777f Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:42:30 +0900 Subject: [PATCH 19/52] =?UTF-8?q?refactor:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EA=B0=80=20=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=EC=9D=B4=20=EC=95=84=EB=8B=8C=20Optional=EC=9D=84=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=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 | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 3b619d6f28..5f776875fd 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; public class JdbcTemplate { @@ -22,7 +23,7 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public T queryForObject(final String sql, final RowMapper rowMapper, final P parameter) { + public Optional queryForObject(final String sql, final RowMapper rowMapper, final P parameter) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); ) { @@ -32,12 +33,12 @@ public T queryForObject(final String sql, final RowMapper rowMapper, f final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { - return rowMapper.mapRow(resultSet); + return Optional.of(rowMapper.mapRow(resultSet)); } if (!resultSet.isLast()) { throw new RuntimeException("단일 데이터가 아닙니다."); } - throw new RuntimeException("찾는 데이터가 존재하지 않습니다."); + return Optional.empty(); } catch (SQLException e) { log.error(e.getMessage(), e); From 3c8ffa65e63fd83ffcaa072b68f24ca5ea86e718 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:42:41 +0900 Subject: [PATCH 20/52] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=84=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserDao.java | 12 ++---------- .../test/java/com/techcourse/dao/UserDaoTest.java | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 4612fb8073..953faea605 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -6,7 +6,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import javax.sql.DataSource; import java.util.List; public class UserDao { @@ -20,16 +19,9 @@ public class UserDao { resultSet.getString("email") ); - private final DataSource dataSource; private final JdbcTemplate jdbcTemplate; - public UserDao(final DataSource dataSource) { - this.dataSource = dataSource; - jdbcTemplate = new JdbcTemplate(dataSource); - } - public UserDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; this.jdbcTemplate = jdbcTemplate; } @@ -57,13 +49,13 @@ public List findAll() { public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id); + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id).get(); return user; } public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account); + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account).get(); return user; } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 773d7faf82..7e9cc2b01a 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -5,6 +5,7 @@ import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -16,7 +17,7 @@ class UserDaoTest { void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - userDao = new UserDao(DataSourceConfig.getInstance()); + userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } From 128b208e9895608fc6a1c6f3f3c51f17ad028a97 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:45:58 +0900 Subject: [PATCH 21/52] =?UTF-8?q?refactor:=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=A0=A4=EB=8A=94=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20DAO=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D=ED=95=98=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 --- app/src/main/java/com/techcourse/dao/UserDao.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 953faea605..74fe0a8ac1 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -49,13 +49,21 @@ public List findAll() { public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id).get(); + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id) + .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); + + log.debug("query : {}", sql); + return user; } public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account).get(); + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account) + .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); + + log.debug("query : {}", sql); + return user; } } From 66f285752f5b95ce46459e324e534ea1e7dfc922 Mon Sep 17 00:00:00 2001 From: swonny Date: Wed, 4 Oct 2023 09:46:08 +0900 Subject: [PATCH 22/52] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserDao.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 74fe0a8ac1..481f9a4b11 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -31,6 +31,8 @@ public void insert(final User user) { if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); } + + log.debug("query : {}", sql); } public void update(final User user) { @@ -39,11 +41,16 @@ public void update(final User user) { if (updatedRows < 1) { throw new RuntimeException("수정된 데이터가 없습니다."); } + + log.debug("query : {}", sql); } public List findAll() { final var sql = "select id, account, password, email from users"; final List users = jdbcTemplate.query(sql, userRowMapper); + + log.debug("query : {}", sql); + return users; } From c68ca2d08645cb05dea54617f89c62e76f1995e7 Mon Sep 17 00:00:00 2001 From: swonny Date: Thu, 5 Oct 2023 01:32:28 +0900 Subject: [PATCH 23/52] =?UTF-8?q?refactor:=20=EB=8B=A8=EC=9D=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EA=B0=80=EB=B3=80=EC=9D=B8=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EB=8F=84=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 --- .../springframework/jdbc/core/JdbcTemplate.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 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 5f776875fd..9794bd0123 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -23,13 +23,13 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public Optional queryForObject(final String sql, final RowMapper rowMapper, final P parameter) { + public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); ) { log.debug("query : {}", sql); - preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameter); + setPreparedStatement(preparedStatement, parameters); final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { @@ -46,6 +46,12 @@ public Optional queryForObject(final String sql, final RowMapper ro } } + private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameters[parameterIndex]); + } + } + public List query(final String sql, final RowMapper rowMapper) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); @@ -71,9 +77,7 @@ public int update(final String sql, final Object... parameters) { ) { log.debug("query : {}", sql); - for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { - preparedStatement.setObject(parameterIndex + 1, parameters[parameterIndex]); - } + setPreparedStatement(preparedStatement, parameters); return preparedStatement.executeUpdate(); } catch (SQLException e) { From f6fc7f658a254f4b7bd7322cfbe9194c9df06232 Mon Sep 17 00:00:00 2001 From: swonny Date: Thu, 5 Oct 2023 02:33:49 +0900 Subject: [PATCH 24/52] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/dao/UserDao.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 481f9a4b11..7a99537c8c 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -27,20 +27,21 @@ public UserDao(final JdbcTemplate jdbcTemplate) { public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - final int updatedRows = jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); + updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); + + log.debug("query : {}", sql); + } + + private void updateQuery(final String sql, final Object... objects) { + final int updatedRows = jdbcTemplate.update(sql, objects); if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); } - - log.debug("query : {}", sql); } public void update(final User user) { final var sql = "update users set (account, password, email) = (?, ?, ?)"; - final int updatedRows = jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); - if (updatedRows < 1) { - throw new RuntimeException("수정된 데이터가 없습니다."); - } + updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); log.debug("query : {}", sql); } From 14a555d09f41fc034909b031465a322371c0c5fa Mon Sep 17 00:00:00 2001 From: swonny Date: Fri, 6 Oct 2023 01:40:18 +0900 Subject: [PATCH 25/52] =?UTF-8?q?refactor:=20=EB=B3=B5=EC=88=98=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=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 | 8 +------- 1 file changed, 1 insertion(+), 7 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 9794bd0123..2f44e47739 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -46,13 +46,7 @@ public Optional queryForObject(final String sql, final RowMapper rowMa } } - private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { - for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { - preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameters[parameterIndex]); - } - } - - public List query(final String sql, final RowMapper rowMapper) { + public List queryForList(final String sql, final RowMapper rowMapper) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); ) { From e1dbf23df1b94389bd02e3bcf03a31a4a8350cbc Mon Sep 17 00:00:00 2001 From: swonny Date: Fri, 6 Oct 2023 01:46:44 +0900 Subject: [PATCH 26/52] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9E=90=EC=9B=90=20=EC=97=B4=EA=B3=A0=20=EB=8B=AB?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=BD=9C=EB=B0=B1=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=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 --- .../jdbc/core/JdbcTemplate.java | 38 +++++++++---------- .../jdbc/core/executeQueryCallback.java | 10 +++++ 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java 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 2f44e47739..26562f743f 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; import javax.sql.DataSource; import java.sql.Connection; @@ -24,14 +25,9 @@ public JdbcTemplate(final DataSource dataSource) { } public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - ) { - log.debug("query : {}", sql); + try { + final ResultSet resultSet = execute(sql, PreparedStatement::executeQuery, parameters); - setPreparedStatement(preparedStatement, parameters); - - final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return Optional.of(rowMapper.mapRow(resultSet)); } @@ -42,18 +38,13 @@ public Optional queryForObject(final String sql, final RowMapper rowMa } catch (SQLException e) { log.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new DataAccessException(e); } } public List queryForList(final String sql, final RowMapper rowMapper) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - ) { - final ResultSet resultSet = preparedStatement.executeQuery(); - - log.debug("query : {}", sql); - + final ResultSet resultSet = execute(sql, PreparedStatement::executeQuery); + try { final List objects = new ArrayList<>(); while (resultSet.next()) { final T object = rowMapper.mapRow(resultSet); @@ -66,16 +57,23 @@ public List queryForList(final String sql, final RowMapper rowMapper) } public int update(final String sql, final Object... parameters) { + return execute(sql, PreparedStatement::executeUpdate, parameters); + } + + public T execute(final String sql, final executeQueryCallback callBack, final Object... objects) { try (final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement = connection.prepareStatement(sql); ) { - log.debug("query : {}", sql); - - setPreparedStatement(preparedStatement, parameters); - - return preparedStatement.executeUpdate(); + setPreparedStatement(preparedStatement, objects); + return callBack.execute(preparedStatement); } catch (SQLException e) { throw new RuntimeException(e); } } + + private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameters[parameterIndex]); + } + } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java b/jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java new file mode 100644 index 0000000000..2f97fc4e97 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java @@ -0,0 +1,10 @@ +package org.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@FunctionalInterface +public interface executeQueryCallback { + + T execute(final PreparedStatement preparedStatement) throws SQLException; +} From 97fa8afe927905c525fbc128f8d1e95de698b64c Mon Sep 17 00:00:00 2001 From: swonny Date: Fri, 6 Oct 2023 01:51:47 +0900 Subject: [PATCH 27/52] =?UTF-8?q?refactor:=20=EC=BD=9C=EB=B0=B1=EC=97=90?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=9E=8C=EB=8B=A4=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=AC=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 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 26562f743f..c95552cc47 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,7 +2,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.dao.DataAccessException; import javax.sql.DataSource; import java.sql.Connection; @@ -25,9 +24,8 @@ public JdbcTemplate(final DataSource dataSource) { } public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { - try { - final ResultSet resultSet = execute(sql, PreparedStatement::executeQuery, parameters); - + return execute(sql, preparedStatement -> { + final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return Optional.of(rowMapper.mapRow(resultSet)); } @@ -35,25 +33,19 @@ public Optional queryForObject(final String sql, final RowMapper rowMa throw new RuntimeException("단일 데이터가 아닙니다."); } return Optional.empty(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - - throw new DataAccessException(e); - } + }, parameters); } public List queryForList(final String sql, final RowMapper rowMapper) { - final ResultSet resultSet = execute(sql, PreparedStatement::executeQuery); - try { + return execute(sql, preparedStatement -> { + final ResultSet resultSet = preparedStatement.executeQuery(); final List objects = new ArrayList<>(); while (resultSet.next()) { final T object = rowMapper.mapRow(resultSet); objects.add(object); } return objects; - } catch (SQLException e) { - throw new RuntimeException(e); - } + }); } public int update(final String sql, final Object... parameters) { From 99a2021d9000c70b4d33aebc09797d1c09255d49 Mon Sep 17 00:00:00 2001 From: swonny Date: Fri, 6 Oct 2023 01:59:45 +0900 Subject: [PATCH 28/52] =?UTF-8?q?refactor:=20jdbc=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?unchecked=20=EC=98=88=EC=99=B8=20=EB=8D=98=EC=A7=80=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/springframework/jdbc/core/JdbcTemplate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c95552cc47..233b57e9ec 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.CannotGetJdbcConnectionException; import javax.sql.DataSource; import java.sql.Connection; @@ -59,7 +60,7 @@ public T execute(final String sql, final executeQueryCallback callBack, f setPreparedStatement(preparedStatement, objects); return callBack.execute(preparedStatement); } catch (SQLException e) { - throw new RuntimeException(e); + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } } From d1c40689b9aa69911308ca1008d4d82ef8e7a982 Mon Sep 17 00:00:00 2001 From: swonny Date: Sun, 8 Oct 2023 20:00:56 +0900 Subject: [PATCH 29/52] =?UTF-8?q?test:=20=EC=8B=A4=EC=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/main/resources/application.yml | 8 +++---- .../java/transaction/stage1/Stage1Test.java | 22 ++++++++--------- .../transaction/stage2/FirstUserService.java | 6 ++--- .../java/transaction/stage2/Stage2Test.java | 24 +++++++++++-------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index b9451e1013..41dd4df9a7 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: password: # 스프링에서 출력하는 트랜잭션 로그를 직접 보고 싶으면 아래 주석 해제 -#logging: -# level: -# org.springframework.transaction.interceptor: TRACE -# org.springframework.transaction.support: DEBUG +logging: + level: + org.springframework.transaction.interceptor: TRACE + org.springframework.transaction.support: DEBUG diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index 8c29944d4e..6f80d093aa 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -58,10 +58,10 @@ private void setUp(final DataSource dataSource) { * Read phenomena | Dirty reads * Isolation level | * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | o + * Read Committed | x + * Repeatable Read | x + * Serializable | x */ @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_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. subConnection.setTransactionIsolation(isolationLevel); @@ -111,10 +111,10 @@ void dirtyReading() throws SQLException { * Read phenomena | Non-repeatable reads * Isolation level | * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | o + * Read Committed | o + * Repeatable Read | x + * Serializable | x */ @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_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -249,7 +249,7 @@ private static DataSource createMySQLDataSource(final JdbcDatabaseContainer c private static DataSource createH2DataSource() { final var jdbcDataSource = new JdbcDataSource(); // h2 로그를 확인하고 싶을 때 사용 -// jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); + jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL;"); jdbcDataSource.setUser("sa"); jdbcDataSource.setPassword(""); diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java index 9a1d415c18..f84aa6d52b 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -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()); @@ -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/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java index e522bf4365..e8a4647bb5 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -38,6 +38,7 @@ void tearDown() { /** * 생성된 트랜잭션이 몇 개인가? * 왜 그런 결과가 나왔을까? + * @Transactional(propagation = Propagation.REQUIRED)에서는 기존에 트랜잭션이 존재하는 경우 새로운 트랜잭션이 기존 트랜잭션에 참여한다. */ @Test void testRequired() { @@ -45,13 +46,14 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** * 생성된 트랜잭션이 몇 개인가? * 왜 그런 결과가 나왔을까? + * ㅊ */ @Test void testRequiredNew() { @@ -59,27 +61,29 @@ void testRequiredNew() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) + .hasSize(2) .containsExactly(""); } /** * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. * REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 지 확인해보자. + * @Transactional(propagation = Propagation.REQUIRED)에서는 새로운 트랜잭션이 커밋된 이후에 기존 트랜잭션이 롤백되어도 새로운 트랜잭션에는 영향을 미치지 않는다. */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(1); } /** * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + * firstUserService에 @Transactional 어노테이션이 없는 경우 두 번째 트랜잭션에서 트랜잭션이 생성되고, 첫 번째 트랜잭션에 있는 경우 두 번째 트랜잭션이 첫 번째 트랜잭션에 참여한다. */ @Test void testSupports() { @@ -88,7 +92,7 @@ void testSupports() { log.info("transactions : {}", actual); assertThat(actual) .hasSize(0) - .containsExactly(""); + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** @@ -102,8 +106,8 @@ void testMandatory() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** @@ -146,7 +150,7 @@ void testNever() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNever"); } } From 7a18ba7839122ce1afd5d6cbb44879605e0b417a Mon Sep 17 00:00:00 2001 From: swonny Date: Sun, 8 Oct 2023 22:17:58 +0900 Subject: [PATCH 30/52] =?UTF-8?q?refactor:=20userHistoryDao=EC=97=90?= =?UTF-8?q?=EC=84=9C=20jdbcTemplate=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/dao/UserHistoryDao.java | 56 +++++-------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..253528653b 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,62 +1,32 @@ 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(final UserHistory userHistory) throws SQLException { 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) {} - } + jdbcTemplate.update( + sql, + userHistory.getUserId(), + userHistory.getAccount(), + userHistory.getPassword(), + userHistory.getEmail(), + userHistory.getCreatedAt(), + userHistory.getCreateBy() + ); } } From 35e449d5f6eae595279705585c128e373a0cc002 Mon Sep 17 00:00:00 2001 From: swonny Date: Sun, 8 Oct 2023 22:20:42 +0900 Subject: [PATCH 31/52] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=97=90=EC=84=9C=20connection?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=ED=95=B4=20=EA=B0=99=EC=9D=80=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=EC=97=90=EC=84=9C=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=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 | 26 +++++----- .../com/techcourse/dao/UserHistoryDao.java | 4 +- .../com/techcourse/service/UserService.java | 49 ++++++++++++++++--- .../jdbc/core/JdbcTemplate.java | 26 ++++------ 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 7a99537c8c..00dbfefa68 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -6,6 +6,8 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; public class UserDao { @@ -25,39 +27,39 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(final User user) { + public void insert(final Connection connection, final User user) throws SQLException { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); + updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); log.debug("query : {}", sql); } - private void updateQuery(final String sql, final Object... objects) { - final int updatedRows = jdbcTemplate.update(sql, objects); + private void updateQuery(final Connection connection, final String sql, final Object... objects) throws SQLException { + final int updatedRows = jdbcTemplate.update(connection, sql, objects); if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); } } - public void update(final User user) { + public void update(final Connection connection, final User user) throws SQLException { final var sql = "update users set (account, password, email) = (?, ?, ?)"; - updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); + updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); log.debug("query : {}", sql); } - public List findAll() { + public List findAll(final Connection connection) throws SQLException { final var sql = "select id, account, password, email from users"; - final List users = jdbcTemplate.query(sql, userRowMapper); + final List users = jdbcTemplate.query(connection, sql, userRowMapper); log.debug("query : {}", sql); return users; } - public User findById(final Long id) { + public User findById(final Connection connection, final Long id) throws SQLException { final var sql = "select id, account, password, email from users where id = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id) + final User user = jdbcTemplate.queryForObject(connection, sql, userRowMapper, id) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); log.debug("query : {}", sql); @@ -65,9 +67,9 @@ public User findById(final Long id) { return user; } - public User findByAccount(final String account) { + public User findByAccount(final Connection connection, final String account) throws SQLException { final var sql = "select id, account, password, email from users where account = ?"; - final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account) + final User user = jdbcTemplate.queryForObject(connection, sql, userRowMapper, account) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); log.debug("query : {}", sql); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 253528653b..29834fe6fb 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; +import java.sql.Connection; import java.sql.SQLException; public class UserHistoryDao { @@ -17,9 +18,10 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final UserHistory userHistory) throws SQLException { + public void log(final Connection connection, final UserHistory userHistory) throws SQLException { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; jdbcTemplate.update( + connection, sql, userHistory.getUserId(), userHistory.getAccount(), diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fcf2159dc8..315f485dd3 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -4,29 +4,62 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import org.springframework.jdbc.CannotGetJdbcConnectionException; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; public class UserService { + private final DataSource dataSource; private final UserDao userDao; private final UserHistoryDao userHistoryDao; - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + public UserService(final DataSource dataSource, final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.dataSource = dataSource; this.userDao = userDao; this.userHistoryDao = userHistoryDao; } public User findById(final long id) { - return userDao.findById(id); + try (final Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + final User findUser = userDao.findById(connection, id); + + connection.commit(); + + return findUser; + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + } } public void insert(final User user) { - userDao.insert(user); + try (final Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + userDao.insert(connection, user); + + connection.commit(); + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + } } - 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 changePassword(long id, final String newPassword, final String createBy) { + try (final Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(connection, user); + userHistoryDao.log(connection, new UserHistory(user, createBy)); + + connection.commit(); + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + } } } 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 233b57e9ec..cf82abc62c 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,7 +2,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.jdbc.CannotGetJdbcConnectionException; import javax.sql.DataSource; import java.sql.Connection; @@ -24,8 +23,8 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { - return execute(sql, preparedStatement -> { + public Optional queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... parameters) throws SQLException { + return execute(connection, sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return Optional.of(rowMapper.mapRow(resultSet)); @@ -37,8 +36,8 @@ public Optional queryForObject(final String sql, final RowMapper rowMa }, parameters); } - public List queryForList(final String sql, final RowMapper rowMapper) { - return execute(sql, preparedStatement -> { + public List query(final Connection connection, final String sql, final RowMapper rowMapper) throws SQLException { + return execute(connection, sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); final List objects = new ArrayList<>(); while (resultSet.next()) { @@ -49,19 +48,14 @@ public List queryForList(final String sql, final RowMapper rowMapper) }); } - public int update(final String sql, final Object... parameters) { - return execute(sql, PreparedStatement::executeUpdate, parameters); + public int update(final Connection connection, final String sql, final Object... parameters) throws SQLException { + return execute(connection, sql, PreparedStatement::executeUpdate, parameters); } - public T execute(final String sql, final executeQueryCallback callBack, final Object... objects) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - ) { - setPreparedStatement(preparedStatement, objects); - return callBack.execute(preparedStatement); - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); - } + public T execute(final Connection connection, final String sql, final executeQueryCallback callBack, final Object... objects) throws SQLException { + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + setPreparedStatement(preparedStatement, objects); + return callBack.execute(preparedStatement); } private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { From 49309d753dfc5a7f368af8f0d9b48caa5cb030cc Mon Sep 17 00:00:00 2001 From: swonny Date: Sun, 8 Oct 2023 22:21:13 +0900 Subject: [PATCH 32/52] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/UserServiceTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..9a49c62846 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,35 +5,35 @@ 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 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() { + void setUp() throws SQLException { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + userDao.insert(DataSourceConfig.getInstance().getConnection(), user); } @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new UserService(DataSourceConfig.getInstance(), userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -48,7 +48,7 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new UserService(DataSourceConfig.getInstance(), userDao, userHistoryDao); final var newPassword = "newPassword"; final var createBy = "gugu"; From 89fcfbe61d8124965a26455c451e32fcd6b98316 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 00:54:14 +0900 Subject: [PATCH 33/52] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9E=90=EC=9B=90=20=EC=97=B4=EA=B3=A0=20=EB=8B=AB?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 15 +++--- .../service/TransactionExecutor.java | 10 ++++ .../com/techcourse/service/UserService.java | 49 ++++++++++--------- .../jdbc/core/JdbcTemplate.java | 12 +++++ 4 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/TransactionExecutor.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 00dbfefa68..090aaee899 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -27,25 +27,26 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(final Connection connection, final User user) throws SQLException { + public int insert(final Connection connection, final User user) throws SQLException { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); - log.debug("query : {}", sql); + + return updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); } - private void updateQuery(final Connection connection, final String sql, final Object... objects) throws SQLException { + private int updateQuery(final Connection connection, final String sql, final Object... objects) throws SQLException { final int updatedRows = jdbcTemplate.update(connection, sql, objects); if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); } + return updatedRows; } - public void update(final Connection connection, final User user) throws SQLException { + public int update(final Connection connection, final User user) throws SQLException { final var sql = "update users set (account, password, email) = (?, ?, ?)"; - updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); - log.debug("query : {}", sql); + + return updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); } public List findAll(final Connection connection) throws SQLException { diff --git a/app/src/main/java/com/techcourse/service/TransactionExecutor.java b/app/src/main/java/com/techcourse/service/TransactionExecutor.java new file mode 100644 index 0000000000..a5fad9b107 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TransactionExecutor.java @@ -0,0 +1,10 @@ +package com.techcourse.service; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface TransactionExecutor { + + T execute(final Connection connection) throws SQLException; +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 315f485dd3..06e0891cb7 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -4,6 +4,8 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.jdbc.CannotGetJdbcConnectionException; import javax.sql.DataSource; @@ -12,6 +14,8 @@ public class UserService { + private static final Logger log = LoggerFactory.getLogger(UserService.class); + private final DataSource dataSource; private final UserDao userDao; private final UserHistoryDao userHistoryDao; @@ -23,43 +27,40 @@ public UserService(final DataSource dataSource, final UserDao userDao, final Use } public User findById(final long id) { - try (final Connection connection = dataSource.getConnection()) { - connection.setAutoCommit(false); - - final User findUser = userDao.findById(connection, id); + return startTransaction(connection -> userDao.findById(connection, id)); + } - connection.commit(); + public void insert(final User user) { + startTransaction(connection -> userDao.insert(connection, user)); + } - return findUser; - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); - } + public void changePassword(long id, final String newPassword, final String createBy) { + startTransaction(connection -> { + final var user = findById(id); + user.changePassword(newPassword); + userHistoryDao.log(connection, new UserHistory(user, createBy)); + return userDao.update(connection, user); + }); } - public void insert(final User user) { + public T startTransaction(final TransactionExecutor transactionExecutor) { try (final Connection connection = dataSource.getConnection()) { connection.setAutoCommit(false); - userDao.insert(connection, user); - - connection.commit(); + return executeTransaction(transactionExecutor, connection); } catch (SQLException e) { throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } } - public void changePassword(long id, final String newPassword, final String createBy) { - try (final Connection connection = dataSource.getConnection()) { - connection.setAutoCommit(false); - - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); + private T executeTransaction(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { + try { + return transactionExecutor.execute(connection); + } catch (Exception ex) { + connection.rollback(); - connection.commit(); - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + log.error(ex.getMessage()); + throw new RuntimeException("실행 중 예외가 발생했습니다."); } } } 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 cf82abc62c..ec69213d73 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.CannotGetJdbcConnectionException; import javax.sql.DataSource; import java.sql.Connection; @@ -52,6 +53,17 @@ public int update(final Connection connection, final String sql, final Object... return execute(connection, sql, PreparedStatement::executeUpdate, parameters); } + public T execute(final String sql, final executeQueryCallback callBack, final Object... objects) { + try (final Connection connection = dataSource.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + ) { + setPreparedStatement(preparedStatement, objects); + return callBack.execute(preparedStatement); + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + } + } + public T execute(final Connection connection, final String sql, final executeQueryCallback callBack, final Object... objects) throws SQLException { final PreparedStatement preparedStatement = connection.prepareStatement(sql); setPreparedStatement(preparedStatement, objects); From 0c1873a6d3713ac41418e74d7fe2b3aedbdc66cc Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:09:29 +0900 Subject: [PATCH 34/52] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/service/UserService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 06e0891cb7..9a5f3d3d37 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -47,13 +47,13 @@ public T startTransaction(final TransactionExecutor transactionExecutor) try (final Connection connection = dataSource.getConnection()) { connection.setAutoCommit(false); - return executeTransaction(transactionExecutor, connection); + return execute(transactionExecutor, connection); } catch (SQLException e) { throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } } - private T executeTransaction(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { + private T execute(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { try { return transactionExecutor.execute(connection); } catch (Exception ex) { From 30ed91efca98b2727f621c86068aa92ae2b9c274 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:11:28 +0900 Subject: [PATCH 35/52] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=A2=85=EB=A3=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/service/UserService.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 9a5f3d3d37..c43c476efe 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -47,15 +47,19 @@ public T startTransaction(final TransactionExecutor transactionExecutor) try (final Connection connection = dataSource.getConnection()) { connection.setAutoCommit(false); - return execute(transactionExecutor, connection); + return commitTransaction(transactionExecutor, connection); } catch (SQLException e) { throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } } - private T execute(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { + private T commitTransaction(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { try { - return transactionExecutor.execute(connection); + final T result = transactionExecutor.execute(connection); + + connection.commit(); + + return result; } catch (Exception ex) { connection.rollback(); From c570870ca987fcac912bad4a9082df1894f05234 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:31:42 +0900 Subject: [PATCH 36/52] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=8D=98=EC=A7=80=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 --- app/src/main/java/com/techcourse/service/UserService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index c43c476efe..ab1084af12 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -6,6 +6,7 @@ import com.techcourse.domain.UserHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.CannotGetJdbcConnectionException; import javax.sql.DataSource; @@ -64,7 +65,7 @@ private T commitTransaction(final TransactionExecutor transactionExecutor connection.rollback(); log.error(ex.getMessage()); - throw new RuntimeException("실행 중 예외가 발생했습니다."); + throw new DataAccessException("실행 중 예외가 발생했습니다."); } } } From 33dff9181adf4acb2a448849c29af39a7fc455dd Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:32:43 +0900 Subject: [PATCH 37/52] =?UTF-8?q?refactor:=20preparedStatement=20try-catch?= =?UTF-8?q?=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8F=84=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 --- .../jdbc/core/JdbcTemplate.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 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 ec69213d73..23ed446c8c 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -24,7 +24,7 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public Optional queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... parameters) throws SQLException { + public Optional queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... parameters) { return execute(connection, sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { @@ -37,7 +37,7 @@ public Optional queryForObject(final Connection connection, final String }, parameters); } - public List query(final Connection connection, final String sql, final RowMapper rowMapper) throws SQLException { + public List query(final Connection connection, final String sql, final RowMapper rowMapper) { return execute(connection, sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); final List objects = new ArrayList<>(); @@ -49,27 +49,20 @@ public List query(final Connection connection, final String sql, final Ro }); } - public int update(final Connection connection, final String sql, final Object... parameters) throws SQLException { + public int update(final Connection connection, final String sql, final Object... parameters) { return execute(connection, sql, PreparedStatement::executeUpdate, parameters); } - public T execute(final String sql, final executeQueryCallback callBack, final Object... objects) { - try (final Connection connection = dataSource.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - ) { + public T execute(final Connection connection, final String sql, final executeQueryCallback callBack, final Object... objects) { + try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { setPreparedStatement(preparedStatement, objects); return callBack.execute(preparedStatement); - } catch (SQLException e) { + } catch (SQLException ex) { + log.error(ex.getMessage()); throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } } - public T execute(final Connection connection, final String sql, final executeQueryCallback callBack, final Object... objects) throws SQLException { - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - setPreparedStatement(preparedStatement, objects); - return callBack.execute(preparedStatement); - } - private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameters[parameterIndex]); From 3fc681d54f8afe9de2796d06c1e4533ef1f4a21c Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:33:36 +0900 Subject: [PATCH 38/52] =?UTF-8?q?fix:=20=EC=B2=AB=20=EB=B2=88=EC=A7=B8=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A7=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=98=EB=8A=94=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/org/springframework/jdbc/core/JdbcTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 23ed446c8c..8c14ce2e83 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -16,7 +16,7 @@ public class JdbcTemplate { private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); - private static final int FIRST_PARAMETER_INDEX = 1; + private static final int PARAMETER_OFFSET = 1; private final DataSource dataSource; @@ -65,7 +65,7 @@ public T execute(final Connection connection, final String sql, final execut private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { - preparedStatement.setObject(FIRST_PARAMETER_INDEX, parameters[parameterIndex]); + preparedStatement.setObject(PARAMETER_OFFSET + parameterIndex, parameters[parameterIndex]); } } } From b4316eff48a0e8282aa496cfd5423f9be31f8c77 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:34:11 +0900 Subject: [PATCH 39/52] =?UTF-8?q?test:=20UserHistoryDao=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=9D=BC=20mock=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88?= =?UTF-8?q?=EC=B2=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/com/techcourse/service/MockUserHistoryDao.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..9937b42bbc 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(final Connection connection, final UserHistory userHistory) { throw new DataAccessException(); } } From 8b5b02d643493daaf1b146cd183c9bc768ac085c Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 01:37:33 +0900 Subject: [PATCH 40/52] =?UTF-8?q?test:=20UserDao=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=8B=88=EC=B2=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/techcourse/dao/UserDaoTest.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 7e9cc2b01a..19606127bb 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -7,63 +7,71 @@ 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; class UserDaoTest { private UserDao userDao; + private DataSource dataSource; @BeforeEach - void setup() { - DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + void setup() throws SQLException { + dataSource = DataSourceConfig.getInstance(); + DatabasePopulatorUtils.execute(dataSource); userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(user); + userDao.insert(dataSource.getConnection(), user); } @Test - void findAll() { - final var users = userDao.findAll(); + void findAll() throws SQLException { + final var users = userDao.findAll(dataSource.getConnection()); assertThat(users).isNotEmpty(); } @Test - void findById() { - final var user = userDao.findById(1L); + void findById() throws SQLException { + final var user = userDao.findById(dataSource.getConnection(), 1L); assertThat(user.getAccount()).isEqualTo("gugu"); } @Test - void findByAccount() { + void findByAccount() throws SQLException { final var account = "gugu"; - final var user = userDao.findByAccount(account); + final var user = userDao.findByAccount(dataSource.getConnection(), account); assertThat(user.getAccount()).isEqualTo(account); } @Test - void insert() { + void insert() throws SQLException { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); - userDao.insert(user); + final Connection connection = dataSource.getConnection(); + userDao.insert(connection, user); - final var actual = userDao.findById(2L); + final var actual = userDao.findById(connection, 2L); assertThat(actual.getAccount()).isEqualTo(account); } @Test - void update() { + void update() throws SQLException { final var newPassword = "password99"; - final var user = userDao.findById(1L); + final Connection connection = dataSource.getConnection(); + final var user = userDao.findById(connection, 1L); user.changePassword(newPassword); - userDao.update(user); + userDao.update(connection, user); - final var actual = userDao.findById(1L); + final var actual = userDao.findById(connection, 1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } From fe1a0cead84d152791287086c6d03f2b99ad62ec Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 02:10:46 +0900 Subject: [PATCH 41/52] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=ED=98=95?= =?UTF-8?q?=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=8C=80?= =?UTF-8?q?=EC=86=8C=EB=AC=B8=EC=9E=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{executeQueryCallback.java => ExecuteQueryCallback.java} | 2 +- .../main/java/org/springframework/jdbc/core/JdbcTemplate.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename jdbc/src/main/java/org/springframework/jdbc/core/{executeQueryCallback.java => ExecuteQueryCallback.java} (82%) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java b/jdbc/src/main/java/org/springframework/jdbc/core/ExecuteQueryCallback.java similarity index 82% rename from jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java rename to jdbc/src/main/java/org/springframework/jdbc/core/ExecuteQueryCallback.java index 2f97fc4e97..913c5f0e1c 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/executeQueryCallback.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/ExecuteQueryCallback.java @@ -4,7 +4,7 @@ import java.sql.SQLException; @FunctionalInterface -public interface executeQueryCallback { +public interface ExecuteQueryCallback { T execute(final PreparedStatement preparedStatement) throws SQLException; } 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 8c14ce2e83..b15ba4510e 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -53,7 +53,7 @@ public int update(final Connection connection, final String sql, final Object... return execute(connection, sql, PreparedStatement::executeUpdate, parameters); } - public T execute(final Connection connection, final String sql, final executeQueryCallback callBack, final Object... objects) { + public T execute(final Connection connection, final String sql, final ExecuteQueryCallback callBack, final Object... objects) { try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { setPreparedStatement(preparedStatement, objects); return callBack.execute(preparedStatement); From c732706af5eca3bde2a39a43ad08919b8fc89660 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 14:01:27 +0900 Subject: [PATCH 42/52] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=84=A5=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=9D=84=20DataSourceUtils=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=ED=95=98=EB=8F=84=EB=A1=9D=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 --- app/src/main/java/com/techcourse/service/UserService.java | 5 ++++- .../org/springframework/jdbc/datasource/DataSourceUtils.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index ab1084af12..aec931f31c 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; @@ -45,7 +46,8 @@ public void changePassword(long id, final String newPassword, final String creat } public T startTransaction(final TransactionExecutor transactionExecutor) { - try (final Connection connection = dataSource.getConnection()) { + try { + final Connection connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); return commitTransaction(transactionExecutor, connection); @@ -59,6 +61,7 @@ private T commitTransaction(final TransactionExecutor transactionExecutor final T result = transactionExecutor.execute(connection); connection.commit(); + DataSourceUtils.releaseConnection(connection, dataSource); return result; } catch (Exception ex) { 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..00efa86c6e 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -7,10 +7,10 @@ import java.sql.Connection; import java.sql.SQLException; -// 4단계 미션에서 사용할 것 public abstract class DataSourceUtils { - private DataSourceUtils() {} + private DataSourceUtils() { + } public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { Connection connection = TransactionSynchronizationManager.getResource(dataSource); @@ -30,6 +30,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { connection.close(); + TransactionSynchronizationManager.unbindResource(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } From 398d8c89d1eccadc8ade0c85bd342eacd6e4ef6c Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:23:20 +0900 Subject: [PATCH 43/52] =?UTF-8?q?test:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{UserServiceTest.java => AppUserServiceTest.java} | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => AppUserServiceTest.java} (84%) diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java similarity index 84% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 9a49c62846..dc73a7e11d 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -class UserServiceTest { +class AppUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @@ -27,13 +27,13 @@ void setUp() throws SQLException { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(DataSourceConfig.getInstance().getConnection(), user); + userDao.insert(user); } @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(DataSourceConfig.getInstance(), userDao, userHistoryDao); + final var userService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -48,7 +48,10 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(DataSourceConfig.getInstance(), userDao, userHistoryDao); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; From 5d63d58a033e86ffb882201ebd50d77cc8de5305 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:29:12 +0900 Subject: [PATCH 44/52] =?UTF-8?q?test:=20=EC=BB=A4=EB=84=A5=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20Tra?= =?UTF-8?q?nsactionSynchronizationManager=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 26 +++++++++---------- .../com/techcourse/dao/UserHistoryDao.java | 6 +---- .../java/com/techcourse/dao/UserDaoTest.java | 18 ++++++------- .../service/MockUserHistoryDao.java | 2 +- .../jdbc/core/JdbcTemplate.java | 20 ++++++++------ .../jdbc/datasource/DataSourceUtils.java | 11 ++------ .../TransactionSynchronizationManager.java | 19 +++++++++++--- 7 files changed, 52 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 090aaee899..dbaa46c96c 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -6,8 +6,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import java.sql.Connection; -import java.sql.SQLException; import java.util.List; public class UserDao { @@ -27,40 +25,40 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public int insert(final Connection connection, final User user) throws SQLException { + public int insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; log.debug("query : {}", sql); - return updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); + return updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - private int updateQuery(final Connection connection, final String sql, final Object... objects) throws SQLException { - final int updatedRows = jdbcTemplate.update(connection, sql, objects); + private int updateQuery(final String sql, final Object... objects) { + final int updatedRows = jdbcTemplate.update(sql, objects); if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); } return updatedRows; } - public int update(final Connection connection, final User user) throws SQLException { + public int update(final User user) { final var sql = "update users set (account, password, email) = (?, ?, ?)"; log.debug("query : {}", sql); - return updateQuery(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); + return updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - public List findAll(final Connection connection) throws SQLException { + public List findAll() { final var sql = "select id, account, password, email from users"; - final List users = jdbcTemplate.query(connection, sql, userRowMapper); + final List users = jdbcTemplate.query(sql, userRowMapper); log.debug("query : {}", sql); return users; } - public User findById(final Connection connection, final Long id) throws SQLException { + public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - final User user = jdbcTemplate.queryForObject(connection, sql, userRowMapper, id) + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); log.debug("query : {}", sql); @@ -68,9 +66,9 @@ public User findById(final Connection connection, final Long id) throws SQLExcep return user; } - public User findByAccount(final Connection connection, final String account) throws SQLException { + public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - final User user = jdbcTemplate.queryForObject(connection, sql, userRowMapper, account) + final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); log.debug("query : {}", sql); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 29834fe6fb..b67bc6b889 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -5,9 +5,6 @@ import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; -import java.sql.SQLException; - public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); @@ -18,10 +15,9 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection connection, final UserHistory userHistory) throws SQLException { + public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; jdbcTemplate.update( - connection, sql, userHistory.getUserId(), userHistory.getAccount(), diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 19606127bb..6941746015 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -25,19 +25,19 @@ void setup() throws SQLException { userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(dataSource.getConnection(), user); + userDao.insert(user); } @Test void findAll() throws SQLException { - final var users = userDao.findAll(dataSource.getConnection()); + final var users = userDao.findAll(); assertThat(users).isNotEmpty(); } @Test void findById() throws SQLException { - final var user = userDao.findById(dataSource.getConnection(), 1L); + final var user = userDao.findById(1L); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -45,7 +45,7 @@ void findById() throws SQLException { @Test void findByAccount() throws SQLException { final var account = "gugu"; - final var user = userDao.findByAccount(dataSource.getConnection(), account); + final var user = userDao.findByAccount(account); assertThat(user.getAccount()).isEqualTo(account); } @@ -55,9 +55,9 @@ void insert() throws SQLException { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); final Connection connection = dataSource.getConnection(); - 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); } @@ -66,12 +66,12 @@ void insert() throws SQLException { void update() throws SQLException { final var newPassword = "password99"; final Connection connection = dataSource.getConnection(); - final var user = userDao.findById(connection, 1L); + final var user = userDao.findById(1L); user.changePassword(newPassword); - userDao.update(connection, user); + userDao.update(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 9937b42bbc..2db229920f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -14,7 +14,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final Connection connection, final UserHistory userHistory) { + public void log(final UserHistory userHistory) { throw new DataAccessException(); } } 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 b15ba4510e..20e0203ffe 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.jdbc.CannotGetJdbcConnectionException; +import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; import java.sql.Connection; @@ -24,8 +25,8 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public Optional queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... parameters) { - return execute(connection, sql, preparedStatement -> { + public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { + return execute(sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return Optional.of(rowMapper.mapRow(resultSet)); @@ -37,8 +38,8 @@ public Optional queryForObject(final Connection connection, final String }, parameters); } - public List query(final Connection connection, final String sql, final RowMapper rowMapper) { - return execute(connection, sql, preparedStatement -> { + public List query(final String sql, final RowMapper rowMapper) { + return execute(sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); final List objects = new ArrayList<>(); while (resultSet.next()) { @@ -49,12 +50,15 @@ public List query(final Connection connection, final String sql, final Ro }); } - public int update(final Connection connection, final String sql, final Object... parameters) { - return execute(connection, sql, PreparedStatement::executeUpdate, parameters); + public int update(final String sql, final Object... parameters) { + return execute(sql, PreparedStatement::executeUpdate, parameters); } - public T execute(final Connection connection, final String sql, final ExecuteQueryCallback callBack, final Object... objects) { - try (final PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + public T execute(final String sql, final ExecuteQueryCallback callBack, final Object... objects) { + try { + final Connection connection = TransactionSynchronizationManager.getResource(dataSource); + final PreparedStatement preparedStatement = connection.prepareStatement(sql); + setPreparedStatement(preparedStatement, objects); return callBack.execute(preparedStatement); } catch (SQLException ex) { 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 00efa86c6e..006e527264 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -12,16 +12,9 @@ public abstract class DataSourceUtils { private DataSourceUtils() { } - public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { - Connection connection = TransactionSynchronizationManager.getResource(dataSource); - if (connection != null) { - return connection; - } - + public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException, SQLException { try { - connection = dataSource.getConnection(); - TransactionSynchronizationManager.bindResource(dataSource, connection); - return connection; + return TransactionSynchronizationManager.getResource(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); } 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..84282c7c2e 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -2,19 +2,30 @@ import javax.sql.DataSource; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); + private static final ThreadLocal isActive = ThreadLocal.withInitial(() -> false); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } - public static Connection getResource(DataSource key) { - return null; + public static Connection getResource(DataSource dataSource) throws SQLException { + if (resources.get().containsKey(dataSource)) { + return resources.get().get(dataSource); + } + final Connection connection = dataSource.getConnection(); + bindResource(dataSource, connection); + return connection; } public static void bindResource(DataSource key, Connection value) { + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { From c588c4061f508083c2453074562e94c241bcac3a Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:32:58 +0900 Subject: [PATCH 45/52] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20Transacti?= =?UTF-8?q?onSynchronizationManager=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 5 +- .../TransactionSynchronizationManager.java | 62 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 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 20e0203ffe..c9a5ab0eab 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -60,8 +60,11 @@ public T execute(final String sql, final ExecuteQueryCallback callBack, f final PreparedStatement preparedStatement = connection.prepareStatement(sql); setPreparedStatement(preparedStatement, objects); - return callBack.execute(preparedStatement); + + return TransactionSynchronizationManager.commit(callBack, preparedStatement, dataSource); } catch (SQLException ex) { + TransactionSynchronizationManager.rollback(dataSource); + log.error(ex.getMessage()); throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); } 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 84282c7c2e..223f58ff47 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,5 +1,10 @@ package org.springframework.transaction.support; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.ExecuteQueryCallback; +import org.springframework.jdbc.datasource.DataSourceUtils; + import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; @@ -29,6 +34,61 @@ public static void bindResource(DataSource key, Connection value) { } public static Connection unbindResource(DataSource key) { - return null; + return resources.get().remove(key); + } + + public static void startTransaction() { + isActive.set(true); + } + + public static void finishTransaction() { + isActive.set(false); + } + + public static T commit(final ExecuteQueryCallback callBack, final PreparedStatement preparedStatement, final DataSource dataSource) { + try { + final Connection connection = DataSourceUtils.getConnection(dataSource); + connection.setAutoCommit(false); + + final T result = callBack.execute(preparedStatement); + + commitTransaction(dataSource, connection); + + return result; + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + } + } + + private static void commitTransaction(final DataSource dataSource, final Connection connection) { + try { + connection.commit(); + + clear(connection, dataSource); + } catch (Exception ex) { + rollback(dataSource); + + throw new DataAccessException("실행 중 예외가 발생했습니다."); + } + } + + public static void rollback(final DataSource dataSource) { + try { + final Connection connection = resources.get().get(dataSource); + connection.rollback(); + + clear(connection, dataSource); + } catch (SQLException ex) { + throw new DataAccessException("트랜잭션 롤백 중 예외가 발생했습니다."); + } + } + + private static void clear(final Connection connection, final DataSource dataSource) { + try { + connection.close(); + DataSourceUtils.releaseConnection(connection, dataSource); + } catch (SQLException e) { + throw new DataAccessException("실행 중 예외가 발생했습니다."); + } } } From 70941b90c845af66f556809efe452f87b86b7e86 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:33:36 +0900 Subject: [PATCH 46/52] =?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 | 35 ++++++++++ .../com/techcourse/service/TxUserService.java | 42 +++++++++++ .../com/techcourse/service/UserService.java | 70 ++----------------- 3 files changed, 81 insertions(+), 66 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..fb2d7a312c --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,35 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + public void changePassword(long id, final String newPassword, final String createBy) { + final var user = findById(id); + user.changePassword(newPassword); + userHistoryDao.log(new UserHistory(user, createBy)); + userDao.update(user); + } +} 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..005a078f8e --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,42 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class TxUserService implements UserService { + + private final AppUserService userService; + + public TxUserService(final AppUserService userService) { + this.userService = userService; + } + + @Override + public User findById(final long id) { + TransactionSynchronizationManager.startTransaction(); + + final User findUser = userService.findById(id); + + TransactionSynchronizationManager.finishTransaction(); + + return findUser; + } + + @Override + public void insert(final User user) { + TransactionSynchronizationManager.startTransaction(); + + userService.insert(user); + + TransactionSynchronizationManager.finishTransaction(); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + TransactionSynchronizationManager.startTransaction(); + + userService.changePassword(id, newPassword, createBy); + + TransactionSynchronizationManager.finishTransaction(); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index aec931f31c..b14dbcacbf 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,74 +1,12 @@ package com.techcourse.service; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.jdbc.datasource.DataSourceUtils; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; +public interface UserService { -public class UserService { + User findById(final long id); - private static final Logger log = LoggerFactory.getLogger(UserService.class); + void insert(final User user); - private final DataSource dataSource; - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - - public UserService(final DataSource dataSource, final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.dataSource = dataSource; - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } - - public User findById(final long id) { - return startTransaction(connection -> userDao.findById(connection, id)); - } - - public void insert(final User user) { - startTransaction(connection -> userDao.insert(connection, user)); - } - - public void changePassword(long id, final String newPassword, final String createBy) { - startTransaction(connection -> { - final var user = findById(id); - user.changePassword(newPassword); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - return userDao.update(connection, user); - }); - } - - public T startTransaction(final TransactionExecutor transactionExecutor) { - try { - final Connection connection = DataSourceUtils.getConnection(dataSource); - connection.setAutoCommit(false); - - return commitTransaction(transactionExecutor, connection); - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); - } - } - - private T commitTransaction(final TransactionExecutor transactionExecutor, final Connection connection) throws SQLException { - try { - final T result = transactionExecutor.execute(connection); - - connection.commit(); - DataSourceUtils.releaseConnection(connection, dataSource); - - return result; - } catch (Exception ex) { - connection.rollback(); - - log.error(ex.getMessage()); - throw new DataAccessException("실행 중 예외가 발생했습니다."); - } - } + void changePassword(final long id, final String newPassword, final String createBy); } From 95dcdfd5c91c075aa83d458fde30333409c9aaf8 Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:34:06 +0900 Subject: [PATCH 47/52] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=A8=EC=88=98=ED=98=95=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TransactionExecutor.java | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 app/src/main/java/com/techcourse/service/TransactionExecutor.java diff --git a/app/src/main/java/com/techcourse/service/TransactionExecutor.java b/app/src/main/java/com/techcourse/service/TransactionExecutor.java deleted file mode 100644 index a5fad9b107..0000000000 --- a/app/src/main/java/com/techcourse/service/TransactionExecutor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.techcourse.service; - -import java.sql.Connection; -import java.sql.SQLException; - -@FunctionalInterface -public interface TransactionExecutor { - - T execute(final Connection connection) throws SQLException; -} From 973c71426a8c9aeb3d577cac6c3cbf9d45dde4cc Mon Sep 17 00:00:00 2001 From: swonny Date: Mon, 9 Oct 2023 17:55:52 +0900 Subject: [PATCH 48/52] =?UTF-8?q?fix:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=A2=85=EB=A3=8C=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20commit=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../support/TransactionSynchronizationManager.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 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 223f58ff47..32c4d7a7b5 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -62,9 +62,11 @@ public static T commit(final ExecuteQueryCallback callBack, final Prepare private static void commitTransaction(final DataSource dataSource, final Connection connection) { try { - connection.commit(); + if (!isActive.get()) { + connection.commit(); - clear(connection, dataSource); + clear(connection, dataSource); + } } catch (Exception ex) { rollback(dataSource); @@ -74,10 +76,12 @@ private static void commitTransaction(final DataSource dataSource, final Connect public static void rollback(final DataSource dataSource) { try { - final Connection connection = resources.get().get(dataSource); - connection.rollback(); + if (!isActive.get()) { + final Connection connection = resources.get().get(dataSource); + connection.rollback(); - clear(connection, dataSource); + clear(connection, dataSource); + } } catch (SQLException ex) { throw new DataAccessException("트랜잭션 롤백 중 예외가 발생했습니다."); } From e63cb29c2fd2868d89491be5f7370e50e3d65a41 Mon Sep 17 00:00:00 2001 From: swonny Date: Sat, 14 Oct 2023 17:49:01 +0900 Subject: [PATCH 49/52] =?UTF-8?q?fix:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=94=20=EB=A7=A4=EB=8B=88=EC=A0=80=EC=9D=98=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=8B=9C=EC=9E=91=EA=B3=BC=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=EA=B0=80=20=EC=A0=95=EC=83=81=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techcourse/service/AppUserService.java | 2 +- .../com/techcourse/service/TxUserService.java | 18 ++-- .../service/AppUserServiceTest.java | 2 +- .../jdbc/core/JdbcTemplate.java | 18 +++- .../jdbc/datasource/DataSourceUtils.java | 5 +- .../TransactionSynchronizationManager.java | 82 ++++++++----------- 6 files changed, 65 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java index fb2d7a312c..52e720a856 100644 --- a/app/src/main/java/com/techcourse/service/AppUserService.java +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -29,7 +29,7 @@ public void insert(final User user) { public void changePassword(long id, final String newPassword, final String createBy) { final var user = findById(id); user.changePassword(newPassword); - userHistoryDao.log(new UserHistory(user, createBy)); userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); } } diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index 005a078f8e..74787c15c5 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -3,40 +3,44 @@ import com.techcourse.domain.User; import org.springframework.transaction.support.TransactionSynchronizationManager; +import javax.sql.DataSource; + public class TxUserService implements UserService { + private final DataSource dataSource; private final AppUserService userService; - public TxUserService(final AppUserService userService) { + public TxUserService(final DataSource dataSource, final AppUserService userService) { + this.dataSource = dataSource; this.userService = userService; } @Override public User findById(final long id) { - TransactionSynchronizationManager.startTransaction(); + TransactionSynchronizationManager.startNewTransaction(dataSource); final User findUser = userService.findById(id); - TransactionSynchronizationManager.finishTransaction(); + TransactionSynchronizationManager.finishTransaction(dataSource); return findUser; } @Override public void insert(final User user) { - TransactionSynchronizationManager.startTransaction(); + TransactionSynchronizationManager.startNewTransaction(dataSource); userService.insert(user); - TransactionSynchronizationManager.finishTransaction(); + TransactionSynchronizationManager.finishTransaction(dataSource); } @Override public void changePassword(final long id, final String newPassword, final String createBy) { - TransactionSynchronizationManager.startTransaction(); + TransactionSynchronizationManager.startNewTransaction(dataSource); userService.changePassword(id, newPassword, createBy); - TransactionSynchronizationManager.finishTransaction(); + TransactionSynchronizationManager.finishTransaction(dataSource); } } diff --git a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java index dc73a7e11d..89db5c5064 100644 --- a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -51,7 +51,7 @@ void testTransactionRollback() { // 애플리케이션 서비스 final var appUserService = new AppUserService(userDao, userHistoryDao); // 트랜잭션 서비스 추상화 - final var userService = new TxUserService(appUserService); + final var userService = new TxUserService(DataSourceConfig.getInstance(), appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; 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 c9a5ab0eab..ad831ce2b1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -56,12 +56,22 @@ public int update(final String sql, final Object... parameters) { public T execute(final String sql, final ExecuteQueryCallback callBack, final Object... objects) { try { - final Connection connection = TransactionSynchronizationManager.getResource(dataSource); - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - + final PreparedStatement preparedStatement = createPreparedStatement(sql); setPreparedStatement(preparedStatement, objects); - return TransactionSynchronizationManager.commit(callBack, preparedStatement, dataSource); + final T result = callBack.execute(preparedStatement); + TransactionSynchronizationManager.commitTransaction(dataSource); + + return result; + } catch (final SQLException ex) { + throw new RuntimeException("실행 중 예외가 발생했습니다."); + } + } + + private PreparedStatement createPreparedStatement(final String sql) { + try { + final Connection connection = TransactionSynchronizationManager.getResource(dataSource); + return connection.prepareStatement(sql); } catch (SQLException ex) { TransactionSynchronizationManager.rollback(dataSource); 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 006e527264..825156bd9a 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -12,7 +12,7 @@ public abstract class DataSourceUtils { private DataSourceUtils() { } - public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException, SQLException { + public static Connection getConnection(final DataSource dataSource) throws CannotGetJdbcConnectionException, SQLException { try { return TransactionSynchronizationManager.getResource(dataSource); } catch (SQLException ex) { @@ -20,10 +20,9 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd } } - public static void releaseConnection(Connection connection, DataSource dataSource) { + public static void releaseConnection(final Connection connection) { try { connection.close(); - TransactionSynchronizationManager.unbindResource(dataSource); } 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 32c4d7a7b5..f2e2db98e2 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,13 +1,10 @@ package org.springframework.transaction.support; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.jdbc.core.ExecuteQueryCallback; import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @@ -20,54 +17,35 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() { } - public static Connection getResource(DataSource dataSource) throws SQLException { - if (resources.get().containsKey(dataSource)) { - return resources.get().get(dataSource); + public static Connection getResource(final DataSource dataSource) throws SQLException { + if (!isActive.get() || !resources.get().containsKey(dataSource)) { + throw new RuntimeException("시작한 트랜잭션이 없습니다."); } - final Connection connection = dataSource.getConnection(); - bindResource(dataSource, connection); - return connection; + return resources.get().get(dataSource); } - public static void bindResource(DataSource key, Connection value) { - resources.get().put(key, value); - } - - public static Connection unbindResource(DataSource key) { - return resources.get().remove(key); - } - - public static void startTransaction() { - isActive.set(true); - } - - public static void finishTransaction() { - isActive.set(false); - } - - public static T commit(final ExecuteQueryCallback callBack, final PreparedStatement preparedStatement, final DataSource dataSource) { + private static Connection bindResource(final DataSource dataSource) { try { final Connection connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); - - final T result = callBack.execute(preparedStatement); - - commitTransaction(dataSource, connection); - - return result; - } catch (SQLException e) { - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); + resources.get().put(dataSource, connection); + return connection; + } catch (final SQLException e) { + throw new RuntimeException(e); } } - private static void commitTransaction(final DataSource dataSource, final Connection connection) { - try { - if (!isActive.get()) { - connection.commit(); + public static void startNewTransaction(final DataSource dataSource) { + // TODO: 2023/10/14 transaction이 active인 경우 기존 트랜잭션 참여 여부에 따라 새로 생성하거나 기존 트랜잭션에 참여하도록 하는 것도 필요할까? + isActive.set(true); + bindResource(dataSource); + } - clear(connection, dataSource); - } - } catch (Exception ex) { + public static void commitTransaction(final DataSource dataSource) { + try { + final Connection connection = resources.get().get(dataSource); + connection.commit(); + } catch (final SQLException ex) { rollback(dataSource); throw new DataAccessException("실행 중 예외가 발생했습니다."); @@ -80,19 +58,31 @@ public static void rollback(final DataSource dataSource) { final Connection connection = resources.get().get(dataSource); connection.rollback(); - clear(connection, dataSource); + clear(dataSource); } - } catch (SQLException ex) { + } catch (final SQLException ex) { throw new DataAccessException("트랜잭션 롤백 중 예외가 발생했습니다."); } } - private static void clear(final Connection connection, final DataSource dataSource) { + private static void clear(final DataSource dataSource) { try { + final Connection connection = resources.get().get(dataSource); + connection.setAutoCommit(true); connection.close(); - DataSourceUtils.releaseConnection(connection, dataSource); - } catch (SQLException e) { + DataSourceUtils.releaseConnection(connection); + } catch (final SQLException ex) { throw new DataAccessException("실행 중 예외가 발생했습니다."); } } + + public static void finishTransaction(final DataSource dataSource) { + isActive.set(false); + unbindResource(dataSource); + clear(dataSource); + } + + public static Connection unbindResource(final DataSource dataSource) { + return resources.get().remove(dataSource); + } } From 05a153ff9e274006afa14c8bb1a4b705195fce4a Mon Sep 17 00:00:00 2001 From: swonny Date: Sat, 14 Oct 2023 18:49:10 +0900 Subject: [PATCH 50/52] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20TxUserService=EC=97=90=EC=84=9C=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TransactionCallback.java | 7 ++++ .../service/TransactionManager.java | 24 +++++++++++++ .../com/techcourse/service/TxUserService.java | 35 +++++++------------ .../service/AppUserServiceTest.java | 3 +- 4 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/TransactionCallback.java create mode 100644 app/src/main/java/com/techcourse/service/TransactionManager.java diff --git a/app/src/main/java/com/techcourse/service/TransactionCallback.java b/app/src/main/java/com/techcourse/service/TransactionCallback.java new file mode 100644 index 0000000000..25695e211f --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TransactionCallback.java @@ -0,0 +1,7 @@ +package com.techcourse.service; + +@FunctionalInterface +public interface TransactionCallback { + + T execute(); +} diff --git a/app/src/main/java/com/techcourse/service/TransactionManager.java b/app/src/main/java/com/techcourse/service/TransactionManager.java new file mode 100644 index 0000000000..9e451a6498 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TransactionManager.java @@ -0,0 +1,24 @@ +package com.techcourse.service; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.sql.DataSource; + +public class TransactionManager { + + private final DataSource dataSource; + + public TransactionManager(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(final TransactionCallback callback) { + TransactionSynchronizationManager.startNewTransaction(dataSource); + + final T result = callback.execute(); + + TransactionSynchronizationManager.finishTransaction(dataSource); + + return result; + } +} diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index 74787c15c5..7e4a245a9b 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -1,46 +1,35 @@ package com.techcourse.service; import com.techcourse.domain.User; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -import javax.sql.DataSource; public class TxUserService implements UserService { - private final DataSource dataSource; private final AppUserService userService; + private final TransactionManager transactionManager; - public TxUserService(final DataSource dataSource, final AppUserService userService) { - this.dataSource = dataSource; + public TxUserService(final AppUserService userService, final TransactionManager transactionManager) { this.userService = userService; + this.transactionManager = transactionManager; } @Override public User findById(final long id) { - TransactionSynchronizationManager.startNewTransaction(dataSource); - - final User findUser = userService.findById(id); - - TransactionSynchronizationManager.finishTransaction(dataSource); - - return findUser; + return transactionManager.execute(() -> userService.findById(id)); } @Override public void insert(final User user) { - TransactionSynchronizationManager.startNewTransaction(dataSource); - - userService.insert(user); - - TransactionSynchronizationManager.finishTransaction(dataSource); + transactionManager.execute(() -> { + userService.insert(user); + return null; + }); } @Override public void changePassword(final long id, final String newPassword, final String createBy) { - TransactionSynchronizationManager.startNewTransaction(dataSource); - - userService.changePassword(id, newPassword, createBy); - - TransactionSynchronizationManager.finishTransaction(dataSource); + transactionManager.execute(() -> { + userService.changePassword(id, newPassword, createBy); + return null; + }); } } diff --git a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 89db5c5064..04e450099f 100644 --- a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -51,7 +51,8 @@ void testTransactionRollback() { // 애플리케이션 서비스 final var appUserService = new AppUserService(userDao, userHistoryDao); // 트랜잭션 서비스 추상화 - final var userService = new TxUserService(DataSourceConfig.getInstance(), appUserService); + final var transactionManager = new TransactionManager(DataSourceConfig.getInstance()); + final var userService = new TxUserService(appUserService, transactionManager); final var newPassword = "newPassword"; final var createBy = "gugu"; From f21c08ec6f9bea5fc2fa8bacd374f412eb288a0d Mon Sep 17 00:00:00 2001 From: swonny Date: Sat, 14 Oct 2023 18:58:09 +0900 Subject: [PATCH 51/52] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=A7=A4=EB=8B=88=EC=97=90=EC=84=9C=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B,=20=EB=A1=A4=EB=B0=B1=20=EC=88=98=ED=96=89=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TransactionManager.java | 39 ++++++++++++++- .../TransactionSynchronizationManager.java | 48 ++----------------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/TransactionManager.java b/app/src/main/java/com/techcourse/service/TransactionManager.java index 9e451a6498..af826a0455 100644 --- a/app/src/main/java/com/techcourse/service/TransactionManager.java +++ b/app/src/main/java/com/techcourse/service/TransactionManager.java @@ -1,8 +1,12 @@ package com.techcourse.service; +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; +import java.sql.SQLException; public class TransactionManager { @@ -13,12 +17,45 @@ public TransactionManager(final DataSource dataSource) { } public T execute(final TransactionCallback callback) { - TransactionSynchronizationManager.startNewTransaction(dataSource); + final Connection connection = TransactionSynchronizationManager.startNewTransaction(dataSource); final T result = callback.execute(); + commitTransaction(connection); TransactionSynchronizationManager.finishTransaction(dataSource); return result; } + + public static void commitTransaction(final Connection connection) { + try { + connection.commit(); + + clear(connection); + } catch (final SQLException ex) { + rollback(connection); + + throw new DataAccessException("실행 중 예외가 발생했습니다."); + } + } + + public static void rollback(final Connection connection) { + try { + connection.rollback(); + + clear(connection); + } catch (final SQLException ex) { + throw new DataAccessException("트랜잭션 롤백 중 예외가 발생했습니다."); + } + } + + private static void clear(final Connection connection) { + try { + connection.setAutoCommit(true); + connection.close(); + DataSourceUtils.releaseConnection(connection); + } catch (final SQLException ex) { + throw new DataAccessException("커넥션 종료 중 예외가 발생했습니다."); + } + } } 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 f2e2db98e2..8758f95954 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,6 +1,5 @@ package org.springframework.transaction.support; -import org.springframework.dao.DataAccessException; import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; @@ -24,6 +23,11 @@ public static Connection getResource(final DataSource dataSource) throws SQLExce return resources.get().get(dataSource); } + public static Connection startNewTransaction(final DataSource dataSource) { + isActive.set(true); + return bindResource(dataSource); + } + private static Connection bindResource(final DataSource dataSource) { try { final Connection connection = DataSourceUtils.getConnection(dataSource); @@ -35,51 +39,9 @@ private static Connection bindResource(final DataSource dataSource) { } } - public static void startNewTransaction(final DataSource dataSource) { - // TODO: 2023/10/14 transaction이 active인 경우 기존 트랜잭션 참여 여부에 따라 새로 생성하거나 기존 트랜잭션에 참여하도록 하는 것도 필요할까? - isActive.set(true); - bindResource(dataSource); - } - - public static void commitTransaction(final DataSource dataSource) { - try { - final Connection connection = resources.get().get(dataSource); - connection.commit(); - } catch (final SQLException ex) { - rollback(dataSource); - - throw new DataAccessException("실행 중 예외가 발생했습니다."); - } - } - - public static void rollback(final DataSource dataSource) { - try { - if (!isActive.get()) { - final Connection connection = resources.get().get(dataSource); - connection.rollback(); - - clear(dataSource); - } - } catch (final SQLException ex) { - throw new DataAccessException("트랜잭션 롤백 중 예외가 발생했습니다."); - } - } - - private static void clear(final DataSource dataSource) { - try { - final Connection connection = resources.get().get(dataSource); - connection.setAutoCommit(true); - connection.close(); - DataSourceUtils.releaseConnection(connection); - } catch (final SQLException ex) { - throw new DataAccessException("실행 중 예외가 발생했습니다."); - } - } - public static void finishTransaction(final DataSource dataSource) { isActive.set(false); unbindResource(dataSource); - clear(dataSource); } public static Connection unbindResource(final DataSource dataSource) { From 88ffa927217325b0b40cb2e636e6aa5f51f19976 Mon Sep 17 00:00:00 2001 From: swonny Date: Sat, 14 Oct 2023 20:07:12 +0900 Subject: [PATCH 52/52] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20TransactionM?= =?UTF-8?q?anager=EC=97=90=EC=84=9C=20=EC=88=98=ED=96=89=ED=95=98=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 | 13 +++--- .../com/techcourse/dao/UserHistoryDao.java | 4 +- .../techcourse/service/AppUserService.java | 8 ++-- .../service/TransactionCallback.java | 4 +- .../service/TransactionManager.java | 14 ++++--- .../com/techcourse/service/UserService.java | 8 ++-- .../service/AppUserServiceTest.java | 6 ++- .../jdbc/core/JdbcTemplate.java | 42 +++++++------------ .../jdbc/datasource/DataSourceUtils.java | 3 +- .../TransactionSynchronizationManager.java | 7 ++-- 10 files changed, 57 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index dbaa46c96c..22f22d0415 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -6,6 +6,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import java.sql.SQLException; import java.util.List; public class UserDao { @@ -25,14 +26,14 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public int insert(final User user) { + public int insert(final User user) throws SQLException { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; log.debug("query : {}", sql); return updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - private int updateQuery(final String sql, final Object... objects) { + private int updateQuery(final String sql, final Object... objects) throws SQLException { final int updatedRows = jdbcTemplate.update(sql, objects); if (updatedRows < 1) { throw new RuntimeException("저장된 데이터가 없습니다."); @@ -40,14 +41,14 @@ private int updateQuery(final String sql, final Object... objects) { return updatedRows; } - public int update(final User user) { + public int update(final User user) throws SQLException { final var sql = "update users set (account, password, email) = (?, ?, ?)"; log.debug("query : {}", sql); return updateQuery(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - public List findAll() { + public List findAll() throws SQLException { final var sql = "select id, account, password, email from users"; final List users = jdbcTemplate.query(sql, userRowMapper); @@ -56,7 +57,7 @@ public List findAll() { return users; } - public User findById(final Long id) { + public User findById(final Long id) throws SQLException { final var sql = "select id, account, password, email from users where id = ?"; final User user = jdbcTemplate.queryForObject(sql, userRowMapper, id) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); @@ -66,7 +67,7 @@ public User findById(final Long id) { return user; } - public User findByAccount(final String account) { + public User findByAccount(final String account) throws SQLException { final var sql = "select id, account, password, email from users where account = ?"; final User user = jdbcTemplate.queryForObject(sql, userRowMapper, account) .orElseThrow(() -> new RuntimeException("찾는 사용자가 존재하지 않습니다.")); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index b67bc6b889..253528653b 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -5,6 +5,8 @@ import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; +import java.sql.SQLException; + public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); @@ -15,7 +17,7 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final UserHistory userHistory) { + public void log(final UserHistory userHistory) throws SQLException { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; jdbcTemplate.update( sql, diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java index 52e720a856..e865f4735c 100644 --- a/app/src/main/java/com/techcourse/service/AppUserService.java +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -5,6 +5,8 @@ import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import java.sql.SQLException; + public class AppUserService implements UserService { private final UserDao userDao; @@ -16,17 +18,17 @@ public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao } @Override - public User findById(final long id) { + public User findById(final long id) throws SQLException { return userDao.findById(id); } @Override - public void insert(final User user) { + public void insert(final User user) throws SQLException { userDao.insert(user); } @Override - public void changePassword(long id, final String newPassword, final String createBy) { + public void changePassword(long id, final String newPassword, final String createBy) throws SQLException { final var user = findById(id); user.changePassword(newPassword); userDao.update(user); diff --git a/app/src/main/java/com/techcourse/service/TransactionCallback.java b/app/src/main/java/com/techcourse/service/TransactionCallback.java index 25695e211f..c160dac30b 100644 --- a/app/src/main/java/com/techcourse/service/TransactionCallback.java +++ b/app/src/main/java/com/techcourse/service/TransactionCallback.java @@ -1,7 +1,9 @@ package com.techcourse.service; +import java.sql.SQLException; + @FunctionalInterface public interface TransactionCallback { - T execute(); + T execute() throws SQLException; } diff --git a/app/src/main/java/com/techcourse/service/TransactionManager.java b/app/src/main/java/com/techcourse/service/TransactionManager.java index af826a0455..9c28147150 100644 --- a/app/src/main/java/com/techcourse/service/TransactionManager.java +++ b/app/src/main/java/com/techcourse/service/TransactionManager.java @@ -17,14 +17,18 @@ public TransactionManager(final DataSource dataSource) { } public T execute(final TransactionCallback callback) { - final Connection connection = TransactionSynchronizationManager.startNewTransaction(dataSource); + try { + final Connection connection = TransactionSynchronizationManager.startNewTransaction(dataSource); - final T result = callback.execute(); + final T result = callback.execute(); - commitTransaction(connection); - TransactionSynchronizationManager.finishTransaction(dataSource); + commitTransaction(connection); + TransactionSynchronizationManager.finishTransaction(dataSource); - return result; + return result; + } catch (final SQLException ex) { + throw new RuntimeException("실행 중 예외가 발생했습니다."); + } } public static void commitTransaction(final 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 b14dbcacbf..7f2430e9ed 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -2,11 +2,13 @@ import com.techcourse.domain.User; +import java.sql.SQLException; + public interface UserService { - User findById(final long id); + User findById(final long id) throws SQLException; - void insert(final User user); + void insert(final User user) throws SQLException; - void changePassword(final long id, final String newPassword, final String createBy); + void changePassword(final long id, final String newPassword, final String createBy) throws SQLException; } diff --git a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java index 04e450099f..98cce20653 100644 --- a/app/src/test/java/com/techcourse/service/AppUserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -10,6 +10,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import javax.sql.DataSource; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; @@ -25,13 +26,14 @@ void setUp() throws SQLException { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); - DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + final DataSource dataSource = DataSourceConfig.getInstance(); + DatabasePopulatorUtils.execute(dataSource); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } @Test - void testChangePassword() { + void testChangePassword() throws SQLException { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); final var userService = new AppUserService(userDao, userHistoryDao); 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 ad831ce2b1..714a1402d6 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,7 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.dao.DataAccessException; import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; @@ -25,20 +25,20 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) { + public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... parameters) throws SQLException { return execute(sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { return Optional.of(rowMapper.mapRow(resultSet)); } if (!resultSet.isLast()) { - throw new RuntimeException("단일 데이터가 아닙니다."); + throw new DataAccessException("단일 데이터가 아닙니다."); } return Optional.empty(); }, parameters); } - public List query(final String sql, final RowMapper rowMapper) { + public List query(final String sql, final RowMapper rowMapper) throws SQLException { return execute(sql, preparedStatement -> { final ResultSet resultSet = preparedStatement.executeQuery(); final List objects = new ArrayList<>(); @@ -50,34 +50,24 @@ public List query(final String sql, final RowMapper rowMapper) { }); } - public int update(final String sql, final Object... parameters) { + public int update(final String sql, final Object... parameters) throws SQLException { return execute(sql, PreparedStatement::executeUpdate, parameters); } - public T execute(final String sql, final ExecuteQueryCallback callBack, final Object... objects) { - try { - final PreparedStatement preparedStatement = createPreparedStatement(sql); - setPreparedStatement(preparedStatement, objects); + public T execute( + final String sql, + final ExecuteQueryCallback callBack, + final Object... objects + ) throws SQLException { + final PreparedStatement preparedStatement = createPreparedStatement(sql); + setPreparedStatement(preparedStatement, objects); - final T result = callBack.execute(preparedStatement); - TransactionSynchronizationManager.commitTransaction(dataSource); - - return result; - } catch (final SQLException ex) { - throw new RuntimeException("실행 중 예외가 발생했습니다."); - } + return callBack.execute(preparedStatement); } - private PreparedStatement createPreparedStatement(final String sql) { - try { - final Connection connection = TransactionSynchronizationManager.getResource(dataSource); - return connection.prepareStatement(sql); - } catch (SQLException ex) { - TransactionSynchronizationManager.rollback(dataSource); - - log.error(ex.getMessage()); - throw new CannotGetJdbcConnectionException("jdbc 연결에 실패했습니다."); - } + private PreparedStatement createPreparedStatement(final String sql) throws SQLException { + final Connection connection = TransactionSynchronizationManager.getResource(dataSource); + return connection.prepareStatement(sql); } private void setPreparedStatement(final PreparedStatement preparedStatement, final Object[] parameters) throws SQLException { 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 825156bd9a..8d3d0c9a0e 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -1,7 +1,6 @@ package org.springframework.jdbc.datasource; import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; import java.sql.Connection; @@ -14,7 +13,7 @@ private DataSourceUtils() { public static Connection getConnection(final DataSource dataSource) throws CannotGetJdbcConnectionException, SQLException { try { - return TransactionSynchronizationManager.getResource(dataSource); + return dataSource.getConnection(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); } 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 8758f95954..6be3352e5f 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -16,9 +16,10 @@ public abstract class TransactionSynchronizationManager { private TransactionSynchronizationManager() { } - public static Connection getResource(final DataSource dataSource) throws SQLException { - if (!isActive.get() || !resources.get().containsKey(dataSource)) { - throw new RuntimeException("시작한 트랜잭션이 없습니다."); + public static Connection getResource(final DataSource dataSource) { + if (!resources.get().containsKey(dataSource)) { + final Connection connection = startNewTransaction(dataSource); + resources.get().put(dataSource, connection); } return resources.get().get(dataSource); }