8 Commits

Author SHA1 Message Date
Colt
c6a8493aa3 SOLID, Design Pattern 샘플 코드 추가 (#17)
* initial commit

* feat : SOLID - SRP 샘플 코드 구현

* feat : SOLID - OCP 샘플 코드 구현

* feat : SOLID - LSP 샘플 코드 구현

* feat : SOLID - ISP 샘플 코드 구현

* feat : SOLID - DIP 샘플 코드 구현

* docs : README.md Design Pattern 항목 추가

* feat : Design Pattern - 전략(Strategy) 패턴 구현

* feat : Design Pattern - 템플릿 메서드(Template Method) 패턴 구현

* feat : Design Pattern - 상태(State) 패턴 구현

* feat : Design Pattern - 프록시(Proxy) 패턴 구현

* feat : Design Pattern - 어댑터(Adapter) 패턴 구현

* feat : Design Pattern - 옵저버(Observer) 패턴 구현

* feat : Design Pattern - 파사드(Facade) 패턴 구현

* feat : Design Pattern - 추상 팩토리 패턴(Abstract Factory) 패턴 구현

* feat : Design Pattern - 컴포지트(Composite) 패턴 구현

* feat : Design Pattern - 미디에이터(Mediator) 패턴 구현

* feat : Design Pattern - 널 객체(Null Object) 패턴 구현

* feat : Design Pattern - 데코레이터(Decorator) 패턴 구현
2022-11-16 01:36:13 +09:00
Colt
ec46451143 Spring cloud open feign 예제 코드 추가 (#16)
* initial commit

* add README.md

* add application.yaml

* add Member Domain Class

* add content in README.md

* Member Domain class 삭제

* README.md 요구사항 수정 및 참고자료 추가

* feat : 게시글 조회 FeignClient 컴포넌트 추가

* refactor : 게시글 클라이언트 컴포넌트 value 변경

* refactor : 게시글 클라이언트 컴포넌트 value 변경 및 주석 추가

* feat : Logger 설정 추가
2022-11-01 15:50:08 +09:00
Colt
8a0df3a322 graphql-kotlin Custom Scalars Type 예제 코드 추가 (#15)
* build : graphql-java-extended-scalars 의존성 추가

* feat : GraphQL 에서 기본적으로 지원하지 않는 Long, java.time 관련 타입 Scalars 생성 훅 설정 추가

* refactor : Long, LocalDateTime 타입 프로퍼티 추가

* feat : @GraphQLName 어노테이션 예제 코드 추가
2022-08-04 12:20:10 +09:00
Colt
6aa4913294 Graphql kotlin 예제 코드 테스트 추가 (#14)
* test : Person Query 테스트 추가
2022-08-02 17:03:57 +09:00
Colt
e05abb53f1 GraphQL Kotlin 예제 코드 추가 (#13)
* graphql-kotlin initial commit

* docs : README.md 및 공식 문서 링크 추가

* build : Spring Web MVC(servlet) stack -> Spring WebFlux(reactive) stack 으로 의존성 변경

* build : GraphQL Kotlin Spring Server 의존성 추가

* add graphql package configuration

* docs : graphql package 설정 설명 주석 추가

* docs : GraphQL Kotlin 공식 문서 링크 및 제목 수정

* feat : Schema, Query, Mutation, Subscription 추가

* docs : README.md 항목 수정

* feat : GraphQL Context 구현체 추가
2022-08-02 15:30:28 +09:00
Colt
c659243c32 [Gradle + Spring Multi Module] 예제 코드 추가 (#12)
* initial commit

* refactor : src 디렉토리 삭제

* build : module-api 추가 및 build.gradle.kts 설정 변경

* build : module-domain 추가 및 build.gradle.kts 설정 변경

* feat : module-api 에 SpringBootApplication 실행 파일 추가

* build : module-api spring-data-jpa 의존성 module-domain 으로 이관

* build : 최상위 build.gradle.kts 에서 subprojects 들에 `org.springframework.boot`, `io.spring.dependency-management` 플러그인 적용하도록 변경

* feat : User Entity 추가

* feat(application.yml) : spring datasource, jpa, h2 설정 추가

* feat(user) : 회원 등록 기능 구현

* build : build.gradle.kts 코틀린 테스트 라이브러리 추가

* fix : 어노테이션 및 이름 수정

* test : 회원 등록 기능 테스트 추가
2022-05-03 16:31:24 +09:00
Colt
a52e454a26 [만들면서 배우는 클린 아키텍처] 부적합한 테스트 예제 코드 삭제 (#11)
* 만들면서 배우는 클린 아키텍처 initial commit

* refactor : 프로젝트 진입점 클래스 이름 변경

* docs : README.md 헥사고날 아키텍처 항목 추가

* docs(README.md) : 내용 정리 추가

* feat(user.domain) : 도메인 모델 User 추가

* feat(user.domain) : User 의 nickname 프로퍼티 값 객체로 포장

* refactor(User) : 닉네임 변경 함수 이름 수정

* test(user.domain) : 회원 닉네임 변경 테스트 추가

* chore : DB 설정 추가

* feat(user.adapter) : User Entity 구현

* feat : User 닉네임 변경 기능 추가

* refactor(user) : domain 패키지 내부 패키지 구성 추가 및 Entity, Model 이관

* refactor : 사용하지 않는 파일 삭제

* refactor : User 닉네임 변경 기능 컴포넌트 이름 변경

* refactor : User 닉네임 변경 기능 in port 이름 변경

* feat : User Upsert Port 및 Adapter 구현, Service 로직에 추가

* chore : Hexagonal Architecture Process 이미지 추가

* docs(README.md) : 요구사항, 구현 항목 추가

* refactor : 패키지 구성 변경

* feat(user.adapter) : UserMapper 추가 및 적용

* docs(README.md) : 참고자료 및 구현 항목 내용 추가

* refactor : ChangeNicknameRequest, ChangeNicknameResponse 패키지 변경

* refactor : adapter 계층만 application 계층에 의존하도록 통신 객체 추가 및 적용

* refactor : UserEntity @Table 이름 적용

* docs(README.md) : 구현 항목 내용 추가

* refactor : Nickname 입력 유효성 검사 ChangeNicknameRequest 에서 수행하도록 변경

* refactor(user.pojo) : 불필요한 테스트 삭제

* refactor(UserTest) : 오탈자 수정

* build : Kotlin 테스트 라이브러리 추가

* test(user.application) : 닉네임 변경 테스트 추가

* test(user.adapter) : 회원 조회 테스트 추가

* refactor : 불필요한 파일 삭제

* test(user.adapter) : 회원 상태 저장 또는 수정 테스트 추가

* test(user.adapter) : User POJO <-> User Entity 매핑 테스트 추가

* test(user.adapter) : 닉네임 변경 Web Adapter 테스트 추가

* refactor : 불필요한 테스트 파일 삭제

* refactor(user) : 닉네임 변경 테스트 케이스 출력 이름 변경

* refactor : 부적합한 테스트 삭제
2022-04-26 21:01:42 +09:00
Colt
c5e5a3047b [만들면서 배우는 클린 아키텍처] 테스트 예제 코드 보충 (#10)
* 만들면서 배우는 클린 아키텍처 initial commit

* refactor : 프로젝트 진입점 클래스 이름 변경

* docs : README.md 헥사고날 아키텍처 항목 추가

* docs(README.md) : 내용 정리 추가

* feat(user.domain) : 도메인 모델 User 추가

* feat(user.domain) : User 의 nickname 프로퍼티 값 객체로 포장

* refactor(User) : 닉네임 변경 함수 이름 수정

* test(user.domain) : 회원 닉네임 변경 테스트 추가

* chore : DB 설정 추가

* feat(user.adapter) : User Entity 구현

* feat : User 닉네임 변경 기능 추가

* refactor(user) : domain 패키지 내부 패키지 구성 추가 및 Entity, Model 이관

* refactor : 사용하지 않는 파일 삭제

* refactor : User 닉네임 변경 기능 컴포넌트 이름 변경

* refactor : User 닉네임 변경 기능 in port 이름 변경

* feat : User Upsert Port 및 Adapter 구현, Service 로직에 추가

* chore : Hexagonal Architecture Process 이미지 추가

* docs(README.md) : 요구사항, 구현 항목 추가

* refactor : 패키지 구성 변경

* feat(user.adapter) : UserMapper 추가 및 적용

* docs(README.md) : 참고자료 및 구현 항목 내용 추가

* refactor : ChangeNicknameRequest, ChangeNicknameResponse 패키지 변경

* refactor : adapter 계층만 application 계층에 의존하도록 통신 객체 추가 및 적용

* refactor : UserEntity @Table 이름 적용

* docs(README.md) : 구현 항목 내용 추가

* refactor : Nickname 입력 유효성 검사 ChangeNicknameRequest 에서 수행하도록 변경

* refactor(user.pojo) : 불필요한 테스트 삭제

* refactor(UserTest) : 오탈자 수정

* build : Kotlin 테스트 라이브러리 추가

* test(user.application) : 닉네임 변경 테스트 추가

* test(user.adapter) : 회원 조회 테스트 추가

* refactor : 불필요한 파일 삭제

* test(user.adapter) : 회원 상태 저장 또는 수정 테스트 추가

* test(user.adapter) : User POJO <-> User Entity 매핑 테스트 추가

* test(user.adapter) : 닉네임 변경 Web Adapter 테스트 추가

* refactor : 불필요한 테스트 파일 삭제

* refactor(user) : 닉네임 변경 테스트 케이스 출력 이름 변경
2022-04-26 20:57:48 +09:00
256 changed files with 5289 additions and 30 deletions

View 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/

View 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)

View 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()
}

View 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

View 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" "$@"

View 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

View File

@@ -0,0 +1 @@
rootProject.name = "graphql-kotlin"

View File

@@ -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")
)
}
}

View File

@@ -1,11 +1,11 @@
package com.banjjoknim.playground package com.banjjoknim.graphqlkotlin
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication
class LearnWithMakingCleanArchitectureApplication class GraphqlKotlinApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {
runApplication<LearnWithMakingCleanArchitectureApplication>(*args) runApplication<GraphqlKotlinApplication>(*args)
} }

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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()
)

View File

@@ -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
}
}
}

