Compare commits
49 Commits
jackson
...
graphql-ko
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eed470c7e | ||
|
|
eb505572a1 | ||
|
|
19de2ec725 | ||
|
|
cf95180522 | ||
|
|
7d8bcdfc82 | ||
|
|
c3a6cbbdf9 | ||
|
|
5a17add9d1 | ||
|
|
0165c1eacb | ||
|
|
e05abb53f1 | ||
|
|
738246a31f | ||
|
|
1721962444 | ||
|
|
f46c2178c0 | ||
|
|
5bb45cc2e6 | ||
|
|
4516309e8f | ||
|
|
33224bb7f2 | ||
|
|
288369de97 | ||
|
|
0543c82018 | ||
|
|
35865696ed | ||
|
|
164cdd73d0 | ||
|
|
e8f1d57f43 | ||
|
|
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-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> {
|
||||
}
|
||||
3
놀이터(예제 코드 작성)/spring-multi-module/settings.gradle.kts
Normal file
3
놀이터(예제 코드 작성)/spring-multi-module/settings.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
rootProject.name = "springmultimodule"
|
||||
include("module-api")
|
||||
include("module-domain")
|
||||
@@ -24,6 +24,10 @@ dependencies {
|
||||
// OAuth2 로그인을 위해 추가. spring-boot-starter-security 의존성이 있어도 기본적으로 추가되지 않기 때문.
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
|
||||
// https://mvnrepository.com/artifact/com.auth0/java-jwt
|
||||
// JWT 사용을 위해 라이브러리 추가. 2022.03.25 기준 최신 바로 전 버전.
|
||||
implementation("com.auth0:java-jwt:3.18.3")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.banjjoknim.playground.config;
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
|
||||
// @EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.csrf().disable();
|
||||
http.authorizeHttpRequests()
|
||||
.antMatchers("/user/**").authenticated()
|
||||
.antMatchers("/manager/**").hasAnyRole("MANAGER", "ADMIN")
|
||||
.antMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
.and()
|
||||
.formLogin()
|
||||
.loginPage("/login");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.banjjoknim.playground.config.security
|
||||
package com.banjjoknim.playground.daooauth.config.security
|
||||
|
||||
import com.banjjoknim.playground.domain.auth.OAuth2Type
|
||||
import com.banjjoknim.playground.domain.user.User
|
||||
import com.banjjoknim.playground.domain.user.UserRepository
|
||||
import com.banjjoknim.playground.daooauth.domain.auth.OAuth2Type
|
||||
import com.banjjoknim.playground.daooauth.domain.user.User
|
||||
import com.banjjoknim.playground.daooauth.domain.user.UserRepository
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
@@ -41,9 +41,18 @@ import org.springframework.stereotype.Service
|
||||
*
|
||||
* OAuth2는 여러가지 방식이 있다. Authorization Code Grant Type 방식 등등..
|
||||
*
|
||||
* `@EnableGlobalMethodSecurity` 어노테이션을 사용하면 스프링 시큐리티 관련 특정 어노테이션에 대한 활성화 설정을 할 수 있다.
|
||||
* [spring-security-method-security](https://www.baeldung.com/spring-security-method-security) 참고.
|
||||
*
|
||||
* @see org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties
|
||||
* @see org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter
|
||||
* @see org.springframework.security.config.oauth2.client.CommonOAuth2Provider
|
||||
* @see org.springframework.security.access.prepost.PreAuthorize
|
||||
* @see org.springframework.security.access.prepost.PostAuthorize
|
||||
* @see org.springframework.security.access.prepost.PreFilter
|
||||
* @see org.springframework.security.access.prepost.PostFilter
|
||||
* @see org.springframework.security.access.annotation.Secured
|
||||
* @see javax.annotation.security.RolesAllowed
|
||||
*/
|
||||
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록되도록 해준다.
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 스프링 시큐리티 관련 특정 어노테이션에 대한 활성화 설정을 할 수 있다.
|
||||
@@ -147,7 +156,7 @@ class PrincipalDetails(
|
||||
}
|
||||
|
||||
// 해당 User 의 권한을 반환하는 함수
|
||||
override fun getAuthorities(): Collection<out GrantedAuthority> {
|
||||
override fun getAuthorities(): Collection<GrantedAuthority> {
|
||||
return listOf(GrantedAuthority { user.role })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.banjjoknim.playground.domain.auth
|
||||
package com.banjjoknim.playground.daooauth.domain.auth
|
||||
|
||||
enum class OAuth2Type(
|
||||
private val provider: String,
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.banjjoknim.playground.domain.auth
|
||||
package com.banjjoknim.playground.daooauth.domain.auth
|
||||
|
||||
interface OAuth2UserInfo {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.banjjoknim.playground.domain.auth
|
||||
package com.banjjoknim.playground.daooauth.domain.auth
|
||||
|
||||
class GoogleUserInfo(
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.banjjoknim.playground.domain.user
|
||||
package com.banjjoknim.playground.daooauth.domain.user
|
||||
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.banjjoknim.playground.domain.user
|
||||
package com.banjjoknim.playground.daooauth.domain.user
|
||||
|
||||
import com.banjjoknim.playground.config.security.PrincipalDetails
|
||||
import com.banjjoknim.playground.config.security.PrincipalOAuth2UserService
|
||||
import com.banjjoknim.playground.daooauth.config.security.PrincipalDetails
|
||||
import com.banjjoknim.playground.daooauth.config.security.PrincipalOAuth2UserService
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.banjjoknim.playground.domain.user
|
||||
package com.banjjoknim.playground.daooauth.domain.user
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.banjjoknim.playground.jwt.config.filter
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import org.springframework.web.filter.CorsFilter
|
||||
|
||||
/**
|
||||
* ```kotlin
|
||||
* corsConfiguration.allowCredentials = true
|
||||
*
|
||||
* user credentials 을 허용한다. 즉, 서버가 응답을 할 때 json 을 자바스크립트에서 처리할 수 있게(응답을 받을 수 있게) 할건지 말건지를 설정한다.
|
||||
* 만약 false 로 설정할 경우, 자바스크립트로 어떤 요청을 했을 때 서버로부터 응답이 오지 않는다.
|
||||
* ```
|
||||
*
|
||||
* ```kotlin
|
||||
* corsConfiguration.addAllowedOrigin("*")
|
||||
*
|
||||
* 모든 IP에 응답을 허용한다는 설정.
|
||||
* ```
|
||||
*
|
||||
* ```kotlin
|
||||
* corsConfiguration.addAllowedHeader("*")
|
||||
*
|
||||
* 모든 Header에 응답을 허용한다는 설정.
|
||||
* ```
|
||||
*
|
||||
* ```kotlin
|
||||
* corsConfiguration.addAllowedMethod("*")
|
||||
*
|
||||
* 모든 HTTP METHOD 요청을 허용한다는 설정.
|
||||
* ```
|
||||
*
|
||||
* CorsFilter 대신 `@CrossOrigin` 어노테이션은 사용하더라도 Security 인증이 필요한 요청은 전부 거부된다.
|
||||
*
|
||||
* `@CrossOrigin` 어노테이션은 인증이 필요하지 않은 요청만 허용해준다. 예를 들어, 로그인을 해야만 할 수 있는 요청들은 모두 거부된다.
|
||||
*
|
||||
* 인증이 필요한 경우는 CorsFilter 를 Security Filter 에 등록해주어야 하고, 인증이 필요 없는 경우는 `@CrossOrigin` 어노테이션을 사용할 수 있다.
|
||||
*
|
||||
* @see org.springframework.web.cors.CorsConfiguration
|
||||
* @see org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
* @see org.springframework.web.filter.CorsFilter
|
||||
* @see org.springframework.web.bind.annotation.CrossOrigin
|
||||
*/
|
||||
@Configuration
|
||||
class CorsFilterConfiguration {
|
||||
|
||||
/**
|
||||
* Spring 에서 관리하는 Bean 으로 등록한 CorsFilter 를 Security Filter 에 등록해주어야 한다.
|
||||
*
|
||||
* 단순히 Bean 으로만 등록해서는 동작하지 않는다.
|
||||
*
|
||||
* @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration
|
||||
*/
|
||||
@Bean
|
||||
fun corsFilter(): CorsFilter {
|
||||
val corsConfigurationSource = UrlBasedCorsConfigurationSource() // URL 을 이용한 CORS 설정을 담아두는 객체.
|
||||
val corsConfiguration = CorsConfiguration() // CORS 설정 객체.
|
||||
corsConfiguration.allowCredentials = true
|
||||
corsConfiguration.addAllowedOrigin("*")
|
||||
corsConfiguration.addAllowedHeader("*")
|
||||
corsConfiguration.addAllowedMethod("*")
|
||||
corsConfigurationSource.registerCorsConfiguration("/api/**", corsConfiguration) // corsSource 에 corsConfiguration 을 등록한다.
|
||||
return CorsFilter(corsConfigurationSource)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.banjjoknim.playground.jwt.config.filter
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* 기본 설정시 Spring Security 는 일련의 Servlet Filter Chain(FilterChainProxy 라는 클래스로 등록되어 있다. 하나의 Filter 로 등록 되어있지만 내부적으로는 여러개의 Filter 가 동작하고 있다) 을 자동으로 구성한다(web tier 에 있는 Spring Security 는 Servlet Filter 에 기반을 두고 있다).
|
||||
*
|
||||
* 일반적인 웹 환경에서 브라우저가 서버에게 요청을 보내게 되면, DispatcherServlet(Controller)가 요청을 받기 이전에 많은 ServletFilter(서블릿 필터)를 거치게 된다.
|
||||
*
|
||||
* Spring Security 역시 Servlet Filter 로써 작동하며, 인증 또는 권한과 관련한 처리를 진행하게 된다.
|
||||
*
|
||||
* 본래 Servlet Filter 는 WAS(Web Application Server)에서 담당하는데 Spring 은 이 Servlet Filter 들을 직접 관리하기 위해서 DelegatingFilterProxy 를 web.xml 에 설정한다.
|
||||
*
|
||||
* 이를 통해 Spring 에서 설정된 Servlet Filter Bean 객체를 거치게 된다.
|
||||
*
|
||||
* 여기서는 스프링 시큐리티 필터체인에 필터를 추가하는 대신, 직접 필터를 만들어서 사용한다.
|
||||
*
|
||||
* 굳이 Security Filter Chain 에 필터를 추가할 필요가 없고, 이렇게 따로 만들어서 사용해도 된다.
|
||||
*
|
||||
* Filter 를 Bean 으로 등록해놓으면, 요청이 들어왔을 때 등록된 Filter 가 동작하게 된다.
|
||||
*
|
||||
* 이때, Security Filter Chain 이 우리가 직접 만든 Filter 보다 먼저 동작한다.
|
||||
*
|
||||
* 만약 우리가 만든 Filter 를 원하는 위치에서 동작하도록 하고 싶다면 원하는 위치에 Filter 를 추가하면 된다.
|
||||
*
|
||||
* 이때, Security Filter Chain 의 순서는 com.banjjoknim.playground.config.filter.SecurityFilterChain.png 이미지를 참고하자.
|
||||
*
|
||||
* - Filter type의 Bean에는 @Order 어노테이션으로 순서를 정할 수 있다.
|
||||
* - FilterRegistrationBean을 이용하여 순서를 정할 수 있다
|
||||
*
|
||||
* ```kotlin
|
||||
* http.addFilterBefore(MySecurityFilter3(), SecurityContextPersistenceFilter::class.java)
|
||||
* ```
|
||||
*
|
||||
* @see org.springframework.boot.web.servlet.FilterRegistrationBean
|
||||
* @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration
|
||||
* @see org.springframework.web.filter.DelegatingFilterProxy
|
||||
* @see org.springframework.security.web.FilterChainProxy
|
||||
*/
|
||||
@Configuration
|
||||
class CustomFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
fun customFilter1(): FilterRegistrationBean<CustomFilter1> {
|
||||
val bean = FilterRegistrationBean(CustomFilter1())
|
||||
bean.addUrlPatterns("/*") // 모든 요청에 대해 필터가 동작하도록 설정한다.
|
||||
bean.order = 0 // 필터의 순서를 정할 수 있는데, 낮은 번호가 필터중에서 가장 먼저 실행된다.
|
||||
return bean
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun customFilter2(): FilterRegistrationBean<CustomFilter2> {
|
||||
val bean = FilterRegistrationBean(CustomFilter2())
|
||||
bean.addUrlPatterns("/*") // 모든 요청에 대해 필터가 동작하도록 설정한다.
|
||||
bean.order = 1 // 필터의 순서를 정할 수 있는데, 낮은 번호가 필터중에서 가장 먼저 실행된다.
|
||||
return bean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.banjjoknim.playground.jwt.config.filter
|
||||
|
||||
import org.springframework.http.HttpMethod
|
||||
import javax.servlet.Filter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.ServletRequest
|
||||
import javax.servlet.ServletResponse
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* @see javax.servlet.Filter
|
||||
* @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration
|
||||
*/
|
||||
class CustomFilter1 : Filter {
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
println("필터1")
|
||||
chain.doFilter(request, response) // request, response 와 함께 doFilter 를 호출해주어야 한다. 그렇지 않으면 현재 필터가 진행되고나서 이 이후의 필터들은 더이상 동작하지 않게 된다.
|
||||
}
|
||||
}
|
||||
|
||||
class CustomFilter2 : Filter {
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
println("필터2")
|
||||
chain.doFilter(request, response) // request, response 와 함께 doFilter 를 호출해주어야 한다. 그렇지 않으면 현재 필터가 진행되고나서 이 이후의 필터들은 더이상 동작하지 않게 된다.
|
||||
}
|
||||
}
|
||||
|
||||
class CustomFilter3 : Filter {
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
println("필터3")
|
||||
chain.doFilter(request, response) // request, response 와 함께 doFilter 를 호출해주어야 한다. 그렇지 않으면 현재 필터가 진행되고나서 이 이후의 필터들은 더이상 동작하지 않게 된다.
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAuthorizationFilter : Filter {
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
val httpServletRequest = request as HttpServletRequest
|
||||
val httpServletResponse = response as HttpServletResponse
|
||||
|
||||
if (httpServletRequest.method == HttpMethod.POST.name) {
|
||||
println("POST 요청됨")
|
||||
val headerAuthorization = httpServletRequest.getHeader("Authorization")
|
||||
println(headerAuthorization)
|
||||
|
||||
// banjjoknim 이 정상적인 토큰이라고 가정한다.
|
||||
// 따라서 banjjoknim 이라는 토큰을 id, password 가 정상적으로 입력되었을 때 토큰을 만들어주고 응답에 실어보낸다.
|
||||
// 클라이언트는 요청할 때마다 해당 토큰을 Header 중 Authorization 의 값으로 포함하여 요청한다.
|
||||
// 따라서 클라이언트가 요청할 때 Authorization 의 값으로 포함되어 온 토큰이 우리 서버에서 만든 토큰이 맞는지 검증만 하면 된다(RSA, HS256).
|
||||
if (headerAuthorization == "banjjoknim") {
|
||||
chain.doFilter(httpServletRequest, httpServletResponse) // request, response 와 함께 doFilter 를 호출해주어야 한다. 그렇지 않으면 현재 필터가 진행되고나서 이 이후의 필터들은 더이상 동작하지 않게 된다.
|
||||
} else {
|
||||
val printWriter = httpServletResponse.writer
|
||||
printWriter.println("인증안됨") // 응답에 '인증안됨' 을 쓴다.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.banjjoknim.playground.jwt.config.filter
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.banjjoknim.playground.jwt.config.security.JwtSecurityProperties
|
||||
import com.banjjoknim.playground.jwt.config.security.PrincipalDetails
|
||||
import com.banjjoknim.playground.jwt.domain.user.JwtUser
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import java.util.Date
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Spring Security 에는 UsernamePasswordAuthenticationFilter 가 있다.
|
||||
*
|
||||
* 기본적으로는 /login 요청에서 username, password 를 전송하면 (post 요청) UsernamePasswordAuthenticationFilter 가 동작한다.
|
||||
*
|
||||
* 하지만 우리는 formLogin().disable() 설정을 해주었기 때문에 직접 Filter 를 만들어서 Security 설정에 등록해주어야 한다. 그래야 Security 에서 UserDetailsService 를 호출할 수 있다.
|
||||
*
|
||||
* 단, Security 에 등록해줄 때 AuthenticationManager 와 함께 등록해주어야 한다. AuthenticationManager 를 통해서 로그인이 진행되기 때문이다.
|
||||
*
|
||||
* 참고로, AuthenticationManager 는 WebSecurityConfigurerAdapter 가 들고 있고, 그 녀석을 사용하면 된다.
|
||||
*
|
||||
* AuthenticationManager 는 AbstractAuthenticationProcessingFilter 또한 가지고 있으므로
|
||||
* UsernamePasswordAuthenticationFilter 를 상속받아서 사용하는 대신 AbstractAuthenticationProcessingFilter 를 상속받아서 사용해도 된다.
|
||||
*
|
||||
* @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
* @see org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
|
||||
* @see org.springframework.security.authentication.AuthenticationManager
|
||||
* @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
* @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration
|
||||
*/
|
||||
class JwtAuthenticationFilter(
|
||||
private val authenticationManagerFromSecurityConfiguration: AuthenticationManager // authenticationManager 로 변수명을 지으면 이름이 겹쳐서 컴파일 에러가 발생하여 변수명 변경.
|
||||
) : UsernamePasswordAuthenticationFilter() {
|
||||
|
||||
/**
|
||||
* 기존의 /login URL로 요청을 하면 로그인 시도를 위해 호출되는 함수이다.
|
||||
*
|
||||
* 추상 메서드로, AbstractAuthenticationProcessingFilter 에 포함되어 있으며,
|
||||
* AbstractAuthenticationProcessingFilter 를 상속받은 UsernamePasswordAuthenticationFilter, OAuth2LoginAuthenticationFilter 등이 구현하고 있다.
|
||||
*
|
||||
* AbstractAuthenticationProcessingFilter#doFilter(HttpServletRequest, HttpServletResponse) 에서 내부적으로 호출하고 있다.
|
||||
*
|
||||
* /login URL로 요청을 하면 UsernamePasswordAuthenticationFilter 가 해당 요청을 낚아채서 아래의 함수가 자동으로 실행된다.
|
||||
*
|
||||
* 따라서 구현해줘야 하는 것들은 아래와 같다.
|
||||
*
|
||||
* 1. username & password 를 받는다.
|
||||
* 2. 포함하고 있는 AuthenticationManager로 정상인지 로그인 시도를 한다.
|
||||
* 3. 로그인 시도를 하면 우리가 만든 PrincipalDetailsService#loadUserByUsername(String) 이 호출된다.
|
||||
* - 데이터베이스로부터 일치하는 id, password 가 있는지 검사한다.
|
||||
* - 로직이 정상적으로 완료되면 로그인을 시도한 유저의 정보를 담고 있는 Authentication 객체가 반환된다.
|
||||
* 4. 정상적으로 로직이 수행되어서 Authentication 객체가 리턴되면 해당 객체를 리턴해서 Spring Security 세션에 담는다.
|
||||
* - 만약 세션에 Authentication 객체를 담지 않으면 Spring Security 에서의 권한관리가 동작하지 않는다.
|
||||
* - Spring Security 는 세션에 Authentication 객체가 존재해야 권한관리를 해준다.
|
||||
* - 만약 Spring Security 를 통해 권한관리를 안할거면 Authentication 객체를 세션에 담을 필요가 없다.
|
||||
* 5. 마지막으로 JWT 토큰을 만들어서 응답으로 돌려주면 된다(선택-successfulAuthentication() 을 override 해서 구현해줘도 됨).
|
||||
*
|
||||
* @see org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
|
||||
* @see org.springframework.security.authentication.AuthenticationManager
|
||||
* @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
* @see org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter
|
||||
* @see com.banjjoknim.playground.jwt.config.security.PrincipalDetailsService
|
||||
* @see org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
*/
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
println("JwtAuthenticationFilter : 로그인 시도중")
|
||||
|
||||
// println(request.inputStream) // username, password 가 담겨있다. request의 inputStream 은 Request 당 1회만 호출할 수 있으므로 주석처리.
|
||||
|
||||
// val bufferedReader = request.reader
|
||||
// bufferedReader.lineSequence().forEach(::println) // request 데이터 확인
|
||||
|
||||
val objectMapper = ObjectMapper().registerKotlinModule()
|
||||
val jwtUser = objectMapper.readValue(request.inputStream, JwtUser::class.java)
|
||||
// println(jwtUser)
|
||||
|
||||
// 로그인 시도를 위해서 id, password 를 이용해서 직접 토큰을 만든다.
|
||||
// UsernamePasswordAuthenticationFilter#attemptAuthentication() 함수를 참고하도록 한다.
|
||||
// 즉, 우리가 직접 토큰을 만들어서 호출을 대신 수행해준다고 보면 될듯.
|
||||
val authenticationToken = UsernamePasswordAuthenticationToken(jwtUser.username, jwtUser.password)
|
||||
|
||||
// 직접 만든 토큰을 인자로 넣고 AuthenticationManager#authenticate(Token) 을 호출하면
|
||||
// 내부적으로 로직이 돌면서 우리가 만든 PrincipalDetailsService#loadUserByUsername(String) 함수가 호출된다.
|
||||
// 그 결과로 User의 로그인 정보가 담긴 Authentication 객체를 얻을 수 있다.
|
||||
// Authentication 객체를 얻어다는 것은 데이터베이스에 있는 username 과 password 가 일치한다는 뜻이다.
|
||||
val authentication = authenticationManagerFromSecurityConfiguration.authenticate(authenticationToken)
|
||||
|
||||
// 위 처럼 인증이 정상적으로 진행되어 Authentication 객체를 얻었다면
|
||||
// 아래처럼 Authentication 객체 내부의 PrincipalDetails 객체를 꺼내어 정보 확인이 가능하다.
|
||||
// 즉, 로그인이 정상적으로 되었다는 뜻이다.
|
||||
val principalDetails = authentication.principal as PrincipalDetails
|
||||
println("로그인 완료됨: ${principalDetails.user.username}")
|
||||
|
||||
// return super.attemptAuthentication(request, response)
|
||||
|
||||
// 로그인이 정상적으로 되었으므로 Authentication 객체를 Session 영역에 저장해야 한다.
|
||||
// Authentication 객체를 Session 영역에 저장하는 방법은 Authentication 객체를 return 해주는 것이다.
|
||||
// Authentication 객체를 return 해주면 Spring Security 가 자동으로 Authentication 객체를 Security Session 영역에 저장해준다.
|
||||
// Authentication 객체를 return 해서 Session 영역에 저장하는 이유는 권한 관리를 Spring Security 가 대신 해주어 관리가 편해지기 때문이다(원하지 않으면 Session 영역에 저장을 안하면 된다).
|
||||
// JWT 토큰을 사용한다면 Session 영역을 굳이 만들 필요가 없다. 다만, 권한 처리 때문에 Session 에 저장하는 것이다.
|
||||
// 기본적으로 Authentication 객체를 세션에 저장하는 로직은 AbstractAuthenticationProcessingFilter#successfulAuthentication() 함수에서 수행하고 있다.
|
||||
// Security Session 영역에 저장되는 정보들은 잠시 사용하고 응답이 끝났을 때 버리면 된다(세션 정보는 시간이 지나면 자동으로 사라진다).
|
||||
return authentication
|
||||
}
|
||||
|
||||
/**
|
||||
* 본 함수는 attemptAuthentication() 함수를 통한 인증이 성공적으로 이루어져서 Authentication 객체를 얻을 수 있는 경우 그 다음으로 호출되는 함수다.
|
||||
*
|
||||
* AbstractAuthenticationProcessingFilter#successfulAuthentication() 함수에는 Security Session 영역에 Authentication 객체를 저장하는 로직이 포함되어 있다.
|
||||
*
|
||||
* 자세한 내용은 AbstractAuthenticationProcessingFilter#successfulAuthentication() 에 달린 javadoc 을 참고하도록 하자.
|
||||
*
|
||||
* 따라서, 여기서 JWT 토큰을 만들어서 Request 요청한 사용자에게 JWT 토큰을 응답해주면 된다(선택사항).
|
||||
*
|
||||
* 기존의 username, password 방식의 로그인을 사용할 경우, 스프링 시큐리티는 세션이 유효할 경우 인증이 필요한 페이지의 권한을 체크해서 알아서 인증이 필요한 페이지로 이동시켜준다.
|
||||
*
|
||||
* 기존의 서버는 세션의 유효성 검증을 할 때 Session.getAttribute("세션값 확인") 와 같은 방식으로 확인하기만 하면 된다.
|
||||
*
|
||||
* 하지만 토큰 방식을 사용하게되면, 세션ID도 만들지 않고, 쿠키도 응답에 제공해주지 않는다(세션에 데이터를 담아둘게 아니다).
|
||||
*
|
||||
* 대신, JWT 토큰을 생성하고 클라이언트쪽으로 JWT 토큰을 응답해준다. 따라서 요청할 때마다 JWT 토큰을 가지고 요청해야 한다.
|
||||
*
|
||||
* 따라서 서버는 JWT 토큰이 유효한지를 판단해야하는데, 이 부분에 대한 필터를 따로 만들어주어야 한다.
|
||||
*/
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
|
||||
authResult: Authentication
|
||||
) {
|
||||
val principalDetails = authResult.principal as PrincipalDetails
|
||||
println("successfulAuthentication 실행됨 : ${principalDetails.user.username}의 인증이 완료되었다는 뜻.")
|
||||
|
||||
// RSA 방식은 아니다. Hash 암호 방식.
|
||||
val jwtToken = JWT.create()
|
||||
.withSubject("banjjoknim 토큰")
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + JwtSecurityProperties.EXPIRATION_TIME_SECONDS))
|
||||
.withClaim("id", principalDetails.user.id)
|
||||
.withClaim("username", principalDetails.user.username)
|
||||
.sign(Algorithm.HMAC512(JwtSecurityProperties.SECRET)) // 서버에서만 알고 있는 비밀 키를 사용한다.
|
||||
|
||||
response.addHeader(HttpHeaders.AUTHORIZATION, "Bearer $jwtToken")
|
||||
|
||||
super.successfulAuthentication(request, response, chain, authResult)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.banjjoknim.playground.jwt.config.filter
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.banjjoknim.playground.jwt.config.security.JwtSecurityProperties
|
||||
import com.banjjoknim.playground.jwt.config.security.PrincipalDetails
|
||||
import com.banjjoknim.playground.jwt.domain.user.JwtUserRepository
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* 로그인을 통해 발행된 JWT 토큰의 전자서명을 이용해서 개인정보에 접근할 수 있게 하기 위한 커스텀 필터.
|
||||
*
|
||||
* Security 가 가진 Filter 중에서 BasicAuthenticationFilter 라는 것이 있다.
|
||||
*
|
||||
* 권한이나 인증이 필요한 특정 URL 을 요청했을 때 위 BasicAuthenticationFilter 를 무조건 거치게 되어 있다.
|
||||
*
|
||||
* 만약 권한이나 인증이 필요한 주소가 아니라면 위 필터를 거치지 않는다. 따라서 BasicAuthenticationFilter 를 상속받아서 필요한 로직을 구현해준다.
|
||||
*
|
||||
* @see org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
*/
|
||||
class JwtAuthorizationFilter(
|
||||
private val authenticationManagerFromSecurityConfiguration: AuthenticationManager,
|
||||
private val jwtUserRepository: JwtUserRepository
|
||||
) :
|
||||
BasicAuthenticationFilter(authenticationManagerFromSecurityConfiguration) {
|
||||
/**
|
||||
* 인증이나 권한이 필요한 URL 요청이 있을 때 BasicAuthenticationFilter#doFilterInternal() 함수를 거치게 된다.
|
||||
*
|
||||
* 따라서 이 함수에서 Header 의 JWT 토큰에 대한 처리를 진행해주면 된다.
|
||||
*/
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
println("인증이나 권한이 필요한 주소가 요청됨.")
|
||||
|
||||
val jwtHeader = request.getHeader(HttpHeaders.AUTHORIZATION)
|
||||
println("jwtHeader: $jwtHeader")
|
||||
|
||||
// Header의 JWT 토큰이 정상적인지 검사한다.
|
||||
if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
// JWT 토큰을 검증해서 정상적인 사용자인지 검사한다.
|
||||
val jwtToken = jwtHeader.replace(JwtSecurityProperties.BEARER_TOKEN_PREFIX, "")
|
||||
val jwtVerifier = JWT.require(Algorithm.HMAC512(JwtSecurityProperties.SECRET)).build()
|
||||
val username = jwtVerifier.verify(jwtToken).getClaim("username").asString()
|
||||
|
||||
// username 이 null 이 아니라면 서명이 정상적으로 된 것이다.
|
||||
if (username != null) {
|
||||
println("username 정상. username: $username")
|
||||
val jwtUser = jwtUserRepository.findByUsername(username)
|
||||
?: throw IllegalArgumentException("can not found jwtUser. username: $username")
|
||||
val principalDetails = PrincipalDetails(jwtUser)
|
||||
|
||||
// Authentication 객체를 강제로 만든다. password 의 경우는 null 을 사용해도 무방하다. 우리가 직접 Authentication 객체를 만들기 때문이다.
|
||||
// 이게 가능한 이유는, username 이 null 이 아니기 때문인데, username 이 null 이 아니라는 것은 인증이 정상적으로 진행되었다는 뜻이기 때문.
|
||||
// 단, 이렇게 Authentication 객체를 만들 때는 권한을 직접 알려주어야(지정해주어야) 한다.
|
||||
// 즉, JWT 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
|
||||
val authenticationToken =
|
||||
UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.authorities)
|
||||
|
||||
// SecurityContext 는 Security 의 세션 공간이다.
|
||||
val securityContext = SecurityContextHolder.getContext()
|
||||
|
||||
// 강제로 Security 의 세션에 접근하여 Authentication 객체를 저장한다. 만약 세션에 저장이 제대로 되면 Authentication 객체를 Controller 단에서 가져올 수 있다.
|
||||
securityContext.authentication = authenticationToken
|
||||
}
|
||||
|
||||
// super.doFilterInternal(request, response, chain) // 아래의 chain.doFilter(request, response) 에서도 응답을 하기 때문에 응답을 총 2번하게 되어 오류가 나므로 지워줘야 한다.
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user