Compare commits
43 Commits
jackson
...
spring-clo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f738bbfba7 | ||
|
|
78353d594a | ||
|
|
2a8480d2f4 | ||
|
|
2bf88262b7 | ||
|
|
ad702891b0 | ||
|
|
41fa3c2f6c | ||
|
|
d00a697f50 | ||
|
|
e990448335 | ||
|
|
5472392ccc | ||
|
|
124473a129 | ||
|
|
a91fb579cf | ||
|
|
8a0df3a322 | ||
|
|
6aa4913294 | ||
|
|
e05abb53f1 | ||
|
|
c659243c32 | ||
|
|
a52e454a26 | ||
|
|
c5e5a3047b | ||
|
|
0f4de6fa3d | ||
|
|
c611ebd226 | ||
|
|
0ee99149df | ||
|
|
9053fd087d | ||
|
|
409b3e4ae1 | ||
|
|
2aba6999bd | ||
|
|
58b8cf6d10 | ||
|
|
7071770951 | ||
|
|
483df22623 | ||
|
|
b2d330db7d | ||
|
|
a3fcd21c29 | ||
|
|
c00f20cbc3 | ||
|
|
a35fe9c2c9 | ||
|
|
e99536f663 | ||
|
|
7bfd6ed19c | ||
|
|
9ff7046f38 | ||
|
|
ed852ff461 | ||
|
|
cc1bd398fb | ||
|
|
e80e676546 | ||
|
|
ba26a7d138 | ||
|
|
bb1945b669 | ||
|
|
16512c7d81 | ||
|
|
b9c7cb13cb | ||
|
|
18fe7b9fc2 | ||
|
|
512960493f | ||
|
|
1a7b52c5b0 |
37
놀이터(예제 코드 작성)/graphql-kotlin/.gitignore
vendored
Normal file
37
놀이터(예제 코드 작성)/graphql-kotlin/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
14
놀이터(예제 코드 작성)/graphql-kotlin/README.md
Normal file
14
놀이터(예제 코드 작성)/graphql-kotlin/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# GraphQL Kotlin
|
||||
|
||||
- [MVN Repository - GraphQL Kotlin Spring Server](https://mvnrepository.com/artifact/com.expediagroup/graphql-kotlin-spring-server)
|
||||
|
||||
## Spring Server
|
||||
|
||||
- [Getting Started](https://opensource.expediagroup.com/graphql-kotlin/docs/)
|
||||
- [Spring Server Overview](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-overview/)
|
||||
- [Writing Schemas with Spring](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-schema)
|
||||
- [Generating GraphQL Context](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-graphql-context)
|
||||
- [HTTP Request and Response](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-http-request-response)
|
||||
- [Automatically Created Beans](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-beans)
|
||||
- [Configuration Properties](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-properties)
|
||||
- [Subscriptions](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-subscriptions)
|
||||
41
놀이터(예제 코드 작성)/graphql-kotlin/build.gradle.kts
Normal file
41
놀이터(예제 코드 작성)/graphql-kotlin/build.gradle.kts
Normal file
@@ -0,0 +1,41 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.7.2"
|
||||
id("io.spring.dependency-management") version "1.0.12.RELEASE"
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("plugin.spring") version "1.6.21"
|
||||
}
|
||||
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("io.projectreactor:reactor-test")
|
||||
|
||||
implementation("com.expediagroup", "graphql-kotlin-spring-server", "6.0.0")
|
||||
implementation("com.graphql-java:graphql-java-extended-scalars:18.1")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
BIN
놀이터(예제 코드 작성)/graphql-kotlin/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
놀이터(예제 코드 작성)/graphql-kotlin/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
놀이터(예제 코드 작성)/graphql-kotlin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
놀이터(예제 코드 작성)/graphql-kotlin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
240
놀이터(예제 코드 작성)/graphql-kotlin/gradlew
vendored
Executable file
240
놀이터(예제 코드 작성)/graphql-kotlin/gradlew
vendored
Executable file
@@ -0,0 +1,240 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
91
놀이터(예제 코드 작성)/graphql-kotlin/gradlew.bat
vendored
Normal file
91
놀이터(예제 코드 작성)/graphql-kotlin/gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
놀이터(예제 코드 작성)/graphql-kotlin/settings.gradle.kts
Normal file
1
놀이터(예제 코드 작성)/graphql-kotlin/settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = "graphql-kotlin"
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.banjjoknim.graphqlkotlin
|
||||
|
||||
import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.server.ServerRequest
|
||||
|
||||
/**
|
||||
* # [Generating GraphQL Context](https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-graphql-context)
|
||||
*
|
||||
* graphql-kotlin-spring-server provides a Spring specific implementation of GraphQLContextFactory and the context.
|
||||
*
|
||||
* SpringGraphQLContext (deprecated) - Implements the Spring ServerRequest and federation tracing HTTPRequestHeaders
|
||||
*
|
||||
* SpringGraphQLContextFactory - Generates GraphQL context map with federated tracing information per request
|
||||
*
|
||||
* If you are using graphql-kotlin-spring-server, you should extend DefaultSpringGraphQLContextFactory to automatically support federated tracing.
|
||||
*
|
||||
* Once your application is configured to build your custom GraphQL context map, you can then access it through a data fetching environment argument.
|
||||
*
|
||||
* While executing the query, data fetching environment will be automatically injected to the function input arguments.
|
||||
*
|
||||
* This argument will not appear in the GraphQL schema.
|
||||
*/
|
||||
@Component
|
||||
class GraphQLContextFactory : DefaultSpringGraphQLContextFactory() {
|
||||
override suspend fun generateContextMap(request: ServerRequest): Map<*, Any> {
|
||||
return super.generateContextMap(request) + mapOf(
|
||||
"myCustomValue" to (request.headers().firstHeader("MyHeader") ?: "defaultContext")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.banjjoknim.graphqlkotlin
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class GraphqlKotlinApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<GraphqlKotlinApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.banjjoknim.graphqlkotlin.configuration
|
||||
|
||||
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
|
||||
import graphql.language.StringValue
|
||||
import graphql.scalars.ExtendedScalars
|
||||
import graphql.scalars.util.Kit.typeName
|
||||
import graphql.schema.Coercing
|
||||
import graphql.schema.CoercingParseLiteralException
|
||||
import graphql.schema.CoercingParseValueException
|
||||
import graphql.schema.CoercingSerializeException
|
||||
import graphql.schema.GraphQLScalarType
|
||||
import graphql.schema.GraphQLType
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
|
||||
/**
|
||||
* - [Extended Scalars for graphql-java](https://github.com/graphql-java/graphql-java-extended-scalars)
|
||||
*
|
||||
* - [Cannot use java.util.Date](https://github.com/ExpediaGroup/graphql-kotlin/discussions/1198)
|
||||
*
|
||||
* - [GraphQL Kotlin - Extended Scalars](https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/writing-schemas/scalars/#common-issues)
|
||||
*
|
||||
* - [GraphQL Kotlin - Generator Configuration & Hooks](https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/customizing-schemas/generator-config)
|
||||
*/
|
||||
@Configuration
|
||||
class ExtendedScalarsConfiguration {
|
||||
/**
|
||||
* 아래와 같이 Bean으로 Hook을 등록해주면 Schema Generator가 Schema를 생성할 때 이 Bean에 정의된 Hook을 이용해서 Schema를 만든다.
|
||||
*/
|
||||
@Bean
|
||||
fun extendedScalarsHooks(): SchemaGeneratorHooks {
|
||||
return object : SchemaGeneratorHooks {
|
||||
override fun willGenerateGraphQLType(type: KType): GraphQLType? {
|
||||
return when (type.classifier as? KClass<*>) {
|
||||
Long::class -> ExtendedScalars.GraphQLLong
|
||||
LocalDateTime::class -> localDateTimeScalar()
|
||||
LocalTime::class -> ExtendedScalars.LocalTime
|
||||
LocalDate::class -> ExtendedScalars.Date
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean으로 ScalarType을 등록해주지 않으면 어플리케이션 실행시 스키마를 구성하는 단계(스키마에 포함될 타입중에서 LocalDateTime 이 포함되어 있는 경우)에서 아래와 같은 예외가 발생한다.
|
||||
*
|
||||
* ```
|
||||
* graphql.AssertException: All types within a GraphQL schema must have unique names. No two provided types may have the same name.
|
||||
* No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types). You have redefined the type 'LocalDateTime' from being a 'GraphQLScalarType' to a 'GraphQLScalarType'
|
||||
* ```
|
||||
*
|
||||
* @see graphql.scalars.datetime.DateTimeScalar
|
||||
*/
|
||||
@Bean
|
||||
fun localDateTimeScalar(): GraphQLScalarType? {
|
||||
val coercing = object : Coercing<LocalDateTime, String> {
|
||||
override fun serialize(dataFetcherResult: Any): String {
|
||||
return when (dataFetcherResult) {
|
||||
is LocalDateTime -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
|
||||
LocalDateTime.from(dataFetcherResult)
|
||||
)
|
||||
is String -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
|
||||
LocalDateTime.parse(dataFetcherResult)
|
||||
)
|
||||
else -> throw CoercingSerializeException(
|
||||
"Expected something we can convert to 'java.time.LocalDateTime' but was '" +
|
||||
"${typeName(dataFetcherResult)}'."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseValue(input: Any): LocalDateTime {
|
||||
return when (input) {
|
||||
is LocalDateTime -> input
|
||||
is String -> LocalDateTime.parse(
|
||||
input.toString(),
|
||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||
)
|
||||
else -> throw CoercingParseValueException(
|
||||
"Expected a 'String' but was '" + "${typeName(input)}'."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseLiteral(input: Any): LocalDateTime {
|
||||
if (input !is StringValue) {
|
||||
throw CoercingParseLiteralException(
|
||||
"Expected AST type 'StringValue' but was '${typeName(input)}'."
|
||||
)
|
||||
}
|
||||
return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
}
|
||||
}
|
||||
return GraphQLScalarType.newScalar()
|
||||
.name("LocalDateTime")
|
||||
.description("Custom LocalDateTime Scalar")
|
||||
.coercing(coercing)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import com.expediagroup.graphql.server.Schema
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* In order to expose your schema directives, queries, mutations, and subscriptions in the GraphQL schema create beans that implement the corresponding marker interface and they will be automatically picked up by graphql-kotlin-spring-server auto-configuration library.
|
||||
*/
|
||||
@GraphQLDescription("Sample GraphQL Schema")
|
||||
@Component
|
||||
class GraphQLSchema : Schema
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class Person(
|
||||
var name: String,
|
||||
var age: Long? = 0L,
|
||||
var birthDate: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import com.expediagroup.graphql.server.operations.Mutation
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class PersonMutation : Mutation {
|
||||
|
||||
fun changeName(person: Person, newName: String): Person {
|
||||
return person.apply {
|
||||
name = newName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLName
|
||||
import com.expediagroup.graphql.server.operations.Query
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
import kotlin.random.Random
|
||||
|
||||
@Component
|
||||
class PersonQuery(
|
||||
/**
|
||||
* # Spring Beans
|
||||
*
|
||||
* Since the top level objects are Spring components, Spring will automatically autowire dependent beans as normal.
|
||||
*
|
||||
* Refer to [Spring Documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/) for details.
|
||||
*/
|
||||
private val personRepository: PersonRepository
|
||||
) : Query {
|
||||
|
||||
@GraphQLDescription("get Person Instance")
|
||||
fun getPerson(name: String): Person = Person(name)
|
||||
|
||||
/**
|
||||
* # Spring Beans in Arguments
|
||||
*
|
||||
* graphql-kotlin-spring-server provides Spring-aware data fetcher that automatically autowires Spring beans when they are specified as function arguments.
|
||||
*
|
||||
* `@Autowired` arguments should be explicitly excluded from the GraphQL schema by also specifying @GraphQLIgnore.
|
||||
*
|
||||
* ```
|
||||
* NOTE
|
||||
* If you are using custom data fetcher make sure that you extend SpringDataFetcher instead of the base FunctionDataFetcher to keep this functionallity.
|
||||
* ```
|
||||
*/
|
||||
@GraphQLDescription("find Person Instance")
|
||||
fun findPerson(@GraphQLIgnore @Autowired personRepository: PersonRepository, name: String): Person? {
|
||||
return personRepository.findPerson(name)
|
||||
}
|
||||
|
||||
@GraphQLDescription("@GraphQLName example")
|
||||
@GraphQLName("somePerson")
|
||||
fun randomPerson(name: String): Person = Person(name = name, age = Random.nextLong())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
interface PersonRepository {
|
||||
|
||||
fun findPerson(name: String): Person?
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class PersonRepositoryImpl : PersonRepository {
|
||||
|
||||
companion object {
|
||||
private val people = mapOf(
|
||||
"banjjoknim" to Person("banjjoknim"),
|
||||
"colt" to Person("colt")
|
||||
)
|
||||
}
|
||||
|
||||
override fun findPerson(name: String): Person? {
|
||||
return people[name]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import com.expediagroup.graphql.server.operations.Subscription
|
||||
import org.reactivestreams.Publisher
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class PersonSubscription : Subscription {
|
||||
|
||||
fun changeName(person: Person, newName: String): Publisher<Person> {
|
||||
return Publisher { println("change name published") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# At a minimum, in order for graphql-kotlin-spring-server to automatically configure your GraphQL web server
|
||||
#
|
||||
# you need to specify a list of supported packages that can be scanned for exposing your schema objects through reflections.
|
||||
#
|
||||
# You can do this through the spring application config or by overriding the SchemaGeneratorConfig bean.
|
||||
#
|
||||
# See customization below.
|
||||
graphql:
|
||||
packages:
|
||||
- "com.banjjoknim.graphqlkotlin"
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.graphqlkotlin
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class GraphqlKotlinApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.banjjoknim.graphqlkotlin.person
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class PersonQueryTest(
|
||||
@Autowired
|
||||
private val webTestClient: WebTestClient
|
||||
) {
|
||||
|
||||
@DisplayName("getPerson Query Tests")
|
||||
@Nested
|
||||
inner class GetPersonTestCases {
|
||||
@Test
|
||||
fun `인자로 넣은 이름을 가진 Person 객체를 얻는다`() {
|
||||
val query = """
|
||||
query {
|
||||
getPerson(name: "colt") {
|
||||
name
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val json = JSONObject().put("query", query).toString()
|
||||
webTestClient.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(json)
|
||||
.exchange()
|
||||
.expectBody().json("""{"data":{"getPerson":{"name":"colt"}}}""")
|
||||
.consumeWith {
|
||||
// assertThat(something...)
|
||||
println(it.responseHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DisplayName("findPerson Query Tests")
|
||||
@Nested
|
||||
inner class FindPersonTestCases {
|
||||
|
||||
@Test
|
||||
fun `메모리에 존재하는 Person 객체 중에서 인자와 이름이 일치하는 객체를 얻는다`() {
|
||||
val query = """
|
||||
|
||||
query {
|
||||
findPerson(name: "banjjoknim") {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
val json = JSONObject().put("query", query).toString()
|
||||
webTestClient.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(json)
|
||||
.exchange()
|
||||
.expectBody().json("""{"data":{"findPerson":{"name":"banjjoknim"}}}""")
|
||||
.consumeWith {
|
||||
// assertThat(something...)
|
||||
println(it.responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `인자와 이름이 일치하는 객체가 메모리에 없으면 null을 얻는다`() {
|
||||
val query = """
|
||||
|
||||
query {
|
||||
findPerson(name: "invalid") {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
val json = JSONObject().put("query", query).toString()
|
||||
webTestClient.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(json)
|
||||
.exchange()
|
||||
.expectBody().json("""{"data":{"findPerson":null}}""")
|
||||
.consumeWith {
|
||||
// assertThat(something...)
|
||||
println(it.responseHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/.gitignore
vendored
Normal file
37
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
257
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/README.md
Normal file
257
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/README.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 만들면서 배우는 클린 아키텍처
|
||||
|
||||
- [《만들면서 배우는 클린 아키텍처》 홈페이지](https://wikibook.co.kr/clean-architecture/)
|
||||
- [《만들면서 배우는 클린 아키텍처》 예제 코드](https://github.com/wikibook/clean-architecture)
|
||||
|
||||
|
||||
## 요구사항
|
||||
|
||||
- 회원은 닉네임을 가진다.
|
||||
- 닉네임은 10글자 이내여야 한다.
|
||||
- 닉네임은 공백이 아니어야 한다.
|
||||
|
||||
## 구현
|
||||
|
||||
- 책에서는 계좌 송금 예제를 사용했지만, 회원의 닉네임을 변경하는 예제로 좀 더 심플하게 구성했다.
|
||||
- 책에서는 `Command` 등의 단어를 사용했지만, 본 예제 코드에서는 `Request`, `Response` 등의 단어로 대체했다.
|
||||
- 책에서는 `adapter.out.persistence` 내부에 `Entity`가 포함되어 있었지만, 본 예제 코드에서는 `domain.entity` 패키지 내부로 옮겼다.
|
||||
- 책에서는 `domain` 이라는 패키지 명을 사용했지만, 본 예제 코드에서는 `pojo` 라는 패키지 명으로 대체했다.
|
||||
- 본 예제 코드에서는 어댑터와 애플리케이션의 완전한 격리를 위해 매핑 전략으로 `'완전' 매핑 전략`을 사용했다.
|
||||
|
||||
## 정리
|
||||
|
||||
### 계층형 아키텍처
|
||||
|
||||
- 데이터베이스 주도 설계를 유도함.
|
||||
- 영속성 계층과 도메인 계층의 강한 결합이 발생함.
|
||||
- 영속성 계층은 모든 것에 접근이 가능 -> 결국 모든 계층이 영속성 계층에 의존하게 됨.
|
||||
- 어느 순간에는 테스트 코드를 작성하는 것보다 의존성을 파악하고 Mocking 하는 데 더 많은 시간이 걸리게 됨. -> 테스트하기 어려워짐.
|
||||
- 넓은 서비스 문제 -> 특정 유스케이스를 찾는 것이 어려워짐. -> 고도로 특화된 좁은 도메인 서비스(유스케이스별로 분리된 각각의 서비스)로 해결.
|
||||
|
||||
> #### *지연되는 소프트웨어 프로젝트에 인력을 더하는 것은 개발을 늦출 뿐이다.*
|
||||
>
|
||||
> *<<맨머스 미신: 소프트웨어 공학에 관한 에세이>>, 프레데릭 P. 브룩스*
|
||||
|
||||
### 육각형 아키텍처(헥사고날 아키텍처)
|
||||
|
||||
- [알리스테어 콕번 - 헥사고날 아키텍처 블로그 원문](https://alistair.cockburn.us/hexagonal-architecture/)
|
||||
|
||||

|
||||

|
||||
|
||||
- `포트(port)와 어댑터(adapter) 아키텍처`라고도 불린다.
|
||||
- `포트(port)`는 인터페이스로, 계층간 경계를 지정하는 역할을 한다.
|
||||
- `포트(port)`는 애플리케이션 서비스와 어댑터 사이의 간접적인 계층이다. 각 계층에 대한 직접적인 코드 의존성을 없애준다.
|
||||
- `어댑터(adapter)`는 포트의 구현체이다. 어댑터는 애플리케이션 서비스를 호출하거나, 반대로 애플리케이션 서비스에 의해서 호출되기도 한다.
|
||||
- `엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터`가 핵심이다.
|
||||
|
||||
### 단일 책임 원칙
|
||||
|
||||
> #### *하나의 컴포넌트는 오로지 한 가지 일만 해야 하고, 그것을 올바르게 수행해야 한다.*
|
||||
>#### *컴포넌트를 변경하는 이유는 오직 하나뿐이어야 한다.*
|
||||
|
||||
- 책임 -> 변경할 이유
|
||||
- 단일 책임 원칙 -> 단일 변경 이유 원칙(Single Reason to Change Principal)
|
||||
|
||||
### 의존성 역전 원칙
|
||||
|
||||
- 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 따라서 의존성을 역전시켜 의존성으로부터 보호(격리)해야 한다.
|
||||
|
||||
> #### *코드상의 어떤 의존성이든 그 방향을 바꿀 수(역전시킬 수) 있다.*
|
||||
|
||||
- 사실 의존성의 양쪽 코드를 모두 제어할 수 있을 때만 의존성을 역전시킬 수 있다.
|
||||
|
||||
> #### *클린 아키텍처에서는 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있다.*
|
||||
|
||||
- 도메인 코드에는 바깥으로 향하는 어떤 의존성도 없어야 한다. 클린 아키텍처에서 모든 의존성은 도메인 로직을 향해 안쪽 방향으로 향해야 한다.
|
||||
|
||||
### 패키지 구조
|
||||
|
||||
- 본 프로젝트의 패키지 구조를 참고하도록 한다. 각각의 패키지는 핵심 요소들을 표현한다.
|
||||
|
||||
### 의존성 주입
|
||||
|
||||
- 모든 계층에 의존성을 가진 중립적인 컴포넌트를 하나 도입하여 사용한다. 그렇게 함으로써 핵심 컴포넌트들을 의존성으로부터 보호할 수 있다.
|
||||
|
||||
### 입력 유효성 검증
|
||||
|
||||
- 입력 모델에서 입력 유효성을 검증하여 애플리케이션 계층에는 온전한 입력만 받도록 한다.
|
||||
- [Java Bean Validation API](https://beanvalidation.org)
|
||||
|
||||
### 유스케이스마다 다른 입력 모델
|
||||
|
||||
- 각 유스케이스 전용 입력 모델은 유스케이스를 훨씬 명확하게 만들고 다른 유스케이스와의 결합도 제거해서 불필요한 부수효과가 발생하지 않게 한다.
|
||||
|
||||
### 비즈니스 규칙 vs 입력 유효성
|
||||
|
||||
- 비즈니스 규칙을 검증하는 것은 도메인 모델의 현재 상태에 접근해야 하지만 입력 유효성은 그럴 필요가 없다.
|
||||
- 입력 유효성을 검증하는 일은 @NotNull 애너테이션을 붙인 것처럼 선언적으로 구현할 수 있지만 비즈니스 규칙을 검증하는 일은 조금 더 맥락이 필요하다.
|
||||
- 입력 유효성을 검증하는 것은 `구문상의(syntactical)` 유효성을 검증하는 것이라고 할 수 있다.
|
||||
- 비즈니스 규칙은 유스케이스의 맥락 속에서 `의미적인(semantical)` 유효성을 검증하는 것이라고 할 수 있다.
|
||||
|
||||
### 비즈니스 규칙 검증 구현
|
||||
|
||||
- 가장 좋은 방법은 비즈니스 규칙을 도메인 엔티티 안에 넣는 것이다.
|
||||
- 만약 도메인 엔티티에서 비즈니스 규칙을 검증하기가 어렵다면 유스케이스 코드에서 도메인 엔티티를 사용하기 전에 해도 된다.
|
||||
|
||||
### 풍부한 도메인 모델 vs. 빈약한 도메인 모델
|
||||
|
||||
- 풍부한 도메인 모델에서는 애플리케이션의 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직이 구현된다. 엔티티들은 상태를 변경하는 메서드를 제공하고, 비즈니스 규칙에 맞는 유효한 변경만을 허용한다.
|
||||
- 빈약한 도메인 모델에서는 엔티티 자체가 굉장히 얇다. 일반적으로 엔티티는 상태를 표현하는 필드와 이 값을 읽고 바꾸기 위한 getter, setter 메서드만 포함하고 어떤 도메인 로직도 갖고 있지 않다.
|
||||
|
||||
### 유스케이스마다 다른 출력 모델
|
||||
|
||||
- 출력도 가능하면 각 유스케이스에 맞게 구체적일수록 좋다. 출력은 호출자에게 꼭 필요한 데이터만 들고 있어야 한다.
|
||||
- 유스케이스들 간에 같은 출력 모델을 공유하게 되면 유스케이스들도 강하게 결합된다. 공유 모델은 장기적으로 봤을 때 갖가지 이유로 점점 커지게 되어 있다.
|
||||
- 단일 책임 원칙을 적용하고 모델을 분리해서 유지하는 것은 유스케이스의 결합을 제거하는 데 도움이 된다.
|
||||
|
||||
### 읽기 전용 유스케이스
|
||||
|
||||
- CQS(Command-Query Separation), CQRS(Command-Query Responsibility Segregation)
|
||||
|
||||
### 컨트롤러 나누기
|
||||
|
||||
- 웹 어댑터는 한 개 이상의 클래스로 구성해도 된다. 단, 클래스들이 같은 소속이라는 것을 표현하기 위해 같은 패키지 수준(hierarchy)에 놓아야 한다.
|
||||
- 컨트롤러는 너무 적은 것보다는 너무 많은 게 낫다. 각 컨트롤러가 가능한 한 좁고 다른 컨트롤러와 가능한 한 적게 공유하는 웹 어댑터 조각을 구현해야 한다.
|
||||
- 클래스마다 코드는 적을수록 좋다. 테스트 코드도 마찬가지다. 클래스가 작을수록 더 찾기가 쉽다.
|
||||
- 모델을 공유하지 않는 여러 작은 클래스들을 만드는 것을 두려워해서는 안 된다. 작은 클래스들은 더 파악하기 쉽고, 더 테스트하기 쉬우며, 동시 작업을 지원한다.
|
||||
|
||||
### 인터페이스 나누기
|
||||
|
||||
- 하나의 인터페이스에 모든 연산을 모아두면 모든 서비스가 실제로는 필요하지 않은 메서드에 의존하게 된다.
|
||||
|
||||
> #### *필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 생길 수 있다.*
|
||||
>
|
||||
> *로버트 C. 마틴*
|
||||
|
||||
- `인터페이스 분리 원칙(Interface Segregation Principle, ISP)`
|
||||
- 클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야 한다.
|
||||
|
||||
### 영속성 어댑터 나누기
|
||||
|
||||
- 하나의 애그리거트당 하나의 영속성 어댑터를 만들어서 여러 개의 영속성 어댑터를 만들 수도 있다. 이렇게 하면 영속성 어댑터들은 각 영속성 기능을 이용하는 도메인 경계를 따라 자동으로 나눠진다.
|
||||
- 영속성 어댑터를 훨씬 많은 클래스로 나눌 수도 있다.
|
||||
|
||||
### 영속성, 도메인 모델 타협
|
||||
|
||||
- 영속성 측면과의 타협 없이 풍부한 도메인 모델을 생성하고 싶다면 도메인 모델과 영속성 모델을 매핑하는 것이 좋다.
|
||||
|
||||
### 테스트 피라미드
|
||||
|
||||
- 비용이 많이 드는 테스트는 지양하고 비용이 적게 드는 테스트를 많이 만들어야 한다.
|
||||
- 단위 테스트, 통합 테스트, 시스템 테스트
|
||||
|
||||
### 단위 테스트로 도메인 엔티티 테스트하기
|
||||
|
||||
- 도메인 엔티티의 행동은 다른 클래스에 거의 의존하지 않기 때문에 다른 종류의 테스트는 필요하지 않다.
|
||||
|
||||
### 단위 테스트로 유스케이스 테스트하기
|
||||
|
||||
- 테스트 중인 유스케이스 서비스는 상태가 없기(stateless) 때문에 테스트에서 상태를 검증할 수 없다.
|
||||
- 대신 의존성의 상호작용을 테스트하기 때문에 통합 테스트에 가깝다. 하지만 목으로 작업하고 실제 의존성을 관리해야 하는 것은 아니기 때문에 완전한 통합 테스트에 비해서는 만들고 유지보수 하기 쉽다.
|
||||
|
||||
### 통합 테스트로 웹 어댑터 테스트하기
|
||||
|
||||
- `@WebMvcTest`
|
||||
- 웹 어댑터 테스트는 통합 테스트를 적용하는 것이 합리적이다.
|
||||
- 사실, 웹 컨트롤러는 스프링 프레임워크에 강하게 묶여 있기 때문에(스프링을 사용한다면) 격리된 상태로 테스트하기보다는 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.
|
||||
- 만약 평범하게 단위 테스트로 테스트하면 모든 매핑, 유효성 검증, HTTP 항목에 대한 커버리지가 낮아지고, 프레임워크를 구성하는 요소들이 프로덕션 환경에서 정상적으로 작동할지 확신할 수 없게 된다.
|
||||
|
||||
### 통합 테스트로 영속성 어댑터 테스트하기
|
||||
|
||||
- `@DataJpaTest`
|
||||
- 영속성 어댑터의 테스트 역시 단위 테스트보다는 통합 테스트를 적용하는 것이 합리적이다. 단순히 어댑터의 로직만 검증하는 것이 아니라 데이터베이스 매핑도 검증해야하기 때문이다.
|
||||
- 영속성 어댑터 테스트는 실제 데이터베이스에서 문제가 생길 확률이 높으므로 실제 데이터베이스를 대상으로 진행해야 한다. -> `TestContainer`와 같은 라이브러리의 도움을 받을 수 있다.
|
||||
|
||||
### 시스템 테스트로 주요 경로 테스트하기
|
||||
|
||||
- 피라미드 최상단에 있는 시스템 테스트는 전체 애플리케이션을 띄우고 API를 통해 요청을 보내고, 모든 계층이 조화롭게 잘 동작하는지 검증한다.
|
||||
- `TestRestTemplate` -> 실제 HTTP 통신 이용
|
||||
|
||||
### 테스트 커버리지
|
||||
|
||||
- 라인 커버리지(line coverage)는 테스트 성공을 측정하는 데 있어서는 잘못된 지표다.
|
||||
- 코드의 중요한 부분이 전혀 커버되지 않을 수 있음.
|
||||
- 테스트의 성공 기준 -> 얼마나 마음 편하게 소프트웨어를 배포할 수 있는가?
|
||||
- 더 자주 배포할수록 테스트를 더 신뢰할 수 있음.
|
||||
|
||||
### 테스트 전략
|
||||
|
||||
- 도메인 엔티티를 구현할 때는 단위 테스트로 커버한다.
|
||||
- 유스케이스를 구현할 때는 단위 테스트로 커버한다.
|
||||
- 어댑터를 구현할 때는 통합 테스트로 커버한다.
|
||||
- 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버한다.
|
||||
|
||||
> #### *리팩터링할 때마다 테스트 코드도 변경해야 한다면 테스트는 테스트로서의 가치를 잃는다.*
|
||||
|
||||
### '매핑하지 않기' 전략
|
||||
|
||||
- No Mapping Strategy
|
||||
- 포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면 두 계층 간의 매핑을 할 필요가 없다.
|
||||
- 웹, 애플리케이션, 영속성 계층과 관련된 이유로 인해 변경되어야 함. -> 단일 책임 원칙 위배
|
||||
- 그 결과, 오로지 한 계층에서만 필요한 필드들을 포함하는 파편화된 도메인 모델로 이어질 수 있다.
|
||||
|
||||
### '양방향' 매핑 전략
|
||||
|
||||
- Two-Way Mapping Strategy
|
||||
- 각 어댑터가 전용 모델을 가지고 있어서 해당 모델을 도메인 모델로, 도메인 모델을 해당 모델로 매핑할 책임을 가지고 있다.
|
||||
- 각 계층이 전용 모델을 가지고 있는 덕분에 각 계층이 전용 모델을 변경하더라도 내용이 변경되지 않는 한 다른 계층에는 영향이 없다.
|
||||
|
||||
### '완전' 매핑 전략
|
||||
|
||||
- Full Mapping Strategy
|
||||
- 각 연산이 전용 모델을 필요로 한다. 따라서 웹 어댑터와 애플리케이션 계층 각각이 자신의 전용 모델을 각 연산을 실행하는 데 필요한 모델로 매핑한다.
|
||||
- 각 연산마다 별도의 특화된 입출력 모델을 사용한다.
|
||||
- 웹 계층과 애플리케이션 계층 사이에서 상태 변경 유스케이스의 경계를 명시할 때 유용하다.
|
||||
- 애플리케이션 계층과 영속성 계층 사이에서는 매핑 오버헤드 때문에 사용하지 않는 것이 좋다.
|
||||
|
||||
### '단방향' 매핑 전략
|
||||
|
||||
- One-Way Mapping Strategy
|
||||
- 동일한 '상태' 인터페이스를 구현하는 도메인 모델과 어댑터 모델을 이용하면 각 계층은 다른 계층으로부터 온 객체를 단방향으로 매핑하기만 하면 된다.
|
||||
- 모든 계층의 모델들이 같은 인터페이스를 구현한다. 이 인터페이스는 관련 있는 특성(attribute)에 대한 getter 메서드를 제공해서 도메인 모델의 상태를 캡슐화 한다.
|
||||
|
||||
### 설정 컴포넌트
|
||||
|
||||
- 유스케이스는 인터페이스만 알아야 하고, 런타임에 이 인터페이스의 구현을 제공받아야 한다.
|
||||
- 애플리케이션의 나머지 부분을 깔끔하게 유지하고 싶다면 아키텍처에 대해 중립적이고 인스턴스 생성을 위해 모든 클래스에 대한 의존성을 가지는 설정 컴포넌트(configuration component)가 필요하다.
|
||||
- 스프링의 클래스패스 스캐닝 방식
|
||||
- 스프링의 자바 컨피그 설정 방식
|
||||
|
||||
### 소프트웨어 아키텍처
|
||||
|
||||
> #### *소프트웨어 아키텍처는 아키텍처 요소 간의 의존성을 관리하는 게 전부다. 만약 의존성이 거대한 진흙 덩어리(big ball of mud)가 된다면 아키텍처 역시 거대한 진흙 덩어리가 된다.*
|
||||
|
||||
### 깨진 창문 이론
|
||||
|
||||
- 품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다.
|
||||
- 코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다.
|
||||
- 지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다.
|
||||
|
||||
### 위험한 지름길
|
||||
|
||||
- 유스케이스 간 모델 공유하기 -> 유스케이스 간에 입출력 모델을 공유하게 되면 유스케이스들 사이에 결합이 생긴다.
|
||||
- 도메인 엔티티를 입출력 모델로 사용하기 -> 도메인 엔티티를 유스케이스의 입출력 모델로 사용하면 도메인 엔티티가 유스케이스에 결합된다.
|
||||
- 인커밍 포트 건너뛰기 -> 인커밍 포트가 없으면 도메인 로직의 진입점이 불분명해진다. 인커밍 포트를 유지하면 아키텍처를 쉽게 강제할 수 있다.
|
||||
- 애플리케이션 서비스 건너뛰기 -> 애플리케이션 서비스가 없으면 도메인 로직을 둘 곳이 없다.
|
||||
|
||||
### 육각형 아키텍처 스타일
|
||||
|
||||
> #### *외부의 영향을 받지 않고 도메인 코드를 자유롭게 발전시킬 수 있다는 것은 육각형 아키텍처 스타일이 내세우는 가장 중요한 가치다.*
|
||||
|
||||
- 영속성 관심사나 외부 시스템에 대한 의존성 등의 변화로부터 자유롭게 도메인 코드를 개발할 수 있다.
|
||||
- 만약 도메인 코드가 애플리케이션에서 가장 중요한 것이 아니라면 이 아키텍처 스타일은 필요하지 않을 것이다.
|
||||
|
||||
## 참고자료
|
||||
|
||||
- [Best way to dynamically load adapters in hexagonal architecture?](https://stackoverflow.com/questions/50436649/best-way-to-dynamically-load-adapters-in-hexagonal-architecture)
|
||||
- [hexagonal architecture with spring data](https://stackoverflow.com/questions/46509252/hexagonal-architecture-with-spring-data)
|
||||
- [Ports-And-Adapters](https://www.dossier-andreas.net/software_architecture/ports_and_adapters.html)
|
||||
- [Ports And Adapters Architecture](http://wiki.c2.com/?PortsAndAdaptersArchitecture)
|
||||
- [Hexagonal architecture with Domain, Presenter & Entity segregation on Spring WebFlux](https://medium.com/javarevisited/hexagonal-architecture-with-domain-presenter-entity-segregation-on-spring-webflux-ef053a495bdc)
|
||||
- [지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기](https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture/)
|
||||
- [JPA Week3 Entity Mapping / Hexagonal Architecture](https://www.slideshare.net/ssuser8f4c99/jpa-week3-entity-mapping-hexagonal-architecture-250068805)
|
||||
- [Hexagonal Architecture Articles](https://jmgarridopaz.github.io/content/articles.html)
|
||||
- [Domain-Driven Design and the Hexagonal Architecture](https://vaadin.com/learn/tutorials/ddd/ddd_and_hexagonal)
|
||||
- [The Pattern: Ports and Adapters (‘’Object Structural’’)](https://alistair.cockburn.us/hexagonal-architecture/)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("org.springframework.boot") version "2.6.1"
|
||||
id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||
kotlin("jvm") version "1.6.0"
|
||||
kotlin("plugin.spring") version "1.6.0"
|
||||
kotlin("plugin.jpa") version "1.6.0"
|
||||
}
|
||||
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("mysql:mysql-connector-java")
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
// testImplementation("org.springframework.security:spring-security-test")
|
||||
testImplementation("io.mockk:mockk:1.12.3")
|
||||
testImplementation("com.ninja-squad:springmockk:3.1.1")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
BIN
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
234
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradlew
vendored
Executable file
234
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradlew
vendored
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradlew.bat
vendored
Normal file
89
놀이터(예제 코드 작성)/learn-with-making-clean-architecture/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 584 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1 @@
|
||||
rootProject.name = "learn-with-making-clean-architecture"
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.banjjoknim.cleanarchitecture
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class LearnWithMakingCleanArchitectureApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<LearnWithMakingCleanArchitectureApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.`in`.web
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameRequestData
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
data class ChangeNicknameRequest(
|
||||
val userId: Long,
|
||||
@field:NotBlank
|
||||
@field:Size(max = NICKNAME_LENGTH_LIMIT)
|
||||
val newNickname: String
|
||||
) {
|
||||
fun toData(): ChangeNicknameRequestData {
|
||||
return ChangeNicknameRequestData(userId, newNickname)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NICKNAME_LENGTH_LIMIT = 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.`in`.web
|
||||
|
||||
data class ChangeNicknameResponse(val userId: Long)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.`in`.web
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameUseCase
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.Valid
|
||||
|
||||
@RequestMapping("/users")
|
||||
@RestController
|
||||
class ChangeNicknameWebAdapter(
|
||||
private val changeNicknameWebPort: ChangeNicknameUseCase
|
||||
) {
|
||||
@PostMapping("")
|
||||
fun changeNickname(@RequestBody @Valid changeNicknameRequest: ChangeNicknameRequest): ChangeNicknameResponse {
|
||||
val requestData = changeNicknameRequest.toData()
|
||||
val responseData = changeNicknameWebPort.changeNickname(requestData)
|
||||
return ChangeNicknameResponse(responseData.userId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.LoadUserPersistencePort
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class LoadUserPersistenceAdapter(
|
||||
private val userMapper: UserMapper,
|
||||
private val userEntityRepository: UserEntityRepository
|
||||
) : LoadUserPersistencePort {
|
||||
override fun loadUser(userId: Long): User {
|
||||
val userEntity = userEntityRepository.findByIdOrNull(userId)
|
||||
?: throw NoSuchElementException("존재하지 않는 회원입니다. userId: $userId")
|
||||
return userMapper.mapToDomainModel(userEntity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Embeddable
|
||||
|
||||
@Embeddable
|
||||
data class NicknameColumn(
|
||||
@Column(name = "nickname")
|
||||
val value: String
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.UpsertUserPersistencePort
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class UpsertUserPersistenceAdapter(
|
||||
private val userMapper: UserMapper,
|
||||
private val userEntityRepository: UserEntityRepository
|
||||
) : UpsertUserPersistencePort {
|
||||
override fun upsertUser(user: User) {
|
||||
val userEntity = userMapper.mapToDomainEntity(user)
|
||||
userEntityRepository.save(userEntity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import javax.persistence.Embedded
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* 패키지를 독립적 배포의 관점에서 바라봤을 때, UserEntity 는 Persistence 영역에 존재하는 것이 타당하다.
|
||||
*
|
||||
* 만약 그렇지 않다면 UserEntity 가 존재하지 않는 UserEntityRepository 가 배포될 것이기 때문이다.
|
||||
*/
|
||||
@Table(name = "User")
|
||||
@Entity
|
||||
class UserEntity(
|
||||
@Id
|
||||
val id: Long = 0L,
|
||||
@Embedded
|
||||
var nickname: NicknameColumn
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface UserEntityRepository: JpaRepository<UserEntity, Long> {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.Nickname
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class UserMapper {
|
||||
fun mapToDomainEntity(user: User): UserEntity {
|
||||
return UserEntity(user.id, NicknameColumn(user.nickname.value))
|
||||
}
|
||||
|
||||
fun mapToDomainModel(userEntity: UserEntity): User {
|
||||
return User(userEntity.id, Nickname(userEntity.nickname.value))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
|
||||
|
||||
data class ChangeNicknameRequestData(val userId: Long, val newNickname: String)
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
|
||||
|
||||
data class ChangeNicknameResponseData(val userId: Long)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
|
||||
|
||||
interface ChangeNicknameUseCase {
|
||||
fun changeNickname(data: ChangeNicknameRequestData): ChangeNicknameResponseData
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.port.out
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
|
||||
interface LoadUserPersistencePort {
|
||||
fun loadUser(userId: Long): User
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.port.out
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
|
||||
interface UpsertUserPersistencePort {
|
||||
fun upsertUser(user: User)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.service
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameRequestData
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameResponseData
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameUseCase
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.LoadUserPersistencePort
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.UpsertUserPersistencePort
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Transactional
|
||||
@Service
|
||||
class ChangeNicknameService(
|
||||
private val loadUserPersistencePort: LoadUserPersistencePort,
|
||||
private val upsertUserPersistencePort: UpsertUserPersistencePort
|
||||
) : ChangeNicknameUseCase {
|
||||
override fun changeNickname(data: ChangeNicknameRequestData): ChangeNicknameResponseData {
|
||||
val user = loadUserPersistencePort.loadUser(data.userId)
|
||||
user.changeNickname(data.newNickname)
|
||||
upsertUserPersistencePort.upsertUser(user)
|
||||
return ChangeNicknameResponseData(user.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.pojo
|
||||
|
||||
data class Nickname(val value: String)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.pojo
|
||||
|
||||
class User(
|
||||
var id: Long = 0L,
|
||||
var nickname: Nickname
|
||||
) {
|
||||
fun changeNickname(newNickname: String) {
|
||||
this.nickname = Nickname(newNickname)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;MODE=MySQL
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<header>
|
||||
<meta lang="ko" charset="UTF-8">
|
||||
<title>만들면서 배우는 클린 아키텍처</title>
|
||||
</header>
|
||||
<body>
|
||||
<h1>만들면서 배우는 클린 아키텍처</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.cleanarchitecture
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class LearnWithMakingCleanArchitectureApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.`in`.web
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameResponseData
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameUseCase
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.post
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
import org.springframework.web.filter.CharacterEncodingFilter
|
||||
|
||||
@WebMvcTest(ChangeNicknameWebAdapter::class)
|
||||
class ChangeNicknameWebAdapterTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockmvc: MockMvc
|
||||
|
||||
@Autowired
|
||||
private lateinit var objectMapper: ObjectMapper
|
||||
|
||||
@MockkBean
|
||||
private lateinit var changeNicknameUseCase: ChangeNicknameUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setUp(webApplicationContext: WebApplicationContext) {
|
||||
mockmvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
|
||||
.addFilter<DefaultMockMvcBuilder>(CharacterEncodingFilter("UTF-8"))
|
||||
.alwaysDo<DefaultMockMvcBuilder>(MockMvcResultHandlers.print())
|
||||
.build()
|
||||
}
|
||||
|
||||
@DisplayName("닉네임 변경 테스트 케이스")
|
||||
@Nested
|
||||
inner class ChangeNicknameTestCases {
|
||||
@Test
|
||||
fun `닉네임을 변경한다`() {
|
||||
every { changeNicknameUseCase.changeNickname(any()) } returns ChangeNicknameResponseData(1L)
|
||||
val request = ChangeNicknameRequest(1L, "banjjoknim")
|
||||
|
||||
mockmvc.post("/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = objectMapper.writeValueAsString(request)
|
||||
}.andExpect {
|
||||
content { json("""{"userId":1}""") }
|
||||
status { isOk() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `잘못된 닉네임 변경 요청에 BadRequest 응답을 반환한다`() {
|
||||
val request = ChangeNicknameRequest(1L, "banjjoknim!!")
|
||||
|
||||
mockmvc.post("/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = objectMapper.writeValueAsString(request)
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.Nickname
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.test.context.jdbc.Sql
|
||||
|
||||
@DataJpaTest
|
||||
@Import(value = [UserMapper::class, LoadUserPersistenceAdapter::class])
|
||||
class LoadUserPersistenceAdapterTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var loadUserPersistenceAdapter: LoadUserPersistenceAdapter
|
||||
|
||||
@DisplayName("회원 조회 테스트 케이스")
|
||||
@Nested
|
||||
inner class LoadUserTestCases {
|
||||
@Sql(statements = ["INSERT INTO USER VALUES (1, 'old')"])
|
||||
@Test
|
||||
fun `회원이 존재하지 않을 경우 예외가 발생한다`() {
|
||||
assertThrows<NoSuchElementException>("존재하지 않는 회원입니다. userId: 100") {
|
||||
loadUserPersistenceAdapter.loadUser(
|
||||
100L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Sql(statements = ["INSERT INTO USER VALUES (1, 'old')"])
|
||||
@Test
|
||||
fun `회원이 존재할 경우 회원을 조회할 수 있다`() {
|
||||
val user = loadUserPersistenceAdapter.loadUser(1L)
|
||||
|
||||
Assertions.assertThat(user.id).isEqualTo(1)
|
||||
Assertions.assertThat(user.nickname).isEqualTo(Nickname("old"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.Nickname
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.test.context.jdbc.Sql
|
||||
|
||||
@DataJpaTest
|
||||
@Import(value = [UserMapper::class, UpsertUserPersistenceAdapter::class])
|
||||
class UpsertUserPersistenceAdapterTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var upsertUserPersistenceAdapter: UpsertUserPersistenceAdapter
|
||||
|
||||
@Autowired
|
||||
private lateinit var userEntityRepository: UserEntityRepository
|
||||
|
||||
@DisplayName("회원 상태 저장 및 수정 테스트 케이스")
|
||||
@Nested
|
||||
inner class UpsertUserTestCases {
|
||||
@Sql(statements = ["INSERT INTO USER VALUES (1, 'old')"])
|
||||
@Test
|
||||
fun `회원 상태를 저장하거나 수정한다`() {
|
||||
val updatedUser = User(1L, Nickname("new"))
|
||||
|
||||
upsertUserPersistenceAdapter.upsertUser(updatedUser)
|
||||
|
||||
val userEntity = userEntityRepository.findByIdOrNull(1L)
|
||||
assertThat(userEntity?.nickname).isEqualTo(NicknameColumn("new"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.Nickname
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest(classes = [UserMapper::class])
|
||||
class UserMapperTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var userMapper: UserMapper
|
||||
|
||||
@DisplayName("User POJO <-> User Entity 매핑 테스트 케이스")
|
||||
@Nested
|
||||
inner class UserMapperTestCases {
|
||||
@Test
|
||||
fun `User POJO 를 User Entity 로 변환한다`() {
|
||||
val user = User(1L, Nickname("banjjoknim"))
|
||||
|
||||
val userEntity = userMapper.mapToDomainEntity(user)
|
||||
|
||||
assertThat(userEntity.id).isEqualTo(1)
|
||||
assertThat(userEntity.nickname).isEqualTo(NicknameColumn("banjjoknim"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `User Entity 를 User POJO 로 변환한다`() {
|
||||
val userEntity = UserEntity(1L, NicknameColumn("banjjoknim"))
|
||||
|
||||
val user = userMapper.mapToDomainModel(userEntity)
|
||||
|
||||
assertThat(user.id).isEqualTo(1)
|
||||
assertThat(user.nickname).isEqualTo(Nickname("banjjoknim"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.application.service
|
||||
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.`in`.ChangeNicknameRequestData
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.LoadUserPersistencePort
|
||||
import com.banjjoknim.cleanarchitecture.user.application.port.out.UpsertUserPersistencePort
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.Nickname
|
||||
import com.banjjoknim.cleanarchitecture.user.pojo.User
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ChangeNicknameServiceTest {
|
||||
|
||||
private val loadUserPersistencePort = mockk<LoadUserPersistencePort>()
|
||||
private val upsertUserPersistencePort = mockk<UpsertUserPersistencePort>()
|
||||
private val changeNicknameService = ChangeNicknameService(loadUserPersistencePort, upsertUserPersistencePort)
|
||||
|
||||
@DisplayName("닉네임 변경 테스트 케이스")
|
||||
@Nested
|
||||
inner class ChangeNicknameTestCases {
|
||||
|
||||
private lateinit var testUser: User
|
||||
private val testChangeNicknameRequestData = ChangeNicknameRequestData(1L, "new")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
testUser = User(1L, Nickname("old"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `닉네임을 변경한다`() {
|
||||
every { loadUserPersistencePort.loadUser(any()) } returns testUser
|
||||
every { upsertUserPersistencePort.upsertUser(any()) } just Runs
|
||||
|
||||
changeNicknameService.changeNickname(testChangeNicknameRequestData)
|
||||
|
||||
assertThat(testUser.nickname).isEqualTo(Nickname("new"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.banjjoknim.cleanarchitecture.user.pojo
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class UserTest {
|
||||
|
||||
|
||||
@DisplayName("회원 닉네임 변경 테스트 케이스")
|
||||
@Nested
|
||||
inner class ChangeNicknameTestCases {
|
||||
@Test
|
||||
fun `회원 닉네임을 변경한다`() {
|
||||
// given
|
||||
val user = User(nickname = Nickname("banjjoknim"))
|
||||
|
||||
// when
|
||||
user.changeNickname("colt")
|
||||
|
||||
// then
|
||||
assertThat(user.nickname).isEqualTo(Nickname("colt"))
|
||||
}
|
||||
}
|
||||
}
|
||||
37
놀이터(예제 코드 작성)/spring-cloud-open-feign/.gitignore
vendored
Normal file
37
놀이터(예제 코드 작성)/spring-cloud-open-feign/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
25
놀이터(예제 코드 작성)/spring-cloud-open-feign/README.md
Normal file
25
놀이터(예제 코드 작성)/spring-cloud-open-feign/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Spring Cloud Open Feign
|
||||
|
||||
## 프로젝트 구성
|
||||
|
||||
- [Spring initializr](https://start.spring.io/) 에서 프로젝트 생성.
|
||||
- 아래 사진은 프로젝트 구성 Dependencies.
|
||||
|
||||

|
||||
|
||||
## 요구사항
|
||||
|
||||
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/) 에서 게시글 목록을 조회한다.
|
||||
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/) 에서 게시글을 조회한다.
|
||||
|
||||
## 참고자료
|
||||
|
||||
- [Introduction to Spring Cloud OpenFeign](https://www.baeldung.com/spring-cloud-openfeign)
|
||||
- [Spring Cloud OpenFeign](https://spring.io/projects/spring-cloud-openfeign)
|
||||
- [Spring Cloud OpenFeign](https://cloud.spring.io/spring-cloud-openfeign/reference/html/)
|
||||
- [우아한 Feign 적용기](https://techblog.woowahan.com/2630/)
|
||||
- [Feign 좀 더 나아가기](https://techblog.woowahan.com/2657/)
|
||||
- [GitHub - OpenFeign/feign](https://github.com/OpenFeign/feign)
|
||||
- [Java Jackson JSON Library](https://kwonnam.pe.kr/wiki/java/jackson)
|
||||
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/)
|
||||
- [Spring Cloud Openfeign](https://brunch.co.kr/@springboot/202)
|
||||
47
놀이터(예제 코드 작성)/spring-cloud-open-feign/build.gradle.kts
Normal file
47
놀이터(예제 코드 작성)/spring-cloud-open-feign/build.gradle.kts
Normal file
@@ -0,0 +1,47 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.7.5"
|
||||
id("io.spring.dependency-management") version "1.0.15.RELEASE"
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("plugin.spring") version "1.6.21"
|
||||
kotlin("plugin.jpa") version "1.6.21"
|
||||
}
|
||||
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
extra["springCloudVersion"] = "2021.0.4"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
BIN
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
240
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradlew
vendored
Executable file
240
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradlew
vendored
Executable file
@@ -0,0 +1,240 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
91
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradlew.bat
vendored
Normal file
91
놀이터(예제 코드 작성)/spring-cloud-open-feign/gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
BIN
놀이터(예제 코드 작성)/spring-cloud-open-feign/img.png
Normal file
BIN
놀이터(예제 코드 작성)/spring-cloud-open-feign/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@@ -0,0 +1 @@
|
||||
rootProject.name = "spring-cloud-open-feign"
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.springcloudopenfeign
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients
|
||||
|
||||
@EnableFeignClients
|
||||
@SpringBootApplication
|
||||
class SpringCloudOpenFeignApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SpringCloudOpenFeignApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.banjjoknim.springcloudopenfeign.configuration
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import feign.Logger
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
||||
|
||||
@Configuration
|
||||
class FeignConfiguration : Jackson2ObjectMapperBuilderCustomizer {
|
||||
override fun customize(jacksonObjectMapperBuilder: Jackson2ObjectMapperBuilder) {
|
||||
jacksonObjectMapperBuilder
|
||||
.featuresToEnable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
|
||||
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For each Feign client, a logger is created by default.
|
||||
*
|
||||
* To enable logging, we should declare it in the application.properties file using the package name of the client interfaces:
|
||||
*
|
||||
* > logging.level.com.baeldung.cloud.openfeign.client: DEBUG
|
||||
*
|
||||
* Or, if we want to enable logging only for one particular client in a package, we can use the full class name:
|
||||
*
|
||||
* > logging.level.com.baeldung.cloud.openfeign.client.JSONPlaceHolderClient: DEBUG
|
||||
*
|
||||
* **Note that Feign logging responds only to the DEBUG level.**
|
||||
*
|
||||
* The ***Logger.Level*** that we may configure per client indicates how much to log:
|
||||
* ```java
|
||||
* public class ClientConfiguration {
|
||||
*
|
||||
* @Bean
|
||||
* Logger.Level feignLoggerLevel() {
|
||||
* return Logger.Level.BASIC;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* There are four logging levels to choose from:
|
||||
*
|
||||
* - NONE – no logging, which is the default
|
||||
* - BASIC – log only the request method, URL and response status
|
||||
* - HEADERS – log the basic information together with request and response headers
|
||||
* - FULL – log the body, headers and metadata for both request and response
|
||||
*/
|
||||
@Configuration
|
||||
class LoggerConfiguration {
|
||||
@Bean
|
||||
fun feignLoggerLevel(): Logger.Level {
|
||||
return Logger.Level.FULL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.banjjoknim.springcloudopenfeign.domain
|
||||
|
||||
data class Post(
|
||||
val userId: Long,
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val completed: Boolean
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.banjjoknim.springcloudopenfeign.domain
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
|
||||
/**
|
||||
* Feign is a declarative web service client. It makes writing web service clients easier.
|
||||
* To use Feign create an interface and annotate it.
|
||||
* It has pluggable annotation support including Feign annotations and JAX-RS annotations.
|
||||
* Feign also supports pluggable encoders and decoders.
|
||||
* Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web.
|
||||
* Spring Cloud integrates Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
*
|
||||
* 쉽게 말하면 RestTemplate, RestClient 등의 `HTTP Client`를 어노테이션으로 선언하여 사용하는 것이라 생각하면 된다.
|
||||
* 인터페이스를 만들고 @FeignClient 를 선언하기만 하면 구현체는 런타임에 알아서 만들어진다.
|
||||
*
|
||||
* @see org.springframework.cloud.openfeign.FeignClient
|
||||
*/
|
||||
@FeignClient(value = "post", url = "https://jsonplaceholder.typicode.com/")
|
||||
interface PostClient {
|
||||
|
||||
@GetMapping("/posts")
|
||||
fun findPosts(): List<Post>
|
||||
|
||||
@GetMapping("/posts/{postId}")
|
||||
fun findPost(@PathVariable postId: Long): Post
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.banjjoknim.springcloudopenfeign.domain
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class PostController(
|
||||
private val postClient: PostClient
|
||||
) {
|
||||
|
||||
@GetMapping("/posts")
|
||||
fun posts(): List<Post> {
|
||||
return postClient.findPosts()
|
||||
}
|
||||
|
||||
@GetMapping("/posts/{postId}")
|
||||
fun post(@PathVariable postId: Long): Post {
|
||||
return postClient.findPost(postId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
|
||||
show-sql: true
|
||||
logging:
|
||||
level:
|
||||
com:
|
||||
banjjoknim:
|
||||
springcloudopenfeign: DEBUG
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.springcloudopenfeign
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class SpringCloudOpenFeignApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
37
놀이터(예제 코드 작성)/spring-multi-module/.gitignore
vendored
Normal file
37
놀이터(예제 코드 작성)/spring-multi-module/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
37
놀이터(예제 코드 작성)/spring-multi-module/build.gradle.kts
Normal file
37
놀이터(예제 코드 작성)/spring-multi-module/build.gradle.kts
Normal file
@@ -0,0 +1,37 @@
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.6.7" apply false
|
||||
id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false
|
||||
kotlin("jvm") version "1.6.21" apply false
|
||||
kotlin("plugin.spring") version "1.6.21" apply false
|
||||
kotlin("plugin.jpa") version "1.6.21" apply false
|
||||
}
|
||||
|
||||
allprojects { // 모든 프로젝트 모듈에 아래의 사항을 적용한다.
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
apply { // subprojects, 서브 모듈들에 아래의 플러그인들을 적용한다.
|
||||
plugin("org.springframework.boot")
|
||||
plugin("io.spring.dependency-management")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
BIN
놀이터(예제 코드 작성)/spring-multi-module/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
놀이터(예제 코드 작성)/spring-multi-module/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
놀이터(예제 코드 작성)/spring-multi-module/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
놀이터(예제 코드 작성)/spring-multi-module/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
234
놀이터(예제 코드 작성)/spring-multi-module/gradlew
vendored
Executable file
234
놀이터(예제 코드 작성)/spring-multi-module/gradlew
vendored
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
놀이터(예제 코드 작성)/spring-multi-module/gradlew.bat
vendored
Normal file
89
놀이터(예제 코드 작성)/spring-multi-module/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
/**
|
||||
* build.gradle.kts(springmultimodule) 의 subprojects 항목에서 아래의 플러그인을 적용해주고 있으므로 주석처리.
|
||||
*
|
||||
* id("org.springframework.boot") version "2.6.7"
|
||||
* id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||
*/
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("plugin.spring") version "1.6.21"
|
||||
}
|
||||
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
dependencies {
|
||||
implementation(project(":module-domain"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("mysql:mysql-connector-java")
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("io.mockk:mockk:1.12.3")
|
||||
testImplementation("com.ninja-squad:springmockk:3.1.1")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.banjjoknim.springmultimodule
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class SpringMultiModuleApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SpringMultiModuleApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
import com.banjjoknim.springmultimodule.user.UserRepository
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterPersistencePort
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class UserRegisterPersistenceAdapter(
|
||||
private val userRepository: UserRepository
|
||||
) : UserRegisterPersistencePort {
|
||||
override fun registerUser(user: User): User {
|
||||
return userRepository.save(user)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterRequestData
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class UserRegisterRequest(
|
||||
@NotBlank
|
||||
val name: String = ""
|
||||
) {
|
||||
fun toData(): UserRegisterRequestData {
|
||||
return UserRegisterRequestData(name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterResponseData
|
||||
|
||||
data class UserRegisterResponse(
|
||||
val userId: Long
|
||||
) {
|
||||
constructor(responseData: UserRegisterResponseData) : this(
|
||||
userId = responseData.userId
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterUseCase
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.Valid
|
||||
|
||||
@RequestMapping("/users")
|
||||
@RestController
|
||||
class UserRegisterWebAdapter(
|
||||
private val userRegisterUseCase: UserRegisterUseCase
|
||||
) {
|
||||
@PostMapping("")
|
||||
fun registerUser(@RequestBody @Valid userRegisterRequest: UserRegisterRequest): ResponseEntity<UserRegisterResponse> {
|
||||
val requestData = userRegisterRequest.toData()
|
||||
val responseData = userRegisterUseCase.registerUser(requestData)
|
||||
return ResponseEntity.ok(UserRegisterResponse(responseData))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
|
||||
interface UserRegisterPersistencePort {
|
||||
fun registerUser(user: User): User
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
data class UserRegisterRequestData(val name: String)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
|
||||
data class UserRegisterResponseData(val userId: Long) {
|
||||
constructor(user: User) : this(
|
||||
userId = user.id
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class UserRegisterService(
|
||||
private val userRegisterPersistencePort: UserRegisterPersistencePort
|
||||
) : UserRegisterUseCase {
|
||||
@Transactional
|
||||
override fun registerUser(requestData: UserRegisterRequestData): UserRegisterResponseData {
|
||||
val newUser = User(name = requestData.name)
|
||||
val user = userRegisterPersistencePort.registerUser(newUser)
|
||||
return UserRegisterResponseData(user)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
interface UserRegisterUseCase {
|
||||
fun registerUser(requestData: UserRegisterRequestData): UserRegisterResponseData
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;MODE=MySQL;
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.banjjoknim.springmultimodule
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class SpringMultiModuleApplicationTest {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
import com.banjjoknim.springmultimodule.user.UserRepository
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
|
||||
@DataJpaTest
|
||||
@Import(value = [UserRegisterPersistenceAdapter::class])
|
||||
class UserRegisterPersistenceAdapterTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var userRegisterPersistenceAdapter: UserRegisterPersistenceAdapter
|
||||
|
||||
@Autowired
|
||||
private lateinit var userRepository: UserRepository
|
||||
|
||||
@Test
|
||||
fun `회원을 등록한다`() {
|
||||
userRegisterPersistenceAdapter.registerUser(User("banjjoknim"))
|
||||
|
||||
val user = userRepository.findByIdOrNull(1)
|
||||
|
||||
assertThat(user).isNotNull
|
||||
assertThat(user?.name).isEqualTo("banjjoknim")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.banjjoknim.springmultimodule.user.adapter.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterResponseData
|
||||
import com.banjjoknim.springmultimodule.user.application.register.UserRegisterUseCase
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.post
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
import org.springframework.web.filter.CharacterEncodingFilter
|
||||
|
||||
@WebMvcTest(controllers = [UserRegisterWebAdapter::class])
|
||||
class UserRegisterWebAdapterTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@Autowired
|
||||
private lateinit var objectMapper: ObjectMapper
|
||||
|
||||
@MockkBean
|
||||
private lateinit var userRegisterUseCase: UserRegisterUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setUp(webApplicationContext: WebApplicationContext) {
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
|
||||
.addFilter<DefaultMockMvcBuilder>(CharacterEncodingFilter("UTF-8"))
|
||||
.alwaysDo<DefaultMockMvcBuilder>(MockMvcResultHandlers.print())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `회원을 등록한다`() {
|
||||
every { userRegisterUseCase.registerUser(any()) } returns UserRegisterResponseData(1)
|
||||
val request = UserRegisterRequest("banjjoknim")
|
||||
|
||||
mockMvc.post("/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = objectMapper.writeValueAsString(request)
|
||||
}.andExpect {
|
||||
content { json("""{"userId":1}""") }
|
||||
status { isOk() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.banjjoknim.springmultimodule.user.application.register
|
||||
|
||||
import com.banjjoknim.springmultimodule.user.User
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class UserRegisterServiceTest {
|
||||
|
||||
private val userRegisterPersistencePort = mockk<UserRegisterPersistencePort>()
|
||||
private val userRegisterService = UserRegisterService(userRegisterPersistencePort)
|
||||
|
||||
@Test
|
||||
fun `회원을 등록한다`() {
|
||||
every { userRegisterPersistencePort.registerUser(any()) } returns User("banjjoknim", 1)
|
||||
val requestData = UserRegisterRequestData("banjjoknim")
|
||||
|
||||
val responseData = userRegisterService.registerUser(requestData)
|
||||
|
||||
assertThat(responseData).isEqualTo(UserRegisterResponseData(1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
/**
|
||||
* build.gradle.kts(springmultimodule) 의 subprojects 항목에서 아래의 플러그인을 적용해주고 있으므로 주석처리.
|
||||
*
|
||||
* id("org.springframework.boot") version "2.6.7"
|
||||
* id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||
*/
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("plugin.jpa") version "1.6.21"
|
||||
}
|
||||
|
||||
group = "com.banjjoknim"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.banjjoknim.springmultimodule.user
|
||||
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
|
||||
@Entity
|
||||
class User(
|
||||
@Column(name = "name")
|
||||
var name: String,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long = 0
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.banjjoknim.springmultimodule.user
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user