View File

@@ -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())
}

View File

@@ -0,0 +1,6 @@
package com.banjjoknim.graphqlkotlin.person
interface PersonRepository {
fun findPerson(name: String): Person?
}

View File

@@ -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]
}
}

View File

@@ -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") }
}
}

View File

@@ -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"

View File

@@ -1,10 +1,10 @@
package com.banjjoknim.playground package com.banjjoknim.graphqlkotlin
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest @SpringBootTest
class LearnWithMakingCleanArchitectureApplicationTests { class GraphqlKotlinApplicationTests {
@Test @Test
fun contextLoads() { fun contextLoads() {

View File

@@ -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)
}
}
}
}

View File

@@ -25,11 +25,15 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")
runtimeOnly("mysql:mysql-connector-java") runtimeOnly("mysql:mysql-connector-java")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client") runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
// testImplementation("org.springframework.security:spring-security-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> { tasks.withType<KotlinCompile> {

View File

@@ -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() }
}
}
}
}

View File

@@ -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"))
}
}
}

View File

@@ -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"))
}
}
}

View File

@@ -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"))
}
}
}

View File

@@ -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"))
}
}
}

View File

@@ -1,24 +0,0 @@
package com.banjjoknim.cleanarchitecture.user.pojo
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
class NicknameTest {
@DisplayName("닉네임 생성 테스트")
@Nested
inner class ChangeNicknameTestCases {
@Test
fun `10글자 이내이면 닉네임을 생성할 수 있다`() {
assertDoesNotThrow { Nickname("banjjoknim") }
}
@Test
fun `10글자가 넘으면 닉네임을 생성할 경우 예외가 발생한다`() {
assertThrows<IllegalArgumentException> { Nickname("i'm banjjoknim") }
}
}
}

