From dc70d1ddf0778aa077348d8a98207876ece28523 Mon Sep 17 00:00:00 2001 From: kang-hyungu Date: Thu, 21 Sep 2023 14:11:58 +0900 Subject: [PATCH 01/15] =?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/15] =?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 f8f2c3b6e8991e7244657a379d8fa4daac26d4bb Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Sat, 30 Sep 2023 14:46:18 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=201=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=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 | 103 ++++-------------- .../java/com/techcourse/dao/UserDaoTest.java | 13 ++- .../jdbc/core/JdbcTemplate.java | 55 ++++++++++ .../springframework/jdbc/core/RowMapper.java | 60 ++++++++++ .../java/aop/config/DataSourceConfig.java | 1 + study/src/main/resources/application.yml | 1 + 6 files changed, 150 insertions(+), 83 deletions(-) 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 d14c545f34..b27e0f6c74 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,6 +1,8 @@ package com.techcourse.dao; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; +import java.util.ArrayList; import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,111 +13,50 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import org.springframework.jdbc.core.RowMapper; public class UserDao { private static final Logger log = LoggerFactory.getLogger(UserDao.class); - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; - public UserDao(final DataSource dataSource) { - this.dataSource = dataSource; - } public UserDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); } 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) {} - } + jdbcTemplate.execute(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(final User user) { - // todo + final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; + jdbcTemplate.execute(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } public List findAll() { - // todo - return null; + final var sql = "select id, account, password, email from users"; + return jdbcTemplate.query(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email"))); } 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) {} - } + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")), id); } public User findByAccount(final String account) { - // todo - return null; + final var sql = "select id, account, password, email from users where account = ?"; + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")), account); } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 773d7faf82..f692842434 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -3,24 +3,32 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import org.junit.jupiter.api.AfterEach; 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; class UserDaoTest { private UserDao userDao; + private JdbcTemplate jdbcTemplate; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - - userDao = new UserDao(DataSourceConfig.getInstance()); + jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + userDao = new UserDao(jdbcTemplate); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } + @AfterEach + void tearDown() { + jdbcTemplate.execute("truncate table users restart identity"); + } + @Test void findAll() { final var users = userDao.findAll(); @@ -66,4 +74,5 @@ void update() { assertThat(actual.getPassword()).isEqualTo(newPassword); } + } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 52a0d30a17..4da1f3afc9 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,5 +1,11 @@ package org.springframework.jdbc.core; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,4 +20,53 @@ public class JdbcTemplate { public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } + + public void execute(String sql, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement pstmt = conn.prepareStatement(sql)) { + log.debug("query : {}", sql); + + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + + pstmt.executeUpdate(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + public T queryForObject(String sql, RowMapper rowMapper, Object... params) { + List results = query(sql, rowMapper, params); + if (results.size() > 1) { + throw new RuntimeException("too many result. expected 1 but was " + results.size()); + } + if (results.isEmpty()) { + throw new RuntimeException("no result"); + } + return results.get(0); + } + + public List query(String sql, RowMapper rowMapper, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement pstmt = conn.prepareStatement(sql)) { + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + ResultSet rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + final List results = new ArrayList<>(); + while (rs.next()) { + results.add(rowMapper.mapRow(rs, rs.getRow())); + } + return results; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + } 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..7fee249381 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for mapping rows of a + * {@link ResultSet} on a per-row basis. Implementations of this + * interface perform the actual work of mapping each row to a result object, + * but don't need to worry about exception handling. + * {@link SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

Typically used either for {@link JdbcTemplate}'s query methods + * or for out parameters of stored procedures. RowMapper objects are + * typically stateless and thus reusable; they are an ideal choice for + * implementing row-mapping logic in a single place. + * + *

Alternatively, consider subclassing + * {@code jdbc.object} package: Instead of working with separate + * JdbcTemplate and RowMapper objects, you can build executable query + * objects (containing row-mapping logic) in that style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @param the result type + * @see JdbcTemplate + */ +@FunctionalInterface +public interface RowMapper { + + /** + * Implementations must implement this method to map each row of data + * in the ResultSet. This method should not call {@code next()} on + * the ResultSet; it is only supposed to map values of the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param rowNum the number of the current row + * @return the result object for the current row (maybe {@code null}) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + T mapRow(ResultSet rs, int rowNum) throws SQLException; + +} 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 5b178145ce8241ddf767615cfa9ebf0ff7bf76a2 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Thu, 5 Oct 2023 01:10:06 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=202=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=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 | 103 ++++++++++++++---- .../java/com/techcourse/dao/UserDaoTest.java | 13 +-- .../jdbc/core/JdbcTemplate.java | 55 ---------- .../springframework/jdbc/core/RowMapper.java | 60 ---------- 4 files changed, 83 insertions(+), 148 deletions(-) delete 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 b27e0f6c74..d14c545f34 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,8 +1,6 @@ package com.techcourse.dao; -import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; -import java.util.ArrayList; import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,50 +11,111 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import org.springframework.jdbc.core.RowMapper; public class UserDao { private static final Logger log = LoggerFactory.getLogger(UserDao.class); - private final JdbcTemplate jdbcTemplate; + private final DataSource dataSource; + public UserDao(final DataSource dataSource) { + this.dataSource = dataSource; + } public UserDao(final JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + this.dataSource = null; } public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - jdbcTemplate.execute(sql, user.getAccount(), user.getPassword(), user.getEmail()); + + 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 void update(final User user) { - final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; - jdbcTemplate.execute(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); + // todo } public List findAll() { - final var sql = "select id, account, password, email from users"; - return jdbcTemplate.query(sql, (rs, rowNum) -> new User(rs.getLong("id"), - rs.getString("account"), - rs.getString("password"), - rs.getString("email"))); + // todo + return null; } public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), - rs.getString("account"), - rs.getString("password"), - rs.getString("email")), 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) {} + } } public User findByAccount(final String account) { - final var sql = "select id, account, password, email from users where account = ?"; - return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), - rs.getString("account"), - rs.getString("password"), - rs.getString("email")), account); + // todo + return null; } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index f692842434..773d7faf82 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -3,32 +3,24 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import org.junit.jupiter.api.AfterEach; 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; class UserDaoTest { private UserDao userDao; - private JdbcTemplate jdbcTemplate; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); - userDao = new UserDao(jdbcTemplate); + + userDao = new UserDao(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } - @AfterEach - void tearDown() { - jdbcTemplate.execute("truncate table users restart identity"); - } - @Test void findAll() { final var users = userDao.findAll(); @@ -74,5 +66,4 @@ void update() { assertThat(actual.getPassword()).isEqualTo(newPassword); } - } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 4da1f3afc9..52a0d30a17 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,11 +1,5 @@ package org.springframework.jdbc.core; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,53 +14,4 @@ public class JdbcTemplate { public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - - public void execute(String sql, Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement pstmt = conn.prepareStatement(sql)) { - log.debug("query : {}", sql); - - for (int i = 0; i < parameters.length; i++) { - pstmt.setObject(i + 1, parameters[i]); - } - - pstmt.executeUpdate(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } - } - - public T queryForObject(String sql, RowMapper rowMapper, Object... params) { - List results = query(sql, rowMapper, params); - if (results.size() > 1) { - throw new RuntimeException("too many result. expected 1 but was " + results.size()); - } - if (results.isEmpty()) { - throw new RuntimeException("no result"); - } - return results.get(0); - } - - public List query(String sql, RowMapper rowMapper, Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement pstmt = conn.prepareStatement(sql)) { - for (int i = 0; i < parameters.length; i++) { - pstmt.setObject(i + 1, parameters[i]); - } - ResultSet rs = pstmt.executeQuery(); - - log.debug("query : {}", sql); - - final List results = new ArrayList<>(); - while (rs.next()) { - results.add(rowMapper.mapRow(rs, rs.getRow())); - } - return results; - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } - } - } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java deleted file mode 100644 index 7fee249381..0000000000 --- a/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.jdbc.core; - -import java.sql.ResultSet; -import java.sql.SQLException; - -/** - * An interface used by {@link JdbcTemplate} for mapping rows of a - * {@link ResultSet} on a per-row basis. Implementations of this - * interface perform the actual work of mapping each row to a result object, - * but don't need to worry about exception handling. - * {@link SQLException SQLExceptions} will be caught and handled - * by the calling JdbcTemplate. - * - *

Typically used either for {@link JdbcTemplate}'s query methods - * or for out parameters of stored procedures. RowMapper objects are - * typically stateless and thus reusable; they are an ideal choice for - * implementing row-mapping logic in a single place. - * - *

Alternatively, consider subclassing - * {@code jdbc.object} package: Instead of working with separate - * JdbcTemplate and RowMapper objects, you can build executable query - * objects (containing row-mapping logic) in that style. - * - * @author Thomas Risberg - * @author Juergen Hoeller - * @param the result type - * @see JdbcTemplate - */ -@FunctionalInterface -public interface RowMapper { - - /** - * Implementations must implement this method to map each row of data - * in the ResultSet. This method should not call {@code next()} on - * the ResultSet; it is only supposed to map values of the current row. - * @param rs the ResultSet to map (pre-initialized for the current row) - * @param rowNum the number of the current row - * @return the result object for the current row (maybe {@code null}) - * @throws SQLException if an SQLException is encountered getting - * column values (that is, there's no need to catch SQLException) - */ - T mapRow(ResultSet rs, int rowNum) throws SQLException; - -} From b74a465ea952e75d700f6f8e0fba05b42fb9e914 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Thu, 5 Oct 2023 01:18:22 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=202=EB=8B=A8=EA=B3=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=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 | 113 ++++-------------- .../com/techcourse/dao/UserHistoryDao.java | 52 +------- .../java/com/techcourse/dao/UserDaoTest.java | 13 +- .../techcourse/dao/UserHistoryDaoTest.java | 55 +++++++++ .../jdbc/core/JdbcTemplate.java | 65 ++++++++++ .../springframework/jdbc/core/RowMapper.java | 60 ++++++++++ .../java/nextstep/jdbc/JdbcTemplateTest.java | 100 ++++++++++++++++ 7 files changed, 317 insertions(+), 141 deletions(-) create mode 100644 app/src/test/java/com/techcourse/dao/UserHistoryDaoTest.java 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 d14c545f34..b5c6b06907 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,121 +1,50 @@ package com.techcourse.dao; import com.techcourse.domain.User; -import org.springframework.jdbc.core.JdbcTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.List; +import org.springframework.jdbc.core.JdbcTemplate; public class UserDao { - private static final Logger log = LoggerFactory.getLogger(UserDao.class); - - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; - public UserDao(final DataSource dataSource) { - this.dataSource = dataSource; - } public UserDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.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) {} - } + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(final User user) { - // todo + final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; + jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } public List findAll() { - // todo - return null; + final var sql = "select id, account, password, email from users"; + return jdbcTemplate.query(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email"))); } 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) {} - } + return jdbcTemplate.queryForObject(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email")), id); } public User findByAccount(final String account) { - // todo - return null; + final var sql = "select id, account, password, email from users where account = ?"; + return jdbcTemplate.queryForObject(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email")), account); } + } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..6c7fa4f339 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -2,61 +2,19 @@ import com.techcourse.domain.UserHistory; import org.springframework.jdbc.core.JdbcTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; public class UserHistoryDao { - private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); - - private final DataSource dataSource; - - public UserHistoryDao(final DataSource dataSource) { - this.dataSource = dataSource; - } + private final JdbcTemplate jdbcTemplate; public UserHistoryDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.jdbcTemplate = jdbcTemplate; } public void log(final UserHistory userHistory) { 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()); } + } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 773d7faf82..5729f42fc4 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -3,24 +3,32 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import org.junit.jupiter.api.AfterEach; 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; class UserDaoTest { private UserDao userDao; + private JdbcTemplate jdbcTemplate; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - - userDao = new UserDao(DataSourceConfig.getInstance()); + jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + userDao = new UserDao(jdbcTemplate); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } + @AfterEach + void tearDown() { + jdbcTemplate.update("truncate table users restart identity"); + } + @Test void findAll() { final var users = userDao.findAll(); @@ -66,4 +74,5 @@ void update() { assertThat(actual.getPassword()).isEqualTo(newPassword); } + } diff --git a/app/src/test/java/com/techcourse/dao/UserHistoryDaoTest.java b/app/src/test/java/com/techcourse/dao/UserHistoryDaoTest.java new file mode 100644 index 0000000000..60e95b64e9 --- /dev/null +++ b/app/src/test/java/com/techcourse/dao/UserHistoryDaoTest.java @@ -0,0 +1,55 @@ +package com.techcourse.dao; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; +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; + +class UserHistoryDaoTest { + + private UserDao userDao; + private UserHistoryDao userHistoryDao; + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); + jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + userDao = new UserDao(jdbcTemplate); + userHistoryDao = new UserHistoryDao(jdbcTemplate); + } + + @Test + void log() { + //given + final String account = "gugu"; + userDao.insert(new User(account, "password", "gugu@woowahan.com")); + final User gugu = userDao.findByAccount(account); + final UserHistory expected = new UserHistory(gugu, "rosie"); + + //when + userHistoryDao.log(expected); + + //then + final UserHistory actual = jdbcTemplate.queryForObject("select * from user_history where id = ?", + (resultSet, rowNum) -> + new UserHistory( + resultSet.getLong("id"), + resultSet.getLong("user_id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email"), + resultSet.getString("created_by") + ), gugu.getId()); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expected); + } +} 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..5d801fdfa0 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,9 +1,16 @@ package org.springframework.jdbc.core; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; public class JdbcTemplate { @@ -14,4 +21,62 @@ public class JdbcTemplate { public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } + + public T queryForObject(String sql, RowMapper rowMapper, Object... params) { + List results = query(sql, rowMapper, params); + if (results.size() > 1) { + throw new DataAccessException("too many result. expected 1 but was " + results.size()); + } + if (results.isEmpty()) { + throw new DataAccessException("no result"); + } + return results.get(0); + } + + public List query(String sql, RowMapper rowMapper, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement preparedStatement = conn.prepareStatement(sql); + final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { + log.debug("query : {}", sql); + return mapResults(rowMapper, resultSet); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e); + } + } + + private ResultSet executeQuery(final PreparedStatement preparedStatement, final Object[] parameters) + throws SQLException { + setParameters(preparedStatement, parameters); + return preparedStatement.executeQuery(); + } + + private void setParameters(final PreparedStatement preparedStatement, final Object[] parameters) + throws SQLException { + for (int i = 0; i < parameters.length; i++) { + preparedStatement.setObject(i + 1, parameters[i]); + } + } + + private List mapResults(final RowMapper rowMapper, final ResultSet resultSet) throws SQLException { + final List results = new ArrayList<>(); + while (resultSet.next()) { + results.add(rowMapper.mapRow(resultSet, resultSet.getRow())); + } + return results; + } + + public void update(String sql, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + log.debug("query : {}", sql); + + setParameters(preparedStatement, parameters); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e); + } + } + } 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..24ab2eea22 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for mapping rows of a + * {@link ResultSet} on a per-row basis. Implementations of this + * interface perform the actual work of mapping each row to a result object, + * but don't need to worry about exception handling. + * {@link SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

Typically used either for {@link JdbcTemplate}'s query methods + * or for out parameters of stored procedures. RowMapper objects are + * typically stateless and thus reusable; they are an ideal choice for + * implementing row-mapping logic in a single place. + * + *

Alternatively, consider subclassing + * {@code jdbc.object} package: Instead of working with separate + * JdbcTemplate and RowMapper objects, you can build executable query + * objects (containing row-mapping logic) in that style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @param the result type + * @see JdbcTemplate + */ +@FunctionalInterface +public interface RowMapper { + + /** + * Implementations must implement this method to map each row of data + * in the ResultSet. This method should not call {@code next()} on + * the ResultSet; it is only supposed to map values of the current row. + * @param resultSet the ResultSet to map (pre-initialized for the current row) + * @param rowNum the number of the current row + * @return the result object for the current row (maybe {@code null}) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + T mapRow(ResultSet resultSet, int rowNum) throws SQLException; + +} diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index d777c8becf..f96bb10211 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -1,5 +1,105 @@ package nextstep.jdbc; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class JdbcTemplateTest { + private JdbcTemplate jdbcTemplate; + private Connection mockConnection = mock(); + private DataSource mockDataSource = mock(); + private PreparedStatement mockPreparedStatement = mock(); + private ResultSet mockResultSet = mock(); + + @BeforeEach + void setUp() throws SQLException { + jdbcTemplate = new JdbcTemplate(mockDataSource); + when(mockDataSource.getConnection()).thenReturn(mockConnection); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + } + + @Nested + class QueryForObject { + + @Test + void throwExceptionWhenResultSetIsEmpty() throws SQLException { + //given + String sql = "select * from users where id = ?"; + Object[] params = {1L}; + + //when + when(mockResultSet.next()).thenReturn(false); + + //expect + assertThatThrownBy( + () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) + .isInstanceOf(DataAccessException.class); + } + + @Test + void throwExceptionWhenResultSetHasSizeMoreThanOne() throws SQLException { + //given + String sql = "select * from users where id = ?"; + Object[] params = {1L}; + + //when + when(mockResultSet.next()).thenReturn(true, true, false); + + //then + assertThatThrownBy( + () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) + .isInstanceOf(DataAccessException.class); + } + + @Test + void testCloseEveryClosable() throws Exception { + //given + String sql = "select * from users where id = ?"; + Object[] params = {1L}; + + //when + when(mockResultSet.next()).thenReturn(true, false); + jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params); + + //then + verify(mockResultSet).close(); + verify(mockPreparedStatement).close(); + verify(mockConnection).close(); + } + + } + + @Nested + class Update { + + @Test + void testCloseEveryClosable() throws Exception { + //given + String sql = "update users set name = ? where id = ?"; + Object[] params = {"test", 1L}; + + //when + jdbcTemplate.update(sql, params); + + //then + verify(mockPreparedStatement).close(); + verify(mockConnection).close(); + } + } + } From c37cf5b9e537662145f322bda2dc6f567acb7a8d Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 6 Oct 2023 11:03:59 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20UserDao=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=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 --- .../main/java/com/techcourse/dao/UserDao.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index b5c6b06907..3d4407639c 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,12 +1,17 @@ package com.techcourse.dao; import com.techcourse.domain.User; + +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; public class UserDao { private final JdbcTemplate jdbcTemplate; + private final RowMapper userMapper = (resultSet, rowNum) -> mapUser(resultSet); public UserDao(final JdbcTemplate jdbcTemplate) { @@ -25,26 +30,24 @@ public void update(final User user) { public List findAll() { final var sql = "select id, account, password, email from users"; - return jdbcTemplate.query(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), - resultSet.getString("account"), - resultSet.getString("password"), - resultSet.getString("email"))); + return jdbcTemplate.query(sql, userMapper); } public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.queryForObject(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), - resultSet.getString("account"), - resultSet.getString("password"), - resultSet.getString("email")), id); + return jdbcTemplate.queryForObject(sql, userMapper, id); } public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - return jdbcTemplate.queryForObject(sql, (resultSet, rowNum) -> new User(resultSet.getLong("id"), + return jdbcTemplate.queryForObject(sql, userMapper, account); + } + + private User mapUser(ResultSet resultSet) throws SQLException { + return new User(resultSet.getLong("id"), resultSet.getString("account"), resultSet.getString("password"), - resultSet.getString("email")), account); + resultSet.getString("email")); } } From 159df88718498eb36526482022c099bd04f4decc Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 6 Oct 2023 11:17:07 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20JdbcTemplate=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=BD=94=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 --- .../main/java/com/techcourse/dao/UserDao.java | 2 +- .../jdbc/core/JdbcTemplate.java | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 3d4407639c..35ee319895 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 void update(final User user) { public List findAll() { final var sql = "select id, account, password, email from users"; - return jdbcTemplate.query(sql, userMapper); + return jdbcTemplate.queryForObjects(sql, userMapper); } 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 5d801fdfa0..67f8421b61 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -23,26 +23,45 @@ public JdbcTemplate(final DataSource dataSource) { } public T queryForObject(String sql, RowMapper rowMapper, Object... params) { - List results = query(sql, rowMapper, params); + List results; + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement preparedStatement = conn.prepareStatement(sql); + final ResultSet resultSet = executeQuery(preparedStatement, params)) { + log.debug("query : {}", sql); + results = mapResults(rowMapper, resultSet); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e); + } + if (results.size() > 1) { throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { throw new DataAccessException("no result"); } + return results.get(0); } - public List query(String sql, RowMapper rowMapper, Object... parameters) { + public List queryForObjects(String sql, RowMapper rowMapper, Object... parameters) { + List results; + try (final Connection conn = dataSource.getConnection(); final PreparedStatement preparedStatement = conn.prepareStatement(sql); final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { log.debug("query : {}", sql); - return mapResults(rowMapper, resultSet); + results = mapResults(rowMapper, resultSet); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); } + + if (results.isEmpty()) { + throw new DataAccessException("no result"); + } + + return results; } private ResultSet executeQuery(final PreparedStatement preparedStatement, final Object[] parameters) From 15da493117dbe4d71013ec291d386ef95da706aa Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 6 Oct 2023 11:22:06 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springframework/dao/SizeException.java | 26 +++++++++++++++++++ .../jdbc/core/JdbcTemplate.java | 5 ++-- .../java/nextstep/jdbc/JdbcTemplateTest.java | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/dao/SizeException.java diff --git a/jdbc/src/main/java/org/springframework/dao/SizeException.java b/jdbc/src/main/java/org/springframework/dao/SizeException.java new file mode 100644 index 0000000000..f5e8cfa4cd --- /dev/null +++ b/jdbc/src/main/java/org/springframework/dao/SizeException.java @@ -0,0 +1,26 @@ +package org.springframework.dao; + +public class SizeException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SizeException() { + super(); + } + + public SizeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public SizeException(String message, Throwable cause) { + super(message, cause); + } + + public SizeException(String message) { + super(message); + } + + public SizeException(Throwable cause) { + super(cause); + } +} 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 67f8421b61..746f02dae2 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,6 +11,7 @@ import javax.sql.DataSource; import org.springframework.dao.DataAccessException; +import org.springframework.dao.SizeException; public class JdbcTemplate { @@ -38,7 +39,7 @@ public T queryForObject(String sql, RowMapper rowMapper, Object... params throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { - throw new DataAccessException("no result"); + throw new SizeException("no result"); } return results.get(0); @@ -58,7 +59,7 @@ public List queryForObjects(String sql, RowMapper rowMapper, Object... } if (results.isEmpty()) { - throw new DataAccessException("no result"); + throw new SizeException("no result"); } return results; diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index f96bb10211..65fd86c3dd 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; +import org.springframework.dao.SizeException; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -48,7 +49,7 @@ void throwExceptionWhenResultSetIsEmpty() throws SQLException { //expect assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(DataAccessException.class); + .isInstanceOf(SizeException.class); } @Test From 034f104dbb7673a3f3e721bdd6f7760d4997d5ad Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Sun, 8 Oct 2023 12:05:44 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20step3=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 | 29 +++++----- .../com/techcourse/dao/UserHistoryDao.java | 7 +++ .../com/techcourse/service/UserService.java | 25 ++++++-- .../service/MockUserHistoryDao.java | 4 +- .../techcourse/service/UserServiceTest.java | 23 +++++--- .../jdbc/core/JdbcTemplate.java | 57 ++++++++++++------- .../java/nextstep/jdbc/JdbcTemplateTest.java | 2 +- 7 files changed, 96 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 35ee319895..568c1abf00 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,17 +1,20 @@ package com.techcourse.dao; import com.techcourse.domain.User; - -import java.sql.ResultSet; -import java.sql.SQLException; +import java.sql.Connection; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; public class UserDao { + public static final RowMapper USER_ROW_MAPPER = (resultSet, rowNum) -> new User(resultSet.getLong("id"), + resultSet.getString("account"), + resultSet.getString("password"), + resultSet.getString("email")); + + private final JdbcTemplate jdbcTemplate; - private final RowMapper userMapper = (resultSet, rowNum) -> mapUser(resultSet); public UserDao(final JdbcTemplate jdbcTemplate) { @@ -28,26 +31,24 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } + public void update(final Connection connection, final User user) { + final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; + jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); + } + public List findAll() { final var sql = "select id, account, password, email from users"; - return jdbcTemplate.queryForObjects(sql, userMapper); + return jdbcTemplate.query(sql, USER_ROW_MAPPER); } public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.queryForObject(sql, userMapper, id); + return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id); } public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - return jdbcTemplate.queryForObject(sql, userMapper, account); - } - - private User mapUser(ResultSet resultSet) throws SQLException { - return new User(resultSet.getLong("id"), - resultSet.getString("account"), - resultSet.getString("password"), - resultSet.getString("email")); + return jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, account); } } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 6c7fa4f339..9554b1adf9 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,6 +1,7 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; +import java.sql.Connection; import org.springframework.jdbc.core.JdbcTemplate; public class UserHistoryDao { @@ -11,6 +12,12 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + public void log(final Connection connection, 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(), userHistory.getPassword(), + userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); + } + 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(), diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fcf2159dc8..d8782ba089 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,9 +1,12 @@ package com.techcourse.service; +import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import java.sql.SQLException; +import org.springframework.dao.DataAccessException; public class UserService { @@ -23,10 +26,22 @@ public void insert(final User user) { userDao.insert(user); } - public void changePassword(final long id, final String newPassword, final String createBy) { - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); + public void changePassword(final long id, final String newPassword, final String createBy) throws SQLException { + final var connection = DataSourceConfig.getInstance().getConnection(); + try { + 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 (final Exception e) { + connection.rollback(); + throw new DataAccessException(e); + } finally { + connection.close(); + } } + } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..789dced668 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,6 +2,7 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; +import java.sql.Connection; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -12,7 +13,8 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final UserHistory userHistory) { + public void log(final Connection connection, final UserHistory userHistory) { + System.out.println("로그뜰까요아닐까용오오오"); throw new DataAccessException(); } } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..957478d82d 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,13 +5,16 @@ 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.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; @Disabled @@ -31,7 +34,7 @@ void setUp() { } @Test - void testChangePassword() { + void testChangePassword() throws SQLException { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); final var userService = new UserService(userDao, userHistoryDao); @@ -47,17 +50,19 @@ void testChangePassword() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. + final MockUserHistoryDao mockUserHistoryDao = new MockUserHistoryDao(jdbcTemplate); + final var userService = new UserService(userDao, mockUserHistoryDao); final var newPassword = "newPassword"; final var createBy = "gugu"; - // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); - assertThat(actual.getPassword()).isNotEqualTo(newPassword); + assertAll( + () -> assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)), + () -> assertThat(actual.getPassword()).isNotEqualTo(newPassword) + ); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 746f02dae2..0a5cc69419 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -23,46 +23,49 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public T queryForObject(String sql, RowMapper rowMapper, Object... params) { - List results; - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql); - final ResultSet resultSet = executeQuery(preparedStatement, params)) { - log.debug("query : {}", sql); - results = mapResults(rowMapper, resultSet); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new DataAccessException(e); + public T queryForObject(final String sql, final RowMapper rowMapper, final Object... params) { + List results = query(sql, rowMapper, params); + if (results.size() > 1) { + throw new SizeException("too many result. expected 1 but was " + results.size()); } + if (results.isEmpty()) { + throw new SizeException("no result"); + } + return results.get(0); + } + public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... params) { + List results = query(connection, sql, rowMapper, params); if (results.size() > 1) { - throw new DataAccessException("too many result. expected 1 but was " + results.size()); + throw new SizeException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { throw new SizeException("no result"); } - return results.get(0); } - public List queryForObjects(String sql, RowMapper rowMapper, Object... parameters) { - List results; - + public List query(final String sql, final RowMapper rowMapper, final Object... parameters) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement preparedStatement = conn.prepareStatement(sql); final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { log.debug("query : {}", sql); - results = mapResults(rowMapper, resultSet); + return mapResults(rowMapper, resultSet); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); } + } - if (results.isEmpty()) { - throw new SizeException("no result"); + public List query(final Connection conn, final String sql, final RowMapper rowMapper, final Object... parameters) { + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql); + final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { + log.debug("query : {}", sql); + return mapResults(rowMapper, resultSet); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e); } - - return results; } private ResultSet executeQuery(final PreparedStatement preparedStatement, final Object[] parameters) @@ -86,7 +89,7 @@ private List mapResults(final RowMapper rowMapper, final ResultSet res return results; } - public void update(String sql, Object... parameters) { + public void update(final String sql, final Object... parameters) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { log.debug("query : {}", sql); @@ -99,4 +102,16 @@ public void update(String sql, Object... parameters) { } } + public void update(final Connection conn, final String sql, final Object... parameters) { + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + log.debug("query : {}", sql); + + setParameters(preparedStatement, parameters); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new DataAccessException(e); + } + } + } diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 65fd86c3dd..3e7fa0c702 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -64,7 +64,7 @@ void throwExceptionWhenResultSetHasSizeMoreThanOne() throws SQLException { //then assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(DataAccessException.class); + .isInstanceOf(SizeException.class); } @Test From 4669db85f666a27e3e622c23917676a2e174bd65 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Tue, 10 Oct 2023 04:03:41 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20step4=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 | 5 -- .../com/techcourse/dao/UserHistoryDao.java | 6 --- .../techcourse/service/AppUserService.java | 36 ++++++++++++++ .../com/techcourse/service/TxUserService.java | 47 +++++++++++++++++++ .../com/techcourse/service/UserService.java | 45 ++---------------- .../service/AppUserServiceTest.java | 43 +++++++++++++++++ .../service/MockUserHistoryDao.java | 5 +- ...erviceTest.java => TxUserServiceTest.java} | 41 +++++----------- .../springframework/dao/SizeException.java | 26 ---------- .../jdbc/core/JdbcTemplate.java | 22 +++++---- .../jdbc/datasource/DataSourceUtils.java | 1 + .../TransactionSynchronizationManager.java | 26 ++++++++-- .../java/nextstep/jdbc/JdbcTemplateTest.java | 5 +- 13 files changed, 181 insertions(+), 127 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 create mode 100644 app/src/test/java/com/techcourse/service/AppUserServiceTest.java rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => TxUserServiceTest.java} (54%) delete mode 100644 jdbc/src/main/java/org/springframework/dao/SizeException.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 568c1abf00..575b425af7 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -31,11 +31,6 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } - public void update(final Connection connection, final User user) { - final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; - jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); - } - public List findAll() { final var sql = "select id, account, password, email from users"; return jdbcTemplate.query(sql, USER_ROW_MAPPER); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 9554b1adf9..38600a75c1 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -12,12 +12,6 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection connection, 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(), userHistory.getPassword(), - userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); - } - 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(), 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..3bac27f084 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,36 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } + +} 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..2feff7282f --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,47 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import java.sql.SQLException; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +public class TxUserService implements UserService { + + private final UserService userService; + + public TxUserService(final UserService userService) { + 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 dataSource = DataSourceConfig.getInstance(); + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + userService.changePassword(id, newPassword, createBy); + + connection.commit(); + } catch (final Exception e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index d8782ba089..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,47 +1,10 @@ package com.techcourse.service; -import com.techcourse.config.DataSourceConfig; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import java.sql.SQLException; -import org.springframework.dao.DataAccessException; -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; - } - - public User findById(final long id) { - return userDao.findById(id); - } - - public void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) throws SQLException { - final var connection = DataSourceConfig.getInstance().getConnection(); - try { - 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 (final Exception e) { - connection.rollback(); - throw new DataAccessException(e); - } finally { - connection.close(); - } - } +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/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java new file mode 100644 index 0000000000..29c1f89a33 --- /dev/null +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -0,0 +1,43 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import java.sql.SQLException; +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; + +class AppUserServiceTest { + + private JdbcTemplate jdbcTemplate; + private UserDao userDao; + + @BeforeEach + void setUp() { + 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); + } + + @Test + void testChangePassword() throws SQLException { + final var userHistoryDao = new UserHistoryDao(jdbcTemplate); + final var userService = new AppUserService(userDao, userHistoryDao); + + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + + final var actual = userService.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 789dced668..c720ab0d58 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,7 +2,6 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import java.sql.Connection; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -13,8 +12,8 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final Connection connection, final UserHistory userHistory) { - System.out.println("로그뜰까요아닐까용오오오"); + public void log(final UserHistory userHistory) { throw new DataAccessException(); } + } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java similarity index 54% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/TxUserServiceTest.java index 957478d82d..df13c9c0d5 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -2,23 +2,17 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import 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.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; -@Disabled -class UserServiceTest { +class TxUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @@ -32,37 +26,24 @@ void setUp() { final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } - - @Test - void testChangePassword() throws SQLException { - final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); - - 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() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - final MockUserHistoryDao mockUserHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, mockUserHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; + // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); - assertAll( - () -> assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)), - () -> assertThat(actual.getPassword()).isNotEqualTo(newPassword) - ); + assertThat(actual.getPassword()).isNotEqualTo(newPassword); } + } diff --git a/jdbc/src/main/java/org/springframework/dao/SizeException.java b/jdbc/src/main/java/org/springframework/dao/SizeException.java deleted file mode 100644 index f5e8cfa4cd..0000000000 --- a/jdbc/src/main/java/org/springframework/dao/SizeException.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.springframework.dao; - -public class SizeException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public SizeException() { - super(); - } - - public SizeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - public SizeException(String message, Throwable cause) { - super(message, cause); - } - - public SizeException(String message) { - super(message); - } - - public SizeException(Throwable cause) { - super(cause); - } -} 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 0a5cc69419..864b266f0c 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,7 +11,7 @@ import javax.sql.DataSource; import org.springframework.dao.DataAccessException; -import org.springframework.dao.SizeException; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -26,10 +26,10 @@ public JdbcTemplate(final DataSource dataSource) { public T queryForObject(final String sql, final RowMapper rowMapper, final Object... params) { List results = query(sql, rowMapper, params); if (results.size() > 1) { - throw new SizeException("too many result. expected 1 but was " + results.size()); + throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { - throw new SizeException("no result"); + throw new DataAccessException("no result"); } return results.get(0); } @@ -37,17 +37,17 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... params) { List results = query(connection, sql, rowMapper, params); if (results.size() > 1) { - throw new SizeException("too many result. expected 1 but was " + results.size()); + throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { - throw new SizeException("no result"); + throw new DataAccessException("no result"); } return results.get(0); } public List query(final String sql, final RowMapper rowMapper, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql); + final var conn = getConnection(); + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql); final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { log.debug("query : {}", sql); return mapResults(rowMapper, resultSet); @@ -90,8 +90,8 @@ private List mapResults(final RowMapper rowMapper, final ResultSet res } public void update(final String sql, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + final Connection conn = getConnection(); + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { log.debug("query : {}", sql); setParameters(preparedStatement, parameters); @@ -114,4 +114,8 @@ public void update(final Connection conn, final String sql, final Object... para } } + private Connection getConnection() { + return DataSourceUtils.getConnection(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 3c40bfec52..be2f114ac1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -29,6 +29,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { + TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..4f1199b94b 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,23 +1,41 @@ package org.springframework.transaction.support; -import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; +import javax.sql.DataSource; public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = new ThreadLocal<>(); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + final Map dataSourceConnectionMap = resources.get(); + + if (dataSourceConnectionMap == null) { + return null; + } + return dataSourceConnectionMap.get(key); } public static void bindResource(DataSource key, Connection value) { + if (resources.get() == null) { + resources.set(new HashMap<>()); + } + + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + final Map dataSourceConnectionMap = resources.get(); + + if (dataSourceConnectionMap == null) { + return null; + } + return dataSourceConnectionMap.remove(key); } + } diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 3e7fa0c702..f96bb10211 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; -import org.springframework.dao.SizeException; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -49,7 +48,7 @@ void throwExceptionWhenResultSetIsEmpty() throws SQLException { //expect assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(SizeException.class); + .isInstanceOf(DataAccessException.class); } @Test @@ -64,7 +63,7 @@ void throwExceptionWhenResultSetHasSizeMoreThanOne() throws SQLException { //then assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(SizeException.class); + .isInstanceOf(DataAccessException.class); } @Test From f4c2f4b91b21bb6136af68d291bb715864254979 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Tue, 10 Oct 2023 04:03:41 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20step4=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 | 5 -- .../com/techcourse/dao/UserHistoryDao.java | 6 --- .../techcourse/service/AppUserService.java | 36 ++++++++++++++ .../com/techcourse/service/TxUserService.java | 47 +++++++++++++++++++ .../com/techcourse/service/UserService.java | 45 ++---------------- .../service/AppUserServiceTest.java | 43 +++++++++++++++++ .../service/MockUserHistoryDao.java | 5 +- ...erviceTest.java => TxUserServiceTest.java} | 41 +++++----------- .../springframework/dao/SizeException.java | 26 ---------- .../jdbc/core/JdbcTemplate.java | 22 +++++---- .../jdbc/datasource/DataSourceUtils.java | 1 + .../TransactionSynchronizationManager.java | 26 ++++++++-- .../java/nextstep/jdbc/JdbcTemplateTest.java | 5 +- 13 files changed, 181 insertions(+), 127 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 create mode 100644 app/src/test/java/com/techcourse/service/AppUserServiceTest.java rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => TxUserServiceTest.java} (54%) delete mode 100644 jdbc/src/main/java/org/springframework/dao/SizeException.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 568c1abf00..575b425af7 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -31,11 +31,6 @@ public void update(final User user) { jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } - public void update(final Connection connection, final User user) { - final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; - jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); - } - public List findAll() { final var sql = "select id, account, password, email from users"; return jdbcTemplate.query(sql, USER_ROW_MAPPER); diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 9554b1adf9..38600a75c1 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -12,12 +12,6 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection connection, 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(), userHistory.getPassword(), - userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); - } - 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(), 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..3bac27f084 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,36 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } + +} 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..2feff7282f --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,47 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import java.sql.SQLException; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +public class TxUserService implements UserService { + + private final UserService userService; + + public TxUserService(final UserService userService) { + 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 dataSource = DataSourceConfig.getInstance(); + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + userService.changePassword(id, newPassword, createBy); + + connection.commit(); + } catch (final Exception e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index d8782ba089..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,47 +1,10 @@ package com.techcourse.service; -import com.techcourse.config.DataSourceConfig; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import java.sql.SQLException; -import org.springframework.dao.DataAccessException; -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; - } - - public User findById(final long id) { - return userDao.findById(id); - } - - public void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) throws SQLException { - final var connection = DataSourceConfig.getInstance().getConnection(); - try { - 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 (final Exception e) { - connection.rollback(); - throw new DataAccessException(e); - } finally { - connection.close(); - } - } +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/app/src/test/java/com/techcourse/service/AppUserServiceTest.java b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java new file mode 100644 index 0000000000..29c1f89a33 --- /dev/null +++ b/app/src/test/java/com/techcourse/service/AppUserServiceTest.java @@ -0,0 +1,43 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import java.sql.SQLException; +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; + +class AppUserServiceTest { + + private JdbcTemplate jdbcTemplate; + private UserDao userDao; + + @BeforeEach + void setUp() { + 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); + } + + @Test + void testChangePassword() throws SQLException { + final var userHistoryDao = new UserHistoryDao(jdbcTemplate); + final var userService = new AppUserService(userDao, userHistoryDao); + + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + + final var actual = userService.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 789dced668..c720ab0d58 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,7 +2,6 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import java.sql.Connection; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -13,8 +12,8 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(final Connection connection, final UserHistory userHistory) { - System.out.println("로그뜰까요아닐까용오오오"); + public void log(final UserHistory userHistory) { throw new DataAccessException(); } + } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java similarity index 54% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/TxUserServiceTest.java index 957478d82d..df13c9c0d5 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -2,23 +2,17 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import 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.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; -@Disabled -class UserServiceTest { +class TxUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @@ -32,37 +26,24 @@ void setUp() { final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } - - @Test - void testChangePassword() throws SQLException { - final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); - - 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() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - final MockUserHistoryDao mockUserHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, mockUserHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService); final var newPassword = "newPassword"; final var createBy = "gugu"; + // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); - assertAll( - () -> assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)), - () -> assertThat(actual.getPassword()).isNotEqualTo(newPassword) - ); + assertThat(actual.getPassword()).isNotEqualTo(newPassword); } + } diff --git a/jdbc/src/main/java/org/springframework/dao/SizeException.java b/jdbc/src/main/java/org/springframework/dao/SizeException.java deleted file mode 100644 index f5e8cfa4cd..0000000000 --- a/jdbc/src/main/java/org/springframework/dao/SizeException.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.springframework.dao; - -public class SizeException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public SizeException() { - super(); - } - - public SizeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - public SizeException(String message, Throwable cause) { - super(message, cause); - } - - public SizeException(String message) { - super(message); - } - - public SizeException(Throwable cause) { - super(cause); - } -} 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 0a5cc69419..864b266f0c 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,7 +11,7 @@ import javax.sql.DataSource; import org.springframework.dao.DataAccessException; -import org.springframework.dao.SizeException; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -26,10 +26,10 @@ public JdbcTemplate(final DataSource dataSource) { public T queryForObject(final String sql, final RowMapper rowMapper, final Object... params) { List results = query(sql, rowMapper, params); if (results.size() > 1) { - throw new SizeException("too many result. expected 1 but was " + results.size()); + throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { - throw new SizeException("no result"); + throw new DataAccessException("no result"); } return results.get(0); } @@ -37,17 +37,17 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... params) { List results = query(connection, sql, rowMapper, params); if (results.size() > 1) { - throw new SizeException("too many result. expected 1 but was " + results.size()); + throw new DataAccessException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { - throw new SizeException("no result"); + throw new DataAccessException("no result"); } return results.get(0); } public List query(final String sql, final RowMapper rowMapper, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql); + final var conn = getConnection(); + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql); final ResultSet resultSet = executeQuery(preparedStatement, parameters)) { log.debug("query : {}", sql); return mapResults(rowMapper, resultSet); @@ -90,8 +90,8 @@ private List mapResults(final RowMapper rowMapper, final ResultSet res } public void update(final String sql, final Object... parameters) { - try (final Connection conn = dataSource.getConnection(); - final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { + final Connection conn = getConnection(); + try (final PreparedStatement preparedStatement = conn.prepareStatement(sql)) { log.debug("query : {}", sql); setParameters(preparedStatement, parameters); @@ -114,4 +114,8 @@ public void update(final Connection conn, final String sql, final Object... para } } + private Connection getConnection() { + return DataSourceUtils.getConnection(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 3c40bfec52..be2f114ac1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -29,6 +29,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { + TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..4f1199b94b 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,23 +1,41 @@ package org.springframework.transaction.support; -import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; +import javax.sql.DataSource; public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = new ThreadLocal<>(); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } public static Connection getResource(DataSource key) { - return null; + final Map dataSourceConnectionMap = resources.get(); + + if (dataSourceConnectionMap == null) { + return null; + } + return dataSourceConnectionMap.get(key); } public static void bindResource(DataSource key, Connection value) { + if (resources.get() == null) { + resources.set(new HashMap<>()); + } + + resources.get().put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + final Map dataSourceConnectionMap = resources.get(); + + if (dataSourceConnectionMap == null) { + return null; + } + return dataSourceConnectionMap.remove(key); } + } diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index 3e7fa0c702..f96bb10211 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; -import org.springframework.dao.SizeException; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -49,7 +48,7 @@ void throwExceptionWhenResultSetIsEmpty() throws SQLException { //expect assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(SizeException.class); + .isInstanceOf(DataAccessException.class); } @Test @@ -64,7 +63,7 @@ void throwExceptionWhenResultSetHasSizeMoreThanOne() throws SQLException { //then assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(SizeException.class); + .isInstanceOf(DataAccessException.class); } @Test From 7b5306c9cf43d3137dbb2607fb52f4a0f3188333 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 13 Oct 2023 10:02:51 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20transaction=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TxUserService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index 2feff7282f..6064d1ab2c 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -21,7 +21,23 @@ public User findById(final long id) { @Override public void insert(final User user) { - userService.insert(user); + final var dataSource = DataSourceConfig.getInstance(); + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + userService.insert(user); + + connection.commit(); + } catch (final Exception e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } } @Override From ea2dcd568be2359d042a694121129d7aefa34715 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 13 Oct 2023 10:07:25 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20SizeException=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springframework/dao/SizeException.java | 26 +++++++++++++++++++ .../jdbc/core/JdbcTemplate.java | 5 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 jdbc/src/main/java/org/springframework/dao/SizeException.java diff --git a/jdbc/src/main/java/org/springframework/dao/SizeException.java b/jdbc/src/main/java/org/springframework/dao/SizeException.java new file mode 100644 index 0000000000..f5e8cfa4cd --- /dev/null +++ b/jdbc/src/main/java/org/springframework/dao/SizeException.java @@ -0,0 +1,26 @@ +package org.springframework.dao; + +public class SizeException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SizeException() { + super(); + } + + public SizeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public SizeException(String message, Throwable cause) { + super(message, cause); + } + + public SizeException(String message) { + super(message); + } + + public SizeException(Throwable cause) { + super(cause); + } +} 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 864b266f0c..50e4510bc0 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -11,6 +11,7 @@ import javax.sql.DataSource; import org.springframework.dao.DataAccessException; +import org.springframework.dao.SizeException; import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -26,7 +27,7 @@ public JdbcTemplate(final DataSource dataSource) { public T queryForObject(final String sql, final RowMapper rowMapper, final Object... params) { List results = query(sql, rowMapper, params); if (results.size() > 1) { - throw new DataAccessException("too many result. expected 1 but was " + results.size()); + throw new SizeException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { throw new DataAccessException("no result"); @@ -37,7 +38,7 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, final Object... params) { List results = query(connection, sql, rowMapper, params); if (results.size() > 1) { - throw new DataAccessException("too many result. expected 1 but was " + results.size()); + throw new SizeException("too many result. expected 1 but was " + results.size()); } if (results.isEmpty()) { throw new DataAccessException("no result"); From c9cd8f8232fcb4b0a29a48541b6375dcb36d9696 Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 13 Oct 2023 10:24:48 +0900 Subject: [PATCH 14/15] =?UTF-8?q?test:=20test=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index f96bb10211..3a0a830125 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; +import org.springframework.dao.SizeException; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -63,7 +64,7 @@ void throwExceptionWhenResultSetHasSizeMoreThanOne() throws SQLException { //then assertThatThrownBy( () -> jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new Integer(1), params)) - .isInstanceOf(DataAccessException.class); + .isInstanceOf(SizeException.class); } @Test From 27379b0ace0cb43ee26928a46a8b818c2308c88b Mon Sep 17 00:00:00 2001 From: eunbii0213 Date: Fri, 13 Oct 2023 10:26:26 +0900 Subject: [PATCH 15/15] =?UTF-8?q?feat:=20findById=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/service/TxUserService.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java index 6064d1ab2c..ea99cfb725 100644 --- a/app/src/main/java/com/techcourse/service/TxUserService.java +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -16,7 +16,27 @@ public TxUserService(final UserService userService) { @Override public User findById(final long id) { - return userService.findById(id); + User user; + + final var dataSource = DataSourceConfig.getInstance(); + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + user = userService.findById(id); + + connection.commit(); + } catch (final Exception e) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(ex); + } + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + + return user; } @Override