├── .gitignore ├── LICENSE ├── README.md └── kopring ├── .gitignore ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── infra ├── README.md ├── mysql │ └── docker-compose.yml └── redis │ └── docker-compose.yml ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── example │ │ └── kopring │ │ ├── BaseEntity.kt │ │ ├── KopringApplication.kt │ │ ├── RetryableService.kt │ │ ├── common │ │ └── support │ │ │ ├── jpa │ │ │ └── CustomCrudRepository.kt │ │ │ └── redis │ │ │ └── RedisKeyValueStore.kt │ │ ├── config │ │ ├── QueryDslConfig.kt │ │ ├── RedisConfig.kt │ │ └── RetryConfig.kt │ │ ├── member │ │ ├── InitLoader.kt │ │ ├── controller │ │ │ └── MemberController.kt │ │ ├── domain │ │ │ ├── Member.kt │ │ │ ├── MemberRepository.kt │ │ │ ├── MyMember.kt │ │ │ ├── MyMemberRepository.kt │ │ │ ├── Team.kt │ │ │ └── TeamRepository.kt │ │ ├── event │ │ │ └── DomainEventPublisher.kt │ │ └── service │ │ │ ├── AbstractService.kt │ │ │ ├── KtFactoryMethodPattern.kt │ │ │ ├── MemberService.kt │ │ │ ├── application │ │ │ └── MemberSave.kt │ │ │ └── query │ │ │ └── MemberFind.kt │ │ └── members │ │ └── domain │ │ ├── AbstractMember.kt │ │ └── AbstractMemberRepository.kt └── resources │ └── application.yml └── test └── kotlin └── com └── example └── kopring ├── KopringApplicationTests.kt ├── common └── support │ └── redis │ └── RedisKeyValueStoreTest.kt ├── member ├── domain │ ├── MemberDomainEventTest.kt │ ├── MemberRepositoryTest.kt │ └── MemberTest.kt └── service │ ├── CallerServiceTest.kt │ ├── KtFactoryMethodPatternTest.kt │ ├── MemberServiceTest.kt │ └── RetryableServiceTest.kt ├── members └── domain │ └── AbstractMemberRepositoryTest.kt └── test ├── BaseTests.kt ├── IntegrationTestExecuteListener.kt └── TruncateTablesTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 yoonsung.jung (Goodall) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-spring-jpa-playground 2 | kotlin에서의 spring, jpa 기능을 학습, 테스트 해보는 용도의 레포지토리입니다. 3 | [브라운](https://github.com/boorownie)님의 [학습테스트](https://github.com/next-step/spring-learning-test/tree/main) 레포지토리 에서 아이디어를 얻어 만들었습니다. 4 | 5 | 6 | ## Goal 7 | - 매번 스프링, JPA 관련 프로젝트를 새롭게 만들면서 기능을 테스트, 학습하는것에 불편함을 느껴 브랜치 기반으로 프로젝트를 사용하기 위해 만들었습니다. 8 | - 테스트해보고 싶은 기능이 있는 경우, 브랜치를 만들어 기능을 학습, 테스트해봅니다. 9 | - java를 사용할떄 이용하던 [레포지토리](https://github.com/unluckyjung/spring-jpa-playground)가 있으나, 주언어가 Kotlin 으로 변경되면서 kotlin 기반의 테스트 환경의 필요성이 느껴저 레포지토리를 새롭게 만들었습니다. 10 | 11 | --- 12 | 13 | ## Infra Setting 14 | - Mysql or Redis 를 띄워야 하는 경우 [infra](https://github.com/unluckyjung/kotlin-spring-jpa-playground/tree/main/infra) 쪽 문서를 참고하셔서 세팅을 해주시면 됩니다. 15 | -------------------------------------------------------------------------------- /kopring/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | 40 | 41 | # Created by https://www.toptal.com/developers/gitignore/api/intellij,kotlin 42 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,kotlin 43 | 44 | ### Intellij ### 45 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 46 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 47 | 48 | # User-specific stuff 49 | .idea/**/workspace.xml 50 | .idea/**/tasks.xml 51 | .idea/**/usage.statistics.xml 52 | .idea/**/dictionaries 53 | .idea/**/shelf 54 | 55 | # AWS User-specific 56 | .idea/**/aws.xml 57 | 58 | # Generated files 59 | .idea/**/contentModel.xml 60 | 61 | # Sensitive or high-churn files 62 | .idea/**/dataSources/ 63 | .idea/**/dataSources.ids 64 | .idea/**/dataSources.local.xml 65 | .idea/**/sqlDataSources.xml 66 | .idea/**/dynamic.xml 67 | .idea/**/uiDesigner.xml 68 | .idea/**/dbnavigator.xml 69 | 70 | # Gradle 71 | .idea/**/gradle.xml 72 | .idea/**/libraries 73 | 74 | # Gradle and Maven with auto-import 75 | # When using Gradle or Maven with auto-import, you should exclude module files, 76 | # since they will be recreated, and may cause churn. Uncomment if using 77 | # auto-import. 78 | # .idea/artifacts 79 | # .idea/compiler.xml 80 | # .idea/jarRepositories.xml 81 | # .idea/modules.xml 82 | # .idea/*.iml 83 | # .idea/modules 84 | # *.iml 85 | # *.ipr 86 | 87 | # CMake 88 | cmake-build-*/ 89 | 90 | # Mongo Explorer plugin 91 | .idea/**/mongoSettings.xml 92 | 93 | # File-based project format 94 | *.iws 95 | 96 | # IntelliJ 97 | out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Cursive Clojure plugin 106 | .idea/replstate.xml 107 | 108 | # SonarLint plugin 109 | .idea/sonarlint/ 110 | 111 | # Crashlytics plugin (for Android Studio and IntelliJ) 112 | com_crashlytics_export_strings.xml 113 | crashlytics.properties 114 | crashlytics-build.properties 115 | fabric.properties 116 | 117 | # Editor-based Rest Client 118 | .idea/httpRequests 119 | 120 | # Android studio 3.1+ serialized cache file 121 | .idea/caches/build_file_checksums.ser 122 | 123 | ### Intellij Patch ### 124 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 125 | 126 | # *.iml 127 | # modules.xml 128 | # .idea/misc.xml 129 | # *.ipr 130 | 131 | # Sonarlint plugin 132 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 133 | .idea/**/sonarlint/ 134 | 135 | # SonarQube Plugin 136 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 137 | .idea/**/sonarIssues.xml 138 | 139 | # Markdown Navigator plugin 140 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 141 | .idea/**/markdown-navigator.xml 142 | .idea/**/markdown-navigator-enh.xml 143 | .idea/**/markdown-navigator/ 144 | 145 | # Cache file creation bug 146 | # See https://youtrack.jetbrains.com/issue/JBR-2257 147 | .idea/$CACHE_FILE$ 148 | 149 | # CodeStream plugin 150 | # https://plugins.jetbrains.com/plugin/12206-codestream 151 | .idea/codestream.xml 152 | 153 | ### Kotlin ### 154 | # Compiled class file 155 | *.class 156 | 157 | # Log file 158 | *.log 159 | 160 | # BlueJ files 161 | *.ctxt 162 | 163 | # Mobile Tools for Java (J2ME) 164 | .mtj.tmp/ 165 | 166 | # Package Files # 167 | *.jar 168 | *.war 169 | *.nar 170 | *.ear 171 | *.zip 172 | *.tar.gz 173 | *.rar 174 | 175 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 176 | hs_err_pid* 177 | replay_pid* 178 | 179 | # End of https://www.toptal.com/developers/gitignore/api/intellij,kotlin 180 | 181 | 182 | # ─────────────────────────────────────────────────── 183 | # MySQL 데이터 디렉토리 전체 무시 184 | infra/mysql/db/mysql/data/ 185 | 186 | # ─────────────────────────────────────────────────── 187 | # MySQL이 자동으로 만드는 파일 확장자 무시 188 | # (인증서·키, 테이블 스페이스, 로그 등) 189 | *.pem 190 | *.key 191 | *.ibd 192 | *.err 193 | *.pid 194 | *.sock 195 | -------------------------------------------------------------------------------- /kopring/README.md: -------------------------------------------------------------------------------- 1 | ## Environment 2 | - Kotlin (jvmTarget: 11) 3 | - Spring boot 2.6.7 4 | - Spring Data JPA 5 | - H2 6 | 7 | ## 참고 8 | - [H2 DB URL](http://localhost:8080/h2-console/) -------------------------------------------------------------------------------- /kopring/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.springframework.boot") version "3.3.11" 5 | id("io.spring.dependency-management") version "1.1.7" 6 | kotlin("jvm") version "1.9.25" 7 | kotlin("plugin.spring") version "1.9.25" 8 | kotlin("plugin.jpa") version "1.9.25" 9 | kotlin("kapt") version "1.9.25" 10 | } 11 | 12 | group = "com.example" 13 | version = "0.0.1-SNAPSHOT" 14 | java.sourceCompatibility = JavaVersion.VERSION_17 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | allOpen { 21 | annotation("jakarta.persistence.Entity") 22 | annotation("jakarta.persistence.MappedSuperclass") 23 | annotation("jakarta.persistence.Embeddable") 24 | } 25 | 26 | dependencies { 27 | // Spring Boot starters (versions managed by Spring Boot 3.3.11 BOM) 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | implementation("org.springframework.boot:spring-boot-starter-validation") 30 | implementation("org.springframework.boot:spring-boot-starter-web") 31 | implementation("org.springframework.boot:spring-boot-starter-data-redis") 32 | 33 | // JSON + Kotlin support 34 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 35 | implementation("org.jetbrains.kotlin:kotlin-reflect") 36 | implementation("org.jetbrains.kotlin:kotlin-stdlib") 37 | 38 | // Database & Drivers 39 | // → let BOM manage mariadb version (3.3.4) 40 | implementation("org.mariadb.jdbc:mariadb-java-client") 41 | runtimeOnly("com.h2database:h2") 42 | 43 | // Utilities 44 | implementation("org.springframework.retry:spring-retry") 45 | implementation("org.springframework:spring-aspects") 46 | implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.11.0") 47 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 48 | 49 | // QueryDSL (Jakarta) 50 | implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta") 51 | kapt("com.querydsl:querydsl-apt:5.1.0:jakarta") 52 | compileOnly("jakarta.annotation:jakarta.annotation-api") 53 | compileOnly("jakarta.persistence:jakarta.persistence-api") 54 | 55 | // Testing 56 | testImplementation("org.springframework.boot:spring-boot-starter-test") 57 | testImplementation("org.junit.jupiter:junit-jupiter-params") 58 | testImplementation("io.kotest:kotest-runner-junit5:5.9.1") 59 | testImplementation("io.kotest:kotest-assertions-core:5.9.1") 60 | testImplementation("io.kotest:kotest-property:5.9.1") 61 | testImplementation("io.mockk:mockk:1.14.2") 62 | testImplementation("com.ninja-squad:springmockk:4.0.2") 63 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") 64 | } 65 | 66 | // Kapt settings for QueryDSL 67 | kapt { 68 | correctErrorTypes = true 69 | arguments { 70 | arg("querydsl.entityAccessors", "true") 71 | } 72 | } 73 | 74 | // Kotlin compile options 75 | tasks.withType { 76 | kotlinOptions { 77 | freeCompilerArgs = listOf("-Xjsr305=strict") 78 | jvmTarget = "17" 79 | } 80 | } 81 | 82 | // Include generated Q-classes in sources 83 | sourceSets { 84 | main { 85 | java { 86 | srcDir("build/generated/source/kapt/main") 87 | } 88 | } 89 | } 90 | 91 | kotlin { 92 | sourceSets.named("main") { 93 | kotlin.srcDir("build/generated/source/kapt/main") 94 | } 95 | } 96 | 97 | // Test setup 98 | tasks.withType { 99 | useJUnitPlatform() 100 | } 101 | 102 | tasks.test { 103 | jvmArgs( 104 | "--add-opens", "java.base/java.time=ALL-UNNAMED", 105 | "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED" 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /kopring/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /kopring/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /kopring/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /kopring/infra/README.md: -------------------------------------------------------------------------------- 1 | # Mysql 2 | 3 | ### Setting 4 | 5 | ```console 6 | $ docker-compose up -d 7 | 8 | $ docker exec -it mysql /bin/bash 9 | $ mysql -u root -p 10 | ``` 11 | 12 | ### Command 13 | 14 | ```console 15 | show databases 16 | use unluckyjung 17 | ``` 18 | 19 | ### Spring Connection 20 | 21 | ```yml 22 | -- applicaton.yml 23 | 24 | spring: 25 | datasource: 26 | driver-class-name: org.mariadb.jdbc.Driver 27 | url: jdbc:mysql://localhost:13306/unluckyjung 28 | username: root 29 | password: asdf 30 | ``` 31 | 32 | image 33 | 34 | # Redis 35 | 36 | ### Setting 37 | ```console 38 | $ docker pull redis 39 | 40 | $ docker-compose -d 41 | 42 | $ docker exec -it redis redis-cli 43 | ``` 44 | 45 | ### Command 46 | 47 | ```console 48 | $ keys * 49 | $ type 50 | $ flushall 51 | 52 | # [string] 53 | $ get 54 | $ mget 55 | $ ttl 56 | $ del 57 | 58 | # [set] 59 | $ smembers 60 | $ srem 61 | 62 | # [hash] 63 | $ hkeys 64 | $ hget 65 | $ hdel 66 | ``` 67 | 68 | ### Spring Connection 69 | 70 | ```gradle 71 | // build.gradle.kts 72 | 73 | implementation("org.springframework.boot:spring-boot-starter-data-redis") 74 | ``` 75 | 76 | ```yml 77 | -- applicaton.yml 78 | 79 | spring: 80 | redis: 81 | host: localhost 82 | port: 6379 83 | ``` 84 | 85 | ```kotlin 86 | @EnableCaching 87 | @SpringBootApplication 88 | class KopringApplication 89 | 90 | ``` 91 | 92 | ```kotlin 93 | import org.springframework.beans.factory.annotation.Value 94 | import org.springframework.context.annotation.Bean 95 | import org.springframework.context.annotation.Configuration 96 | import org.springframework.data.redis.connection.RedisConnectionFactory 97 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory 98 | import org.springframework.data.redis.core.RedisTemplate 99 | 100 | @Configuration 101 | class RedisConfig( 102 | @Value("\${spring.redis.host}") 103 | val host: String, 104 | 105 | @Value("\${spring.redis.port}") 106 | val port: Int, 107 | ) { 108 | 109 | @Bean 110 | fun redisConnectionFactory(): RedisConnectionFactory { 111 | return LettuceConnectionFactory(host, port) 112 | } 113 | 114 | @Bean 115 | fun redisTemplate(): RedisTemplate<*, *> { 116 | return RedisTemplate().apply { 117 | this.setConnectionFactory(redisConnectionFactory()) 118 | } 119 | } 120 | } 121 | ``` 122 | -------------------------------------------------------------------------------- /kopring/infra/mysql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | local-db: 4 | image: mysql:8.0 5 | container_name: mysql 6 | hostname: mysql13306 7 | restart: always 8 | ports: 9 | - 13306:3306 10 | environment: 11 | MYSQL_DATABASE: unluckyjung 12 | MYSQL_ROOT_PASSWORD: asdf 13 | TZ: Asia/Seoul 14 | volumes: 15 | - ./db/mysql/data:/var/lib/mysql 16 | - ./db/mysql/init:/docker-entrypoint-initdb. 17 | 18 | -------------------------------------------------------------------------------- /kopring/infra/redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | redis: 4 | image: redis:latest 5 | container_name: redis 6 | hostname: redis6379 7 | command: redis-server 8 | labels: 9 | - "name=redis" 10 | - "mode=standalone" 11 | ports: 12 | - 6379:6379 13 | -------------------------------------------------------------------------------- /kopring/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kopring" 2 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/BaseEntity.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring 2 | 3 | import java.time.ZonedDateTime 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.MappedSuperclass 6 | import jakarta.persistence.PrePersist 7 | import jakarta.persistence.PreUpdate 8 | 9 | @MappedSuperclass 10 | abstract class BaseEntity( 11 | @Column(name = "created_at") 12 | var createdAt: ZonedDateTime = ZonedDateTime.now(), 13 | 14 | @Column(name = "updated_at") 15 | var updatedAt: ZonedDateTime = ZonedDateTime.now() 16 | ) { 17 | @PrePersist 18 | fun prePersist() { 19 | createdAt = ZonedDateTime.now() 20 | updatedAt = ZonedDateTime.now() 21 | } 22 | 23 | @PreUpdate 24 | fun preUpdate() { 25 | updatedAt = ZonedDateTime.now() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/KopringApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.cache.annotation.EnableCaching 6 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing 7 | 8 | // @ActiveProfiles("mysql") // mysql 로 띄우고 싶은경우 활성화. 9 | @EnableJpaAuditing 10 | @EnableCaching 11 | @SpringBootApplication 12 | class KopringApplication 13 | 14 | fun main(args: Array) { 15 | runApplication(*args) 16 | } 17 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/RetryableService.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring 2 | 3 | import org.springframework.retry.annotation.Backoff 4 | import org.springframework.retry.annotation.Retryable 5 | import org.springframework.stereotype.Service 6 | 7 | 8 | object RetryCounter { 9 | var counter = 0 10 | } 11 | 12 | @Service 13 | class RetryService { 14 | @Retryable( 15 | value = [IllegalArgumentException::class], 16 | maxAttempts = 3, 17 | backoff = Backoff(delay = 1000) 18 | ) 19 | fun retryFun(pivotCount: Int): String { 20 | RetryCounter.counter++ 21 | println("try ${RetryCounter.counter}") 22 | 23 | if (RetryCounter.counter < pivotCount) { 24 | throw IllegalArgumentException() 25 | } 26 | return "success" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/common/support/jpa/CustomCrudRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.common.support.jpa 2 | 3 | import org.springframework.data.repository.CrudRepository 4 | import org.springframework.data.repository.findByIdOrNull 5 | 6 | inline fun CrudRepository.findByIdOrThrow( 7 | id: ID, e: Exception = IllegalArgumentException("${T::class.java.name} entity 를 찾을 수 없습니다. id=$id") 8 | ): T = findByIdOrNull(id) ?: throw e 9 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/common/support/redis/RedisKeyValueStore.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.common.support.redis 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.springframework.data.redis.core.RedisTemplate 5 | import org.springframework.stereotype.Component 6 | import java.util.concurrent.TimeUnit 7 | 8 | @Component 9 | class RedisKeyValueStore( 10 | private val redisTemplate: RedisTemplate, 11 | private val objectMapper: ObjectMapper, 12 | ) { 13 | 14 | fun save(key: String, value: Any, timeOut: RedisKeyValueTimeOut? = null) { 15 | 16 | timeOut?.let { 17 | redisTemplate.opsForValue().set( 18 | key, 19 | objectMapper.writeValueAsString(value), 20 | timeOut.time, 21 | timeOut.timeUnit 22 | ) 23 | } ?: run { 24 | redisTemplate.opsForValue().set( 25 | key, 26 | objectMapper.writeValueAsString(value), 27 | ) 28 | } 29 | } 30 | 31 | fun delete(key: String){ 32 | redisTemplate.delete(key) 33 | } 34 | 35 | fun getByKey(key: String, clazz: Class): T? { 36 | val result = redisTemplate.opsForValue()[key].toString() 37 | return if (result.isEmpty()) null 38 | else { 39 | return objectMapper.readValue(result, clazz) 40 | } 41 | } 42 | 43 | operator fun get(key: String, clazz: Class): T? { 44 | return getByKey(key = key, clazz = clazz) 45 | } 46 | 47 | operator fun set(key: String, value: Any) { 48 | return save(key = key, value = value) 49 | } 50 | } 51 | 52 | class RedisKeyValueTimeOut( 53 | val time: Long, 54 | val timeUnit: TimeUnit, 55 | ) 56 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/config/QueryDslConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.config 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import jakarta.persistence.EntityManager 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | 8 | 9 | @Configuration 10 | class QueryDSLConfig( 11 | private val entityManager: EntityManager, 12 | ) { 13 | 14 | @Bean 15 | fun jpaQueryFactory(): JPAQueryFactory { 16 | return JPAQueryFactory(entityManager) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/config/RedisConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.config 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.cache.annotation.EnableCaching 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.data.redis.connection.RedisConnectionFactory 8 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory 9 | import org.springframework.data.redis.core.RedisTemplate 10 | import org.springframework.data.redis.serializer.StringRedisSerializer 11 | 12 | @Configuration 13 | @EnableCaching 14 | class RedisConfig( 15 | @Value("\${spring.data.redis.host}") 16 | val host: String, 17 | 18 | @Value("\${spring.data.redis.port}") 19 | val port: Int, 20 | ) { 21 | 22 | @Bean 23 | fun redisConnectionFactory(): RedisConnectionFactory { 24 | return LettuceConnectionFactory(host, port) 25 | } 26 | 27 | @Bean 28 | fun redisTemplate(): RedisTemplate { 29 | return RedisTemplate().apply { 30 | this.setConnectionFactory(redisConnectionFactory()) 31 | 32 | this.keySerializer = StringRedisSerializer() 33 | this.valueSerializer = StringRedisSerializer() 34 | 35 | this.hashKeySerializer = StringRedisSerializer() 36 | this.hashValueSerializer = StringRedisSerializer() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/config/RetryConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.retry.annotation.EnableRetry 5 | 6 | @Configuration 7 | @EnableRetry 8 | class RetryConfig 9 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/InitLoader.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member 2 | 3 | import com.example.kopring.common.support.jpa.findByIdOrThrow 4 | import com.example.kopring.member.domain.MyMember 5 | import com.example.kopring.member.domain.Team 6 | import com.example.kopring.member.domain.TeamRepository 7 | import org.slf4j.LoggerFactory 8 | import org.springframework.boot.ApplicationArguments 9 | import org.springframework.boot.ApplicationRunner 10 | import org.springframework.core.annotation.Order 11 | import org.springframework.stereotype.Component 12 | import org.springframework.transaction.annotation.Transactional 13 | 14 | @Order(1) 15 | @Component 16 | class InitLoader( 17 | private val teamRepository: TeamRepository, 18 | ) : ApplicationRunner { 19 | 20 | @Transactional 21 | override fun run(args: ApplicationArguments?) { 22 | Team("team1").apply { 23 | // 연관관계의 주인인 MyMember 에 바로 team 을 넣어주면서 삽입 (team 을 NotNull 하게 관리가능) 24 | this.addMember(MyMember("member1", this)) 25 | this.addMember(MyMember("member2", this)) 26 | }.let(teamRepository::save) 27 | 28 | Team("team2").apply { 29 | this.addAllMembers(listOf(MyMember("member3", this), MyMember("member4", this))) 30 | }.let(teamRepository::save) 31 | 32 | // MyMember.team 을 nullable 하면 객체 생성 타이밍을 다르게 가져갈 수 있다. 33 | val members = mutableListOf(MyMember("member5", null), MyMember("member6", null)) 34 | Team("team3").apply { 35 | this.addAllMembers(members) 36 | }.let(teamRepository::save) 37 | 38 | Team("team4").apply { 39 | this.addAllMembers(listOf(MyMember("member7", this), MyMember("member8", this))) 40 | }.let(teamRepository::save) 41 | 42 | Team("team5").apply { 43 | this.addAllMembers(listOf(MyMember("member9", this), MyMember("member10", this))) 44 | }.let(teamRepository::save) 45 | } 46 | } 47 | 48 | @Order(2) 49 | @Component 50 | class InitLoader2( 51 | private val teamRepository: TeamRepository, 52 | ) : ApplicationRunner { 53 | @Transactional 54 | override fun run(args: ApplicationArguments?) { 55 | val team = teamRepository.findByIdOrThrow(1) 56 | 57 | // cascade 를 이용한 삭제 58 | team.members.removeAt(1) 59 | } 60 | } 61 | 62 | @Order(3) 63 | @Component 64 | class InitLoader3( 65 | private val teamRepository: TeamRepository, 66 | ) : ApplicationRunner { 67 | 68 | @Transactional(readOnly = true) 69 | override fun run(args: ApplicationArguments?) { 70 | logger.info("lazyLoading Check lazy ======================") 71 | val team = teamRepository.findByIdOrThrow(1) // team 만 select 하고 member join 쿼리 안나감. 72 | logger.info("lazyLoading Check: loading =====================") 73 | 74 | // member join query 발생. 75 | logger.info("members size : ${team.members.size}") 76 | } 77 | 78 | companion object { 79 | val logger = LoggerFactory.getLogger(this::class.java) 80 | } 81 | } 82 | 83 | @Order(4) 84 | @Component 85 | class InitLoader4( 86 | private val teamRepository: TeamRepository, 87 | ) : ApplicationRunner { 88 | 89 | @Transactional 90 | override fun run(args: ApplicationArguments?) { 91 | val team = teamRepository.findByIdOrThrow(3) 92 | team.delete() 93 | } 94 | } 95 | 96 | @Order(5) 97 | @Component 98 | class InitLoader5( 99 | private val teamRepository: TeamRepository, 100 | ) : ApplicationRunner { 101 | 102 | @Transactional 103 | override fun run(args: ApplicationArguments?) { 104 | val team = teamRepository.findByIdOrThrow(5) 105 | team.apply { 106 | addMember(MyMember("goodall", this)) 107 | addMember(MyMember("unluckyjung", this)) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/controller/MemberController.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.controller 2 | 3 | import com.example.kopring.member.domain.Member 4 | import com.example.kopring.member.service.MemberService 5 | import org.springframework.web.bind.annotation.* 6 | import jakarta.validation.Valid 7 | 8 | @RequestMapping("api/v1/member") 9 | @RestController 10 | class MemberController( 11 | private val memberService: MemberService, 12 | ) { 13 | 14 | @GetMapping("/{id}") 15 | fun find(@PathVariable id: Long): Member.Response { 16 | return memberService.find(id = id) 17 | } 18 | 19 | @PostMapping 20 | fun save(@RequestBody @Valid request: Member.Request): Member.Response { 21 | return memberService.save(request) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/Member.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import com.example.kopring.BaseEntity 4 | import java.time.ZonedDateTime 5 | import jakarta.persistence.* 6 | import jakarta.validation.constraints.NotBlank 7 | 8 | @Table(name = "member") 9 | @Entity 10 | class Member( 11 | name: String, 12 | 13 | createdAt: ZonedDateTime = ZonedDateTime.now(), 14 | 15 | @Column(name = "id", nullable = false) 16 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | val id: Long = 0L 18 | ) : BaseEntity(createdAt = createdAt) { 19 | 20 | @Column(name = "name", nullable = false) 21 | var name = name 22 | protected set 23 | 24 | fun changeName(name: String) { 25 | this.name = name 26 | } 27 | 28 | data class Request( 29 | @field: NotBlank(message = "이름은 공백으로 이루어져있을 수 없습니다.") 30 | val name: String, 31 | ) 32 | 33 | data class Response( 34 | val id: Long, 35 | val name: String, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/MemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | import org.springframework.data.repository.findByIdOrNull 5 | 6 | 7 | fun MemberRepository.findByMemberId(id: Long): Member = 8 | findByIdOrNull(id) ?: throw NoSuchElementException("member id가 존재하지 않습니다. id: $id") 9 | 10 | interface MemberRepository : JpaRepository { 11 | fun findByName(name: String): Member? 12 | fun existsByName(name: String): Boolean 13 | } 14 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/MyMember.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import org.hibernate.annotations.SQLDelete 4 | import org.hibernate.annotations.Where 5 | import java.time.ZonedDateTime 6 | import jakarta.persistence.* 7 | 8 | @Where(clause = "deleted_at is null") 9 | @SQLDelete(sql = "UPDATE my_member SET deleted_at = NOW() WHERE id = ?") 10 | @Table(name = "my_member") 11 | @Entity 12 | class MyMember( 13 | @Column(name = "name", nullable = false) 14 | val name: String, 15 | 16 | @ManyToOne(fetch = FetchType.LAZY) 17 | @JoinColumn(name = "team_id", nullable = false) 18 | var team: Team?, 19 | 20 | deletedAt: ZonedDateTime? = null, 21 | 22 | @Column(name = "id", nullable = false) 23 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | val id: Long = 0L 25 | ) { 26 | @Column(name = "deleted_at") 27 | var deletedAt: ZonedDateTime? = deletedAt 28 | protected set 29 | 30 | fun delete() { 31 | deletedAt = ZonedDateTime.now() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/MyMemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface MyMemberRepository : JpaRepository -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/Team.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import org.hibernate.annotations.SQLDelete 4 | import org.hibernate.annotations.Where 5 | import java.time.ZonedDateTime 6 | import jakarta.persistence.* 7 | 8 | @SQLDelete(sql = "UPDATE team SET deleted_at = NOW() WHERE id = ?") 9 | @Where(clause = "deleted_at is null") 10 | @Table(name = "team") 11 | @Entity 12 | class Team( 13 | @Column(name = "name", nullable = false) 14 | val name: String, 15 | 16 | // 연관관계 주인은 MyMember(fk team_id 소유) 17 | @OneToMany(mappedBy = "team", cascade = [CascadeType.ALL], orphanRemoval = true) 18 | val members: MutableList = mutableListOf(), 19 | 20 | deletedAt: ZonedDateTime? = null, 21 | 22 | @Column(name = "id", nullable = false) 23 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | val id: Long = 0L 25 | ) { 26 | @Column(name = "deleted_at", nullable = true) 27 | var deletedAt: ZonedDateTime? = deletedAt 28 | protected set 29 | 30 | fun addMember(member: MyMember) { 31 | members.add(member) 32 | member.team = this 33 | } 34 | 35 | fun addAllMembers(members: List) { 36 | members.forEach { 37 | addMember(it) 38 | } 39 | } 40 | 41 | fun delete() { 42 | this.deletedAt = ZonedDateTime.now() 43 | members.forEach { 44 | it.delete() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/domain/TeamRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface TeamRepository : JpaRepository 6 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/event/DomainEventPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.event 2 | 3 | import org.springframework.context.ApplicationEventPublisher 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | class DomainEventPublisher private constructor( 8 | applicationEventPublisher: ApplicationEventPublisher, 9 | ) { 10 | init { 11 | DomainEventPublisher.applicationEventPublisher = applicationEventPublisher 12 | } 13 | companion object{ 14 | private lateinit var applicationEventPublisher: ApplicationEventPublisher 15 | 16 | fun publishEvent(event: Any){ 17 | applicationEventPublisher.publishEvent(event) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/service/AbstractService.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.stereotype.Service 5 | 6 | abstract class AbstractService( 7 | private val memberService: MemberService, 8 | ) { 9 | fun getHashCode(): Int { 10 | return memberService.hashCode() 11 | } 12 | 13 | abstract fun abstractFun() 14 | } 15 | 16 | @Service 17 | class AbstractServiceImpl( 18 | memberService: MemberService, 19 | ) : AbstractService( 20 | memberService = memberService, 21 | ) { 22 | override fun abstractFun() { 23 | println("constructor abstractFun") 24 | } 25 | } 26 | 27 | @Service 28 | abstract class AbstractServiceUseAutoWired { 29 | 30 | // autowired 를 이용하면, 구현체 클래스에서 또 의존성 주입을 받아줄필요가 없어진다. 31 | @Autowired 32 | private lateinit var memberService: MemberService 33 | 34 | fun getHashCode(): Int { 35 | return memberService.hashCode() 36 | } 37 | 38 | abstract fun abstractFun() 39 | } 40 | 41 | @Service 42 | class AbstractServiceUseAutoWiredImpl : AbstractServiceUseAutoWired() { 43 | override fun abstractFun() { 44 | println("autowired abstractFun") 45 | } 46 | } 47 | 48 | 49 | @Service 50 | class CallerService( 51 | private val abstractServiceImpl: AbstractServiceImpl, 52 | private val abstractServiceUseAutoWiredImpl: AbstractServiceUseAutoWiredImpl 53 | ) { 54 | fun runConstructor(){ 55 | println("runConstructor") 56 | println(abstractServiceImpl.getHashCode()) 57 | abstractServiceImpl.abstractFun() 58 | } 59 | 60 | fun runAutowired(){ 61 | println("runAutowired") 62 | println(abstractServiceUseAutoWiredImpl.getHashCode()) 63 | abstractServiceUseAutoWiredImpl.abstractFun() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/service/KtFactoryMethodPattern.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import com.example.kopring.member.domain.Member 4 | import com.example.kopring.member.domain.MemberRepository 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class KtFactoryMethodPattern private constructor( 9 | private val memberRepository: MemberRepository, 10 | ) { 11 | init { 12 | ktFactoryMethodPattern = this 13 | } 14 | 15 | private fun getMemberList(): MutableList { 16 | return memberRepository.findAll() 17 | } 18 | 19 | companion object { 20 | lateinit var ktFactoryMethodPattern: KtFactoryMethodPattern 21 | 22 | fun getMemberList(): MutableList { 23 | return ktFactoryMethodPattern.getMemberList() 24 | } 25 | } 26 | } 27 | 28 | @Component 29 | class KtFactoryMethodPattern2 private constructor( 30 | private val memberRepository: MemberRepository, 31 | ) { 32 | init { 33 | KtFactoryMethodPattern2.memberRepository = memberRepository 34 | } 35 | 36 | companion object { 37 | // 의존성 주입 받아야 하는 종류를 이렇게 처리가능 38 | private lateinit var memberRepository: MemberRepository 39 | 40 | fun getMemberList(): MutableList { 41 | return memberRepository.findAll() 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/service/MemberService.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import com.example.kopring.member.domain.Member 4 | import com.example.kopring.member.service.application.MemberSave 5 | import com.example.kopring.member.service.query.MemberFind 6 | import org.springframework.stereotype.Service 7 | import org.springframework.transaction.annotation.Propagation 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @Service 11 | class MemberService( 12 | private val memberSave: MemberSave, 13 | private val memberFind: MemberFind, 14 | ) { 15 | fun find(id: Long): Member.Response { 16 | return memberFind.findById(id = id).run { 17 | Member.Response( 18 | id = this.id, 19 | name = this.name 20 | ) 21 | } 22 | } 23 | 24 | fun save(req: Member.Request): Member.Response { 25 | return memberSave.save(req).run { 26 | Member.Response( 27 | id = this.id, 28 | name = this.name 29 | ) 30 | } 31 | } 32 | 33 | @Transactional(propagation = Propagation.NEVER) 34 | fun noTransactionFun(){ 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/service/application/MemberSave.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service.application 2 | 3 | import com.example.kopring.common.support.jpa.findByIdOrThrow 4 | import com.example.kopring.member.domain.Member 5 | import com.example.kopring.member.domain.MemberRepository 6 | import com.example.kopring.member.event.DomainEventPublisher 7 | import org.springframework.stereotype.Component 8 | import org.springframework.transaction.annotation.Propagation 9 | import org.springframework.transaction.annotation.Transactional 10 | import org.springframework.transaction.event.TransactionPhase 11 | import org.springframework.transaction.event.TransactionalEventListener 12 | 13 | @Transactional 14 | @Component 15 | class MemberSave( 16 | private val memberRepository: MemberRepository, 17 | ) { 18 | fun save(req: Member.Request): Member { 19 | val member = Member(name = req.name).let(memberRepository::save) 20 | 21 | // member 가 저장했을때 이벤트 발생 22 | DomainEventPublisher.publishEvent( 23 | MemberSaveEvent(memberId = member.id) 24 | ) 25 | 26 | return member 27 | } 28 | } 29 | 30 | data class MemberSaveEvent( 31 | val memberId: Long, 32 | ) 33 | 34 | @Component 35 | class MemberSaveEventHandler( 36 | private val memberRepository: MemberRepository, 37 | ) { 38 | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 앞 작업에서의 커밋이 종료된뒤 처리 39 | @Transactional(propagation = Propagation.REQUIRES_NEW) // 앞에서 트랜잭션이 완료되어 버렸기 떄문에, 여기서 변경작업을 하려면 새로운 트랜잭션을 열어야함. 40 | fun onSave(memberSaveEvent: MemberSaveEvent) { 41 | 42 | memberRepository.findByIdOrThrow(memberSaveEvent.memberId).run { 43 | this.changeName(name = "${this.name} event") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/member/service/query/MemberFind.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service.query 2 | 3 | import com.example.kopring.common.support.jpa.findByIdOrThrow 4 | import com.example.kopring.member.domain.Member 5 | import com.example.kopring.member.domain.MemberRepository 6 | import org.springframework.stereotype.Component 7 | import org.springframework.transaction.annotation.Transactional 8 | 9 | @Transactional(readOnly = true) 10 | @Component 11 | class MemberFind( 12 | private val memberRepository: MemberRepository, 13 | ) { 14 | fun findById(id: Long): Member { 15 | return memberRepository.findByIdOrThrow(id = id) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/members/domain/AbstractMember.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.members.domain 2 | 3 | import com.example.kopring.BaseEntity 4 | import jakarta.persistence.* 5 | 6 | @Entity(name = "abstract_member") 7 | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 8 | @DiscriminatorColumn(name = "DTYPE") 9 | abstract class AbstractMember( 10 | @Column(name = "name") 11 | val name: String, 12 | 13 | @Column(name = "id") 14 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | val id: Long = 0L 16 | ) : BaseEntity() { 17 | abstract fun isBlackList(): Boolean 18 | } 19 | 20 | @Entity 21 | @DiscriminatorValue("BLACK") 22 | class BMember(name: String) : AbstractMember(name = name) { 23 | override fun isBlackList(): Boolean { 24 | return true 25 | } 26 | } 27 | 28 | @Entity 29 | @DiscriminatorValue("WHITE") 30 | class WMember(name: String) : AbstractMember(name = name) { 31 | override fun isBlackList(): Boolean { 32 | return false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /kopring/src/main/kotlin/com/example/kopring/members/domain/AbstractMemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.members.domain 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface AbstractMemberRepository : JpaRepository 6 | 7 | interface BMemberRepository : JpaRepository 8 | 9 | interface WMemberRepository : JpaRepository 10 | -------------------------------------------------------------------------------- /kopring/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # application.yml 2 | 3 | # ──────────────────────────────── 4 | # 1) 공통 설정 (프로파일 상관없이 항상 로드) 5 | spring: 6 | profiles: 7 | active: h2 # 기본 활성 프로파일 8 | data: 9 | redis: 10 | host: "localhost" # Spring Boot 3.x부터는 spring.data.redis로 설정합니다. 11 | port: 6379 # default port 12 | h2: 13 | console: 14 | enabled: true 15 | path: "/h2-console" 16 | logging: 17 | level: 18 | p6spy: debug 19 | com.p6spy.engine.spy: debug 20 | decorator: 21 | datasource: 22 | p6spy: 23 | enable-logging: true 24 | multiline: true 25 | logging: slf4j 26 | 27 | --- 28 | 29 | # ──────────────────────────────── 30 | # 2) H2 프로파일 31 | spring: 32 | config: 33 | activate: 34 | on-profile: h2 35 | datasource: 36 | url: "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" 37 | driver-class-name: "org.h2.Driver" 38 | username: "sa" 39 | password: "" 40 | jpa: 41 | hibernate: 42 | ddl-auto: "create" 43 | properties: 44 | hibernate: 45 | dialect: "org.hibernate.dialect.H2Dialect" 46 | format_sql: true 47 | 48 | --- 49 | 50 | # ──────────────────────────────── 51 | # 3) mariadb 프로파일 52 | spring: 53 | config: 54 | activate: 55 | on-profile: mysql 56 | datasource: 57 | url: "jdbc:mariadb://localhost:13306/unluckyjung?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" 58 | driver-class-name: org.mariadb.jdbc.Driver # ← MariaDB 드라이버 59 | username: "root" 60 | password: "asdf" 61 | jpa: 62 | hibernate: 63 | ddl-auto: "create" 64 | properties: 65 | hibernate: 66 | dialect: org.hibernate.dialect.MariaDBDialect 67 | format_sql: true 68 | show-sql: true 69 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/KopringApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | // @ActiveProfiles("mysql") // mysql 로 띄우고 싶은경우 활성화. 7 | @SpringBootTest 8 | class KopringApplicationTests { 9 | 10 | @Test 11 | fun contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/common/support/redis/RedisKeyValueStoreTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.common.support.redis 2 | 3 | import com.example.kopring.test.IntegrationTest 4 | import io.kotest.matchers.shouldBe 5 | import org.junit.jupiter.api.AfterEach 6 | import org.junit.jupiter.api.Assertions.* 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.data.redis.core.RedisTemplate 11 | import java.util.concurrent.TimeUnit 12 | 13 | @IntegrationTest 14 | internal class RedisKeyValueStoreTest( 15 | private val redisKeyValueStore: RedisKeyValueStore, 16 | private val redisTemplate: RedisTemplate, 17 | ) { 18 | @BeforeEach 19 | internal fun setUp() { 20 | flushRedis() 21 | } 22 | 23 | @AfterEach 24 | internal fun tearDown() { 25 | redisTemplate.connectionFactory?.connection?.flushAll() 26 | } 27 | 28 | private fun flushRedis() { 29 | redisTemplate.connectionFactory?.connection?.flushAll() 30 | } 31 | 32 | @DisplayName("[] 연산자를 이용해서 get, set 이 가능하다.") 33 | @Test 34 | fun operatorTest() { 35 | val key = "yoonsung" 36 | val value = "goodall" 37 | redisKeyValueStore[key] = value 38 | 39 | val result = redisKeyValueStore[key, String::class.java] 40 | result shouldBe value 41 | } 42 | 43 | @DisplayName("object 를 String 으로 변환해서 저장, 조회할 수 있다.") 44 | @Test 45 | fun ttlTest() { 46 | val key = "yoonsung" 47 | val value = UnluckyJung(name = "goodall", age = 30) 48 | redisKeyValueStore[key] = value 49 | 50 | val result = redisKeyValueStore[key, UnluckyJung::class.java] 51 | result shouldBe value 52 | } 53 | 54 | @DisplayName("key 에 따른 3초후 삭제 옵션을 주면, 3초후 조회되지 않는다.") 55 | @Test 56 | fun redisDeleteTTLTest() { 57 | val key = "expireKey" 58 | val value = "goodall" 59 | 60 | redisKeyValueStore.save( 61 | key = key, 62 | value = value, 63 | timeOut = RedisKeyValueTimeOut(time = 3L, TimeUnit.SECONDS), 64 | ) 65 | 66 | redisKeyValueStore[key, String::class.java] shouldBe value 67 | 68 | Thread.sleep(3500) 69 | redisKeyValueStore[key, String::class.java] shouldBe null 70 | } 71 | } 72 | 73 | data class UnluckyJung( 74 | val name: String, 75 | val age: Int, 76 | ) 77 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/domain/MemberDomainEventTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import com.example.kopring.common.support.jpa.findByIdOrThrow 4 | import com.example.kopring.member.event.DomainEventPublisher 5 | import com.example.kopring.member.service.application.MemberSave 6 | import com.example.kopring.member.service.application.MemberSaveEvent 7 | import com.example.kopring.test.IntegrationTest 8 | import io.kotest.matchers.shouldBe 9 | import org.junit.jupiter.api.DisplayName 10 | import org.junit.jupiter.api.Test 11 | import org.springframework.test.context.transaction.TestTransaction 12 | 13 | @IntegrationTest 14 | class MemberDomainEventTest( 15 | val memberSave: MemberSave, 16 | val memberRepository: MemberRepository, 17 | ) { 18 | 19 | @DisplayName("도메인 이벤트 발생시키면, 이벤트 리스너에서 받아서 처리된다.") 20 | @Test 21 | fun memberSaveDomainEventTest() { 22 | val beforeName = "yoonsung" 23 | val afterName = "$beforeName event" 24 | 25 | TestTransaction.flagForCommit() 26 | 27 | val member = memberSave.save(Member.Request(name = beforeName)) 28 | memberRepository.findByIdOrThrow(member.id).name shouldBe beforeName 29 | 30 | // 트랜잭션 종료, @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 수행시킴 31 | TestTransaction.end() 32 | 33 | val afterMember = memberRepository.findByIdOrThrow(member.id) 34 | afterMember.name shouldBe afterName 35 | 36 | // 메모리 DB가 아닌경우 새로운 트랜잭션에서의 저장이라, 롤백이 안되므로 명시적으로 삭제해줘야함. 37 | memberRepository.delete(afterMember) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/domain/MemberRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import com.example.kopring.common.support.jpa.findByIdOrThrow 4 | import com.example.kopring.test.RepositoryTest 5 | import io.kotest.assertions.throwables.shouldThrowExactly 6 | import io.kotest.matchers.shouldBe 7 | import io.kotest.matchers.shouldNotBe 8 | import io.mockk.every 9 | import io.mockk.mockkStatic 10 | import io.mockk.spyk 11 | import org.junit.jupiter.api.BeforeEach 12 | import org.junit.jupiter.api.DisplayName 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.assertThrows 15 | import org.junit.jupiter.params.ParameterizedTest 16 | import org.junit.jupiter.params.provider.ValueSource 17 | import java.time.LocalDateTime 18 | import java.time.ZoneId 19 | import java.time.ZonedDateTime 20 | 21 | @RepositoryTest 22 | class MemberRepositoryTest( 23 | private val memberRepository: MemberRepository 24 | ) { 25 | 26 | lateinit var member: Member 27 | 28 | @BeforeEach 29 | internal fun setUp() { 30 | member = memberRepository.save( 31 | Member("goodall") 32 | ) 33 | } 34 | 35 | @DisplayName("멤버가 최초로 저장되면, id로 조회했을때 조회된다.") 36 | @Test 37 | fun memberGetByIdTest() { 38 | val member1 = memberRepository.findByMemberId(member.id) 39 | member1 shouldBe member 40 | } 41 | 42 | @DisplayName("존재하지 않는 id로 조회하면, 예외가 발생한다.") 43 | @Test 44 | fun memberGetByIdFailTest() { 45 | assertThrows { 46 | memberRepository.findByMemberId(100L) 47 | } 48 | } 49 | 50 | @DisplayName("존재하지 않는 id로 조회하면, 예외가 발생한다2") 51 | @Test 52 | fun findByIdOrThrowTest() { 53 | shouldThrowExactly { 54 | memberRepository.findByIdOrThrow(100L) 55 | } 56 | } 57 | 58 | @DisplayName("외부에서 createdAt 시간을 주입해주면, 현재 시간을 무시하고 주입받은 시간으로 createdAt 시간이 설정된다.") 59 | @Test 60 | fun timeDependencyInjectionTest() { 61 | val inputKoreaTime = ZonedDateTime.of( 62 | LocalDateTime.of(2222, 2, 2, 2, 2), ZoneId.of("Asia/Seoul") 63 | ) 64 | val member1 = Member( 65 | "jys", 66 | createdAt = inputKoreaTime 67 | ) 68 | 69 | member1.createdAt shouldNotBe ZonedDateTime.now() 70 | member1.createdAt shouldBe inputKoreaTime 71 | } 72 | 73 | @DisplayName("prePersist() 메소드를 spyk 를 이용하여 mocking 하면, 영속화가 되어도 모킹한 시간이 유지된다.") 74 | @Test 75 | fun spyMockTest() { 76 | val inputKoreaTime = ZonedDateTime.of( 77 | LocalDateTime.of(2222, 2, 2, 2, 2), ZoneId.of("Asia/Seoul") 78 | ) 79 | 80 | val spyMember: Member = spyk(Member("jys")) { 81 | every { prePersist() } answers { 82 | createdAt = inputKoreaTime 83 | } 84 | } 85 | val savedMember = memberRepository.save(spyMember) // inputKoreaTime이 아닌, now()로 시간이 저장된다. 86 | 87 | savedMember.createdAt shouldBe inputKoreaTime 88 | } 89 | 90 | @DisplayName("ZonedDateTime.now()를 static mocking 이용해 시간을 더해주면, 더해준 시간으로 반환된다.") 91 | @ParameterizedTest 92 | @ValueSource(longs = [1, 2, 3, 4]) 93 | fun zoneDateTimeMockingTest(plusDay: Long) { 94 | val inputKoreaTime = ZonedDateTime.of( 95 | LocalDateTime.of(2222, 2, 2, 2, 2), ZoneId.of("Asia/Seoul") 96 | ) 97 | 98 | mockkStatic(ZonedDateTime::class) 99 | every { 100 | ZonedDateTime.now() 101 | }.returns(inputKoreaTime.plusDays(plusDay)) 102 | val savedMember = memberRepository.save(Member("unluckyjung")) 103 | 104 | savedMember.createdAt shouldBe inputKoreaTime.plusDays(plusDay) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/domain/MemberTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.domain 2 | 3 | import io.kotest.matchers.shouldBe 4 | import io.kotest.matchers.string.shouldMatch 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | import jakarta.validation.Validation 9 | 10 | class MemberTest { 11 | 12 | @DisplayName("Request 테스트") 13 | @Nested 14 | inner class RequestTest { 15 | private val validator = Validation.buildDefaultValidatorFactory().validator 16 | 17 | @DisplayName("이름이 공백이면 Validation 의 validator 에 예외가 쌓인다.") 18 | @Test 19 | fun nameBlankFailTest() { 20 | 21 | val memberReq = Member.Request(" ") 22 | val violations = validator.validate(memberReq) 23 | 24 | violations.size shouldBe 1 25 | 26 | for (failMsg in violations) { 27 | failMsg.message shouldMatch ("이름은 공백으로 이루어져있을 수 없습니다.") 28 | } 29 | } 30 | 31 | @DisplayName("이름이 공백으로 이루어있지 않으면, 예외가 쌓이지 않는다.") 32 | @Test 33 | fun nameBlankTest() { 34 | 35 | val memberReq = Member.Request("jys") 36 | val violations = validator.validate(memberReq) 37 | 38 | violations.size shouldBe 0 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/service/CallerServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import com.example.kopring.test.IntegrationTest 4 | import org.junit.jupiter.api.Test 5 | 6 | @IntegrationTest 7 | class CallerServiceTest( 8 | private val callerService: CallerService, 9 | ){ 10 | @Test 11 | fun test(){ 12 | callerService.runConstructor() 13 | callerService.runAutowired() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/service/KtFactoryMethodPatternTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import com.example.kopring.member.domain.Member 4 | import com.example.kopring.member.domain.MemberRepository 5 | import com.example.kopring.test.IntegrationTest 6 | import io.kotest.matchers.shouldBe 7 | import org.junit.jupiter.api.Test 8 | 9 | @IntegrationTest 10 | class KtFactoryMethodPatternTest( 11 | private val memberRepository: MemberRepository, 12 | ) { 13 | @Test 14 | fun factoryMethodPatternTest() { 15 | Member(name = "jys").run(memberRepository::save) 16 | 17 | KtFactoryMethodPattern.getMemberList().size shouldBe 1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/service/MemberServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import io.kotest.assertions.throwables.shouldThrowExactly 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.boot.test.context.SpringBootTest 6 | import org.springframework.test.context.TestConstructor 7 | import org.springframework.transaction.IllegalTransactionStateException 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 11 | @SpringBootTest 12 | internal class MemberServiceTest( 13 | private val memberService: MemberService, 14 | ) { 15 | 16 | @Transactional 17 | @Test 18 | fun noTransactionFun() { 19 | shouldThrowExactly { 20 | memberService.noTransactionFun() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/member/service/RetryableServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.member.service 2 | 3 | import com.example.kopring.RetryCounter 4 | import com.example.kopring.RetryService 5 | import com.example.kopring.test.IntegrationTest 6 | import io.kotest.assertions.throwables.shouldNotThrowAny 7 | import io.kotest.assertions.throwables.shouldThrowExactly 8 | import io.kotest.matchers.shouldBe 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.DisplayName 11 | import org.junit.jupiter.api.Test 12 | 13 | @IntegrationTest 14 | class RetryableServiceTest( 15 | private val retryService: RetryService, 16 | ) { 17 | @BeforeEach 18 | internal fun setUp() { 19 | RetryCounter.counter = 0 20 | } 21 | 22 | @DisplayName("3회 초과하도록 실패하면 예외가 발생한다.") 23 | @Test 24 | fun retryTest1() { 25 | shouldThrowExactly { 26 | retryService.retryFun(pivotCount = 4) 27 | } 28 | } 29 | 30 | @DisplayName("3회 까지는 재시도를 한뒤 success 를 응답한다.") 31 | @Test 32 | fun retryTest2() { 33 | shouldNotThrowAny { 34 | retryService.retryFun(pivotCount = 3) shouldBe "success" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/members/domain/AbstractMemberRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.members.domain 2 | 3 | import com.example.kopring.test.IntegrationTest 4 | import io.kotest.matchers.shouldBe 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Test 7 | 8 | @IntegrationTest 9 | internal class AbstractMemberRepositoryTest( 10 | private val abstractMemberRepository: AbstractMemberRepository, 11 | private val bMemberRepository: BMemberRepository, 12 | private val wMemberRepository: WMemberRepository, 13 | ) { 14 | @DisplayName("저장은 DTYPE 타입 구분없이 가능하다. 조회시에는 레포지토리 별로 DTYPE 조건을 보고 쿼리 필터가 되어 조회된다.") 15 | @Test 16 | fun saveTest() { 17 | val whiteMemberName = "goodall" 18 | val blackMemberName = "unluckyjung" 19 | 20 | abstractMemberRepository.save(WMember(name = whiteMemberName)) 21 | abstractMemberRepository.save(BMember(name = blackMemberName)) 22 | 23 | val result = abstractMemberRepository.findAll() 24 | result.size shouldBe 2 25 | result[0].name shouldBe whiteMemberName 26 | result[1].name shouldBe blackMemberName 27 | 28 | 29 | val wResult = wMemberRepository.findAll() 30 | wResult.size shouldBe 1 31 | wResult[0].name shouldBe whiteMemberName 32 | wResult[0].isBlackList() shouldBe false 33 | 34 | 35 | val bResult = bMemberRepository.findAll() 36 | bResult.size shouldBe 1 37 | bResult[0].name shouldBe blackMemberName 38 | bResult[0].isBlackList() shouldBe true 39 | } 40 | 41 | @DisplayName("sealed class 을 이용해 타입 구분을 쉽게할 수 있다.") 42 | @Test 43 | fun sealedClassType() { 44 | val whiteMemberName = "goodall" 45 | val blackMemberName = "unluckyjung" 46 | 47 | abstractMemberRepository.save(WMember(name = whiteMemberName)) 48 | abstractMemberRepository.save(BMember(name = blackMemberName)) 49 | 50 | val result = abstractMemberRepository.findAll() 51 | 52 | result.forEach { 53 | when (it) { 54 | is WMember -> { 55 | it.name shouldBe whiteMemberName 56 | it.isBlackList() shouldBe false 57 | } 58 | 59 | is BMember -> { 60 | it.name shouldBe blackMemberName 61 | it.isBlackList() shouldBe true 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/test/BaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.test 2 | 3 | import io.mockk.junit5.MockKExtension 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.test.context.TestConstructor 8 | import org.springframework.test.context.TestExecutionListeners 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 14 | annotation class TestEnvironment 15 | 16 | @Target(AnnotationTarget.CLASS) 17 | @Retention(AnnotationRetention.RUNTIME) 18 | @ExtendWith(MockKExtension::class) 19 | annotation class UnitTest 20 | 21 | @Target(AnnotationTarget.CLASS) 22 | @Retention(AnnotationRetention.RUNTIME) 23 | @DataJpaTest 24 | @TestEnvironment 25 | annotation class RepositoryTest 26 | 27 | @Target(AnnotationTarget.CLASS) 28 | @Retention(AnnotationRetention.RUNTIME) 29 | @Transactional 30 | @SpringBootTest 31 | @TestEnvironment 32 | annotation class IntegrationTest 33 | 34 | @Target(AnnotationTarget.CLASS) 35 | @Retention(AnnotationRetention.RUNTIME) 36 | @TestExecutionListeners( 37 | value = [IntegrationTestExecuteListener::class], 38 | mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS 39 | ) 40 | annotation class TruncateAllTables 41 | 42 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/test/IntegrationTestExecuteListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.test 2 | 3 | import org.springframework.jdbc.core.JdbcTemplate 4 | import org.springframework.test.context.TestContext 5 | import org.springframework.test.context.support.AbstractTestExecutionListener 6 | import org.springframework.test.jdbc.JdbcTestUtils 7 | import org.springframework.transaction.TransactionStatus 8 | import org.springframework.transaction.support.TransactionCallbackWithoutResult 9 | import org.springframework.transaction.support.TransactionTemplate 10 | import java.sql.DatabaseMetaData 11 | import java.sql.SQLException 12 | 13 | 14 | class IntegrationTestExecuteListener : AbstractTestExecutionListener() { 15 | 16 | override fun beforeTestMethod(testContext: TestContext) { 17 | val jdbcTemplate = getJdbcTemplate(testContext) 18 | val transactionTemplate = getTransactionTemplate(testContext) 19 | 20 | truncateAllTables(jdbcTemplate, transactionTemplate) 21 | } 22 | 23 | override fun afterTestMethod(testContext: TestContext) { 24 | val jdbcTemplate = getJdbcTemplate(testContext) 25 | val transactionTemplate = getTransactionTemplate(testContext) 26 | truncateAllTables(jdbcTemplate, transactionTemplate) 27 | } 28 | 29 | private fun getTransactionTemplate(testContext: TestContext): TransactionTemplate { 30 | return testContext.applicationContext.getBean(TransactionTemplate::class.java) 31 | } 32 | 33 | private fun getJdbcTemplate(testContext: TestContext): JdbcTemplate { 34 | return testContext.applicationContext.getBean(JdbcTemplate::class.java) 35 | } 36 | 37 | private fun truncateAllTables(jdbcTemplate: JdbcTemplate, transactionTemplate: TransactionTemplate) { 38 | transactionTemplate.execute(object : TransactionCallbackWithoutResult() { 39 | override fun doInTransactionWithoutResult(status: TransactionStatus) { 40 | jdbcTemplate.execute("set FOREIGN_KEY_CHECKS = 0;") 41 | JdbcTestUtils.deleteFromTables(jdbcTemplate, *getAllTables(jdbcTemplate).toTypedArray()) 42 | jdbcTemplate.execute("set FOREIGN_KEY_CHECKS = 1;") 43 | } 44 | }) 45 | } 46 | 47 | private fun getAllTables(jdbcTemplate: JdbcTemplate): List { 48 | try { 49 | jdbcTemplate.dataSource?.connection.use { connection -> 50 | val metaData: DatabaseMetaData = connection!!.metaData 51 | val tables: MutableList = ArrayList() 52 | metaData.getTables(null, null, null, arrayOf("TABLE")).use { resultSet -> 53 | while (resultSet.next()) { 54 | tables.add(resultSet.getString("TABLE_NAME")) 55 | } 56 | } 57 | return tables.filter { NOT_DELETE_TABLES.contains(it).not() } 58 | } 59 | } catch (exception: SQLException) { 60 | throw IllegalStateException(exception) 61 | } 62 | } 63 | 64 | companion object { 65 | private val NOT_DELETE_TABLES = setOf("flyway_schema_history") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /kopring/src/test/kotlin/com/example/kopring/test/TruncateTablesTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.kopring.test 2 | 3 | import com.example.kopring.member.domain.TeamRepository 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.matchers.shouldNotBe 6 | import org.junit.jupiter.api.Test 7 | 8 | @IntegrationTest 9 | @TruncateAllTables 10 | class TruncateTablesTest( 11 | private val teamRepository: TeamRepository, 12 | ) { 13 | @Test 14 | fun truncateTest() { 15 | teamRepository.findAll() shouldBe emptyList() 16 | } 17 | } 18 | 19 | @IntegrationTest 20 | class TruncateTablesTest2( 21 | private val teamRepository: TeamRepository, 22 | ) { 23 | @Test 24 | fun truncateTest() { 25 | teamRepository.findAll().size shouldNotBe 0 26 | } 27 | } 28 | --------------------------------------------------------------------------------