View File

@@ -7,7 +7,8 @@ import org.junit.jupiter.api.Test
class UserTest { class UserTest {
@DisplayName("회원 이름 변경 테스트 케이스")
@DisplayName("회원 닉네임 변경 테스트 케이스")
@Nested @Nested
inner class ChangeNicknameTestCases { inner class ChangeNicknameTestCases {
@Test @Test

View 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/

View File

@@ -0,0 +1,178 @@
# SOLID & Design Pattern Sample
## SOLID
- SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다.
- 각 항목별 before & after 를 비교하면서 어떤 차이가 있는지 생각해본다.
- before & after 에 새로운, 또는 동일한 규칙(비즈니스 로직, 유효성 검사 등)을 적용해야 한다고 가정하고 변경을 시도해본다.
### SRP(Single Responsibility Principle)
- 단일 책임 원칙
- 한 클래스는 하나의 책임만 가져야 한다. 즉, 한 클래스가 변경되는 이유는 한 가지여야 한다.
#### 구현 요구사항
- 예금주의 이름으로 계좌를 생성할 수 있다.
- 계좌에 입금할 수 있다.
- 계좌에서 출금할 수 있다.
### OCP(Open/Closed Principle)
- 개방-폐쇄 원칙
- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
#### 구현 요구사항
- 최초의 결제방식은 현금, 카드 두 가지가 있다.
- 쿠폰 결제방식을 추가한다고 가정한다.
- 쿠폰으로 결제할 경우 잔고가 차감되지 않는다.
### LSP(Liskov Substitution Principle)
- 리스코프 치환 원칙
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
#### 구현 요구사항
- 좌표를 입력하면 해당 좌표로 이동수단의 위치가 변한다.
### ISP(Interface Segregation Principle)
- 인터페이스 분리 원칙
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
#### 구현 요구사항
- 영화 상영관 좌석 현황을 조회할 수 있다.
- 영화표 예매를 진행할 수 있다.
### DIP(Dependency Inversion Principle)
- 의존 역전 원칙
- 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
#### 구현 요구사항
- 저장소에서 파일 데이터를 불러올 수 있다.
- 하나의 저장소만 사용하며 저장소의 타입은 언제든지 변경될 수 있다.
- 각각의 저장소는 각자만의 파일 데이터 조회 요청 인터페이스를 제공한다.
- 아마존에서는 파일 이름을 요구한다.
- 구글에서는 파일 번호를 요구한다.
## Design Pattern
### 전략(Strategy) 패턴
#### 구현 요구사항
- 연령대별로 물건의 할인된 금액 계산기를 구현한다.
- 아이일 경우 할인율은 15%이다.
- 어른일 경우 할인율은 30%이다.
### 템플릿 메서드(Template Method) 패턴
#### 구현 요구사항
- 스포츠 강사를 구현한다.
- 모든 강사는 강의가 시작할 때 스트레칭을 한다.
- 모든 강사는 강의가 끝날 때 인사한다.
- 수영 강사는 강의 중간에 수영 동작을 보여준다.
- 축구 강사는 강의 중간에 축구 동작을 보여준다.
### 상태(State) 패턴
#### 구현 요구사항
- 상태별로 다르게 움직이는 게임 캐릭터를 구현한다.
- 기본적으로 게임 캐릭터는 정지상태이다.
- 정지 상태에서 버튼을 누르면 이동한다.
- 이동하는 상태에서 버튼을 누르면 점프한다.
- 점프하는 상태에서 버튼을 누르면 착지한 뒤 정지한다.
- 게임 캐릭터는 버튼을 누르면 동작하는 상태로 스스로의 상태를 변경한다.
### 데코레이터(Decorator) 패턴
#### 구현 요구사항
- 용사는 검을 가진다.
- 용사는 검을 바꿀 수 있다.
- 검에는 속성이 부여될 수 있으며, 속성에 따라 공격이 변한다.
- 용사가 공격하면 소지한 무기의 공격이 발동된다.
### 프록시(Proxy) 패턴
#### 구현 요구사항
- 지갑의 주인은 지갑에 대해 입금 또는 출금을 진행할 수 있다.
- 지갑의 동작은 입금 및 출금만 가능하도록 제한된다.
- 지갑에 대해 입금 또는 출금이 이루어질 때 메시지를 출력한다.
### 어댑터(Adapter) 패턴
#### 구현 요구사항
- 발전소는 정해진 요청 규격에 따라 전기를 생산한다.
- 가정은 정해진 규격에 따라 전기를 얻는다.
- 발전소와 가정에서 사용하는 전기는 각각 다르다.
- 발전소에서 생산한 전기를 가정용으로 변환하는 계산식은 아래와 같다.
- 생산된 전류량 * (발전소 전압 / 가정용 전압)
- 발전소에서 생산한 전기의 전압은 11000V 이다.
- 가정에서 사용하는 전기의 전압은 220V 이다.
### 옵저버(Observer) 패턴
#### 구현 요구사항
- 주문의 상태가 변경되면 배송팀과 운영팀에 알리고, 각각의 팀은 그에 따른 조치를 취한다.
- 배송팀은 배송을 시작한다.
- 운영팀은 고객에게 메시지를 전송한다.
- 배송팀과 운영팀 외에 다른 팀에도 필요하다면 또 다른 팀에 메시지를 전송해야 할 수도 있다.
### 미디에이터(Mediator) 패턴
#### 구현 요구사항
- 가전제품의 전원을 제어한다.
- 가전제품의 전원은 하나만 켤 수 있고, 하나를 켜면 나머지는 모두 꺼진다.
- 가전제품을 켤 경우, 해당 가전제품의 이름을 메시지로 전송한다.
### 파사드(Facade) 패턴
#### 구현 요구사항
- 회원의 모든 정보를 조회한다.
- 회원의 모든 정보에는 회원의 기본 정보와 친구들에 대한 정보가 있다.
- 회원의 기본 정보에는 이름, 나이가 있다.
- 회원의 친구 정보에는 친구의 이름, 나이가 있다.
### 추상 팩토리(Abstract Factory) 패턴
#### 구현 요구사항
- 과일 이름을 입력하면 해당 과일 또는 과일주스를 만들어주는 마법이 있다.
- 과일의 종류는 사과, 바나나, 오렌지로 총 세 가지다.
- 과일주스의 종류도 마찬가지로 사과주스, 바나나주스, 오렌즈주스로 총 세 가지다.
### 컴포지트(Composite) 패턴
#### 구현 요구사항
- 프랜차이즈 기업의 정산은 가맹점으로 가입된 모든 가게들의 정산을 포함한다.
- 프랜차이즈 기업은 다른 프랜차이즈 기업을 포함할 수 있다.
- 프랜차이즈 기업은 이름을 갖는다.
- 프랜차이즈 가맹점은 이름을 갖는다.
### 널 객체(Null Object) 패턴
#### 구현 요구사항
- 식당에서는 직원을 이름으로 관리하며 동명이인은 없다고 가정한다.
- 이름으로 직원을 찾을 수 있고, 해당 직원에게 근무일자를 할당할 수 있다.
- 직원은 근무일자를 고지 받으면 해당 날짜를 기억한다.
## 참고자료
- [SOLID (객체 지향 설계)](https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84))

View File

@@ -0,0 +1,39 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.7.5"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
kotlin("plugin.jpa") version "1.6.21"
}
group = "com.banjjoknim"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
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")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View 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" "$@"

View 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

View File

@@ -0,0 +1 @@
rootProject.name = "solid-design-pattern-sample"

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class SolidDesignPatternSampleApplication
fun main(args: Array<String>) {
runApplication<SolidDesignPatternSampleApplication>(*args)
}

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.after
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Apple
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.AppleJuice
class AppleMagic : Magic {
override fun createFruit() {
val apple = Apple()
println("과일 [${apple.name}]를 만들었습니다!")
}
override fun createJuice() {
val appleJuice = AppleJuice()
println("과일주스 [${appleJuice.name}]을 만들었습니다!")
}
}

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.after
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Banana
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.BananaJuice
class BananaMagic : Magic {
override fun createFruit() {
val banana = Banana()
println("과일 [${banana.name}]를 만들었습니다!")
}
override fun createJuice() {
val bananaJuice = BananaJuice()
println("과일주스 [${bananaJuice.name}]을 만들었습니다!")
}
}

View File

@@ -0,0 +1,35 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.after
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.FruitType
interface Magic {
companion object {
fun getMagic(fruitType: FruitType): Magic {
return when (fruitType) {
FruitType.APPLE -> AppleMagic()
FruitType.BANANA -> BananaMagic()
FruitType.ORANGE -> OrangeMagic()
}
}
}
fun createFruit()
fun createJuice()
}
//abstract class Magic {
// companion object {
// fun getMagic(fruitType: FruitType): Magic {
// return when (fruitType) {
// FruitType.APPLE -> AppleMagic()
// FruitType.BANANA -> BananaMagic()
// FruitType.ORANGE -> OrangeMagic()
// }
// }
// }
//
// abstract fun createFruit()
//
// abstract fun createJuice()
//}

View File

@@ -0,0 +1,12 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.after
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.FruitType
class MagicApplication
fun main() {
val fruitType = FruitType.APPLE
val magic = Magic.getMagic(fruitType)
magic.createFruit()
magic.createJuice()
}

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.after
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Orange
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.OrangeJuice
class OrangeMagic : Magic {
override fun createFruit() {
val orange = Orange()
println("과일 [${orange.name}]를 만들었습니다!")
}
override fun createJuice() {
val orangeJuice = OrangeJuice()
println("과일주스 [${orangeJuice.name}]을 만들었습니다!")
}
}

View File

@@ -0,0 +1,29 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.before
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Apple
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.AppleJuice
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Banana
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.BananaJuice
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.FruitType
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.Orange
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.OrangeJuice
class Magic {
fun createFruit(type: FruitType) {
val fruit = when (type) {
FruitType.APPLE -> Apple()
FruitType.BANANA -> Banana()
FruitType.ORANGE -> Orange()
}
println("과일 [${fruit.name}]를 만들었습니다!")
}
fun createJuice(type: FruitType) {
val juice = when (type) {
FruitType.APPLE -> AppleJuice()
FruitType.BANANA -> BananaJuice()
FruitType.ORANGE -> OrangeJuice()
}
println("과일주스 [${juice.name}]을 만들었습니다!")
}
}

View File

@@ -0,0 +1,12 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.before
import com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common.FruitType
class MagicApplication
fun main() {
val magic = Magic()
val fruitType = FruitType.APPLE
magic.createFruit(fruitType)
magic.createJuice(fruitType)
}

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common
enum class FruitType {
APPLE,
BANANA,
ORANGE
}

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common
interface Fruit {
val name: String
}
data class Apple(override val name: String = "사과") : Fruit
data class Banana(override val name: String = "바나나") : Fruit
data class Orange(override val name: String = "오렌지") : Fruit

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.abstractfactory.common
interface Juice {
val name: String
}
data class AppleJuice(override val name: String = "사과주스") : Juice
data class BananaJuice(override val name: String = "바나나주스") : Juice
data class OrangeJuice(override val name: String = "오렌지주스") : Juice

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.after
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.HomeElectric
interface ElectricService {
fun generateElectric(current: Int): HomeElectric
}

View File

@@ -0,0 +1,15 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.after
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.HomeElectric
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.PowerStation
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.PowerStationElectricRequest
class HomePowerStationElectricAdapter(
private val powerStation: PowerStation
): ElectricService {
override fun generateElectric(current: Int): HomeElectric {
val powerStationElectricRequest = PowerStationElectricRequest(current)
val powerStationElectric = powerStation.generateElectric(powerStationElectricRequest)
return powerStationElectric.toHomeElectric()
}
}

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.after
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.Home
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.PowerStation
class PowerStationApplication
fun main() {
val powerStation = PowerStation()
val homePowerStationElectricAdapter = HomePowerStationElectricAdapter(powerStation)
val homeElectric = homePowerStationElectricAdapter.generateElectric(1000)
val home = Home()
home.takeElectric(homeElectric) // 가정은 발전소의 전기를 사용하는 클라이언트 역할을 한다.
}

View File

@@ -0,0 +1,19 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.before
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.Home
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.PowerStation
import com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common.PowerStationElectricRequest
class PowerStationApplication
fun main() {
val powerStation = PowerStation()
val electricRequest = PowerStationElectricRequest(1000)
val powerStationElectric = powerStation.generateElectric(electricRequest)
val homeElectric = powerStationElectric.toHomeElectric()
val home = Home()
home.takeElectric(homeElectric) // 가정은 발전소의 전기를 사용하는 클라이언트 역할을 한다.
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common
class Home(
private var electric: HomeElectric = HomeElectric(0)
) {
fun takeElectric(electric: HomeElectric) {
this.electric = electric
}
}

View File

@@ -0,0 +1,5 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common
data class HomeElectric(
val current: Int,
)

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common
class PowerStation {
fun generateElectric(request: PowerStationElectricRequest): PowerStationElectric {
return PowerStationElectric(request.current)
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common
data class PowerStationElectric(
val current: Int,
) {
fun toHomeElectric(): HomeElectric {
return HomeElectric(this.current * (11000 / 220))
}
}

View File

@@ -0,0 +1,5 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.adapter.common
data class PowerStationElectricRequest(
val current: Int
)

View File

@@ -0,0 +1,17 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.after
class FranchiseeApplication
fun main() {
val coffeeCorporation = FranchiseeCorporation("커피프린스")
coffeeCorporation.addStore(FranchiseeStore("1호점"))
coffeeCorporation.calculate()
println()
println("가맹점 정산을 완료했습니다.")
println()
val franchiseeCorporation = FranchiseeCorporation("모두의 프랜차이즈")
franchiseeCorporation.addStore(coffeeCorporation)
franchiseeCorporation.calculate()
}

View File

@@ -0,0 +1,21 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.after
class FranchiseeCorporation(
private val name: String,
private val stores: MutableList<Store> = mutableListOf()
) : Store {
fun addStore(store: Store) {
stores.add(store)
}
fun removeStore(store: Store) {
stores.remove(store)
}
override fun calculate() {
for (store in stores) {
store.calculate()
}
println("프랜차이즈 기업 [$name]의 정산을 완료했습니다.")
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.after
class FranchiseeStore(
val name: String
) : Store {
override fun calculate() {
println("프랜차이즈 가게 [$name]이(가) 정산을 진행합니다.")
}
}

View File

@@ -0,0 +1,5 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.after
interface Store {
fun calculate()
}

View File

@@ -0,0 +1,17 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.before
class FranchiseeApplication
fun main() {
val coffeeCorporation = FranchiseeCorporation("커피프린스")
coffeeCorporation.addStore(FranchiseeStore("1호점"))
coffeeCorporation.calculateAllStores()
println()
println("가맹점 정산을 완료했습니다.")
println()
val franchiseeCorporation = FranchiseeCorporation("모두의 프랜차이즈")
franchiseeCorporation.addCorporation(coffeeCorporation)
franchiseeCorporation.calculateAllCorporations()
}

View File

@@ -0,0 +1,37 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.before
class FranchiseeCorporation(
private val name: String,
private val corporations: MutableList<FranchiseeCorporation> = mutableListOf(),
private val stores: MutableList<FranchiseeStore> = mutableListOf()
) {
fun addCorporation(franchiseeCorporation: FranchiseeCorporation) {
corporations.add(franchiseeCorporation)
}
fun removeCorporation(franchiseeCorporation: FranchiseeCorporation) {
corporations.remove(franchiseeCorporation)
}
fun addStore(store: FranchiseeStore) {
stores.add(store)
}
fun removeStore(store: FranchiseeStore) {
stores.remove(store)
}
fun calculateAllCorporations() {
for (corporation in corporations) {
corporation.calculateAllStores()
}
println("프랜차이즈 기업 [$name]의 정산을 완료했습니다.")
}
fun calculateAllStores() {
for (store in stores) {
store.calculate()
}
println("프랜차이즈 기업 [$name]의 정산을 완료했습니다.")
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.composite.before
class FranchiseeStore(
val name: String
) {
fun calculate() {
println("프랜차이즈 가게 [$name]이(가) 정산을 진행합니다.")
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.after
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class Fire(private val sword: Sword) : SwordDecorator() {
override fun skill(): String {
return "불꽃 ${sword.skill()}"
}
}

View File

@@ -0,0 +1,23 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.after
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Hero
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class HeroApplication
fun main() {
val hero = Hero()
hero.attack()
val fireSword = Fire(Sword.DEFAULT)
hero.changeSword(fireSword)
hero.attack()
val iceSword = Ice(Sword.DEFAULT)
hero.changeSword(iceSword)
hero.attack()
val fireIceSword = Fire(Ice(Sword.DEFAULT))
hero.changeSword(fireIceSword)
hero.attack()
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.after
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class Ice(private val sword: Sword) : SwordDecorator() {
override fun skill(): String {
return "얼음 ${sword.skill()}"
}
}

View File

@@ -0,0 +1,5 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.after
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
abstract class SwordDecorator : Sword()

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.before
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class FireIceSword : Sword() {
override fun skill(): String {
return "화염 얼음 ${DEFAULT.skill()}"
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.before
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class FireSword : Sword() {
override fun skill(): String {
return "화염 ${DEFAULT.skill()}"
}
}

View File

@@ -0,0 +1,22 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.before
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Hero
class HeroApplication
fun main() {
val hero = Hero()
hero.attack()
val fireSword = FireSword()
hero.changeSword(fireSword)
hero.attack()
val iceSword = IceSword()
hero.changeSword(iceSword)
hero.attack()
val fireIceSword = FireIceSword()
hero.changeSword(fireIceSword)
hero.attack()
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.before
import com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common.Sword
class IceSword : Sword() {
override fun skill(): String {
return "얼음 ${DEFAULT.skill()}"
}
}

View File

@@ -0,0 +1,14 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common
class Hero(
private var sword: Sword = Sword.DEFAULT
) {
fun changeSword(sword: Sword) {
this.sword = sword
}
fun attack() {
val swordSkill = sword.skill()
println("용사가 [$swordSkill]을 발동합니다.")
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.decorator.common
abstract class Sword {
abstract fun skill(): String
companion object {
val DEFAULT = object : Sword() {
override fun skill(): String {
return "참격"
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.after
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.Information
interface InformationFinder {
fun findInformation(): Information
}

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.after
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.FriendFinder
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.MemberFinder
class MemberApplication
fun main() {
val memberFinder = MemberFinder()
val friendFinder = FriendFinder()
val memberInformationFinder = MemberInformationFinder(memberFinder, friendFinder)
val memberInformation = memberInformationFinder.findInformation()
println(memberInformation)
}

View File

@@ -0,0 +1,17 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.after
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.FriendFinder
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.Information
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.MemberFinder
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.MemberInformation
class MemberInformationFinder(
private val memberFinder: MemberFinder,
private val friendFinder: FriendFinder,
): InformationFinder {
override fun findInformation(): Information {
val member = memberFinder.findMember()
val friends = friendFinder.findFriends(member)
return MemberInformation(member, friends)
}
}

View File

@@ -0,0 +1,18 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.before
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.FriendFinder
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.MemberFinder
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.MemberInformation
class MemberApplication
fun main() {
val memberFinder = MemberFinder()
val friendFinder = FriendFinder()
val member = memberFinder.findMember()
val friends = friendFinder.findFriends(member)
val memberInformation = MemberInformation(member, friends)
println(memberInformation)
}

View File

@@ -0,0 +1,6 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
data class Friend(
val name: String,
val age: Int
)

View File

@@ -0,0 +1,10 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.Friend
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.Member
class FriendFinder {
fun findFriends(member: Member): List<Friend> {
return listOf(Friend("colt", 29))
}
}

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
data class FriendInformation(
val name: String,
val age: Int
) : Information {
constructor(friend: Friend) : this(
name = friend.name,
age = friend.age
)
}

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
interface Information

View File

@@ -0,0 +1,6 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
class Member(
val name: String,
val age: Int
)

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
import com.banjjoknim.soliddesignpatternsample.designpattern.facade.common.Member
class MemberFinder {
fun findMember(): Member {
return Member("banjjoknim", 29)
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.facade.common
data class MemberInformation(
val memberName: String,
val memberAge: Int,
val friendsInformation: List<FriendInformation>
): Information {
constructor(member: Member, friends: List<Friend>) : this(
memberName = member.name,
memberAge = member.age,
friendsInformation = friends.map { FriendInformation(it) }
)
}

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
class AirConditioner(override val name: String) : HomeAppliance {
override fun turnOn() {
println("${name}을 켭니다.")
}
override fun turnOff() {
println("${name}을 끕니다.")
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
interface HomeAppliance {
val name: String
fun turnOn()
fun turnOff()
}

View File

@@ -0,0 +1,12 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
class HomeApplianceApplication
fun main() {
val airConditioner = AirConditioner("에어컨")
val television = Television("텔레비전")
val messageSender = MessageSender()
val remoteControl = RemoteControl(listOf(airConditioner, television), messageSender)
remoteControl.turnOn(airConditioner)
}

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
class MessageSender {
fun sendMessage(homeAppliance: HomeAppliance) {
println("메시지 : ${homeAppliance.name}을 켰습니다.")
}
}

View File

@@ -0,0 +1,17 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
class RemoteControl(
private val homeAppliances: List<HomeAppliance>,
private val messageSender: MessageSender
) {
fun turnOn(homeAppliance: HomeAppliance) {
homeAppliances.filterNot { it == homeAppliance }
.forEach { it.turnOff() }
homeAppliance.turnOn()
messageSender.sendMessage(homeAppliance)
}
fun turnOff(homeAppliance: HomeAppliance) {
homeAppliance.turnOff()
}
}

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.after
class Television(override val name: String) : HomeAppliance {
override fun turnOn() {
println("${name}을 켭니다.")
}
override fun turnOff() {
println("${name}을 끕니다.")
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.before
class AirConditioner(override val name: String) : HomeAppliance {
override fun turnOnBut(homeAppliance: HomeAppliance, messageSender: MessageSender) {
homeAppliance.turnOff()
println("${name}을 켭니다.")
messageSender.sendMessage(this)
}
override fun turnOff() {
println("${name}을 끕니다.")
}
}

View File

@@ -0,0 +1,9 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.before
interface HomeAppliance {
val name: String
fun turnOnBut(homeAppliance: HomeAppliance, messageSender: MessageSender)
fun turnOff()
}

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.before
class HomeApplianceApplication
fun main() {
val airConditioner = AirConditioner("에어컨")
val television = Television("텔레비전")
val messageSender = MessageSender()
airConditioner.turnOnBut(television, messageSender)
}

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.before
class MessageSender {
fun sendMessage(homeAppliance: HomeAppliance) {
println("메시지 : ${homeAppliance.name}을 켰습니다.")
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.mediator.before
class Television(override val name: String) : HomeAppliance {
override fun turnOnBut(homeAppliance: HomeAppliance, messageSender: MessageSender) {
homeAppliance.turnOff()
println("${name}을 켭니다.")
messageSender.sendMessage(this)
}
override fun turnOff() {
println("${name}을 끕니다.")
}
}

View File

@@ -0,0 +1,23 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.nill.after
interface Employee {
companion object {
val NOT_FOUND = object : Employee {
override val name: String
get() = "존재하지 않음"
override fun memorizeWorkingDays(workingDays: List<Int>) {
// 아무 작업도 하지 않는다.
}
}
}
val name: String
var workingDays: List<Int>
get() = listOf()
set(workingDays) {
memorizeWorkingDays(workingDays)
}
fun memorizeWorkingDays(workingDays: List<Int>)
}

View File

@@ -0,0 +1,28 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.nill.after
class Restaurant(
private val employees: MutableList<Employee> = mutableListOf()
) {
fun addEmployee(employee: Employee) {
employees.add(employee)
}
fun removeEmployee(employee: Employee) {
employees.remove(employee)
}
fun showAllEmployeesWorkingDays() {
for (employee in employees) {
println("${employee.name}의 근무일은 ${employee.workingDays}일 입니다.")
}
}
fun assignWorkingDays(workingDays: List<Int>, employeeName: String) {
val employee = findEmployee(employeeName)
employee.memorizeWorkingDays(workingDays)
}
private fun findEmployee(employeeName: String): Employee {
return employees.find { it.name == employeeName } ?: Employee.NOT_FOUND
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.nill.after
class RestaurantApplication
fun main() {
val restaurant = Restaurant()
restaurant.addEmployee(WorkingEmployee("banjjoknim"))
restaurant.assignWorkingDays(listOf(1, 4, 7, 14, 20, 27), "banjjoknim")
restaurant.assignWorkingDays(listOf(2, 5, 10, 17, 23, 29), "colt")
restaurant.showAllEmployeesWorkingDays()
}

View File

@@ -0,0 +1,10 @@
package com.banjjoknim.soliddesignpatternsample.designpattern.nill.after
class WorkingEmployee(
override val name: String,
override var workingDays: List<Int> = listOf()
) : Employee {
override fun memorizeWorkingDays(workingDays: List<Int>) {
this.workingDays = workingDays
}
}

Some files were not shown because too many files have changed in this diff Show More