71 Commits

Author SHA1 Message Date
Colt
f738bbfba7 feat : Logger 설정 추가 2022-10-28 20:29:33 +09:00
Colt
78353d594a refactor : 게시글 클라이언트 컴포넌트 value 변경 및 주석 추가 2022-10-28 20:18:51 +09:00
Colt
2a8480d2f4 refactor : 게시글 클라이언트 컴포넌트 value 변경 2022-10-28 20:09:13 +09:00
Colt
2bf88262b7 feat : 게시글 조회 FeignClient 컴포넌트 추가 2022-10-28 20:06:20 +09:00
Colt
ad702891b0 README.md 요구사항 수정 및 참고자료 추가 2022-10-28 20:04:20 +09:00
Colt
41fa3c2f6c Member Domain class 삭제 2022-10-28 19:56:35 +09:00
Colt
d00a697f50 add content in README.md 2022-10-28 19:39:46 +09:00
Colt
e990448335 add Member Domain Class 2022-10-28 19:14:14 +09:00
Colt
5472392ccc add application.yaml 2022-10-28 19:10:48 +09:00
Colt
124473a129 add README.md 2022-10-28 19:03:27 +09:00
Colt
a91fb579cf initial commit 2022-10-28 18:56:48 +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
Colt
0f4de6fa3d [만들면서 배우는 클린 아키텍처] 예제 코드 추가 (#9)
* 만들면서 배우는 클린 아키텍처 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 에서 수행하도록 변경
2022-04-24 18:19:38 +09:00
Colt
c611ebd226 만들면서 배우는 클린 아키텍처 initial commit (#7)
학습용 예제 코드 작성 프로젝트 추가
2022-04-10 00:16:35 +09:00
Colt
0ee99149df Merge pull request #6 from banjjoknim/security-jwt
spring-security JWT 예제 코드 추가
2022-04-04 00:17:21 +09:00
banjjoknim
9053fd087d refactor : JwtSecurityProperties 상수용 object 추가 2022-04-04 00:15:12 +09:00
banjjoknim
409b3e4ae1 feat : 권한별로 접근 가능한 API 추가 2022-04-04 00:04:50 +09:00
banjjoknim
2aba6999bd feat : JWT를 이용한 인증에 사용될 JwtAuthorizationFilter 추가 2022-04-04 00:04:23 +09:00
banjjoknim
58b8cf6d10 feat : JWT 토큰 생성 및 응답 Header 에 추가하는 로직 추가 2022-03-31 03:31:26 +09:00
banjjoknim
7071770951 feat : JwtAuthenticationFilter 를 이용한 로그인 인증 로직 추가 2022-03-27 21:52:33 +09:00
banjjoknim
483df22623 feat : PasswordEncoder Bean 및 로그인 로직 추가 2022-03-27 19:07:31 +09:00
banjjoknim
b2d330db7d feat : JwtSecurtyConfiguration, JwtAuthenticationFilter, JwtUserRepository 추가 2022-03-27 18:51:07 +09:00
banjjoknim
a3fcd21c29 refactor : 이름 변경 2022-03-27 18:44:04 +09:00
banjjoknim
c00f20cbc3 chore : 주석 중 일부 패키지 명시 내용 변경 2022-03-27 15:51:03 +09:00
banjjoknim
a35fe9c2c9 refactor : 전체 패키지 구조 변경 2022-03-27 15:49:18 +09:00
banjjoknim
e99536f663 feat : HttpServletRequest 의 Header 중 Authorization 값을 검사하는 AuthorizationFilter 추가 2022-03-27 03:46:53 +09:00
banjjoknim
7bfd6ed19c feat : CustomFilter 및 관련 설정, 주석 추가 2022-03-27 03:09:27 +09:00
banjjoknim
9ff7046f38 refactor : CorsConfiguration 패키지 변경 및 불필요한 파일 삭제 2022-03-26 23:01:03 +09:00
banjjoknim
ed852ff461 chore : Security SessionCreationPolicy 설정 부분 주석 추가 2022-03-26 19:24:05 +09:00
banjjoknim
cc1bd398fb chore : @CrossOrigin 어노테이션 관련 주석 추가 2022-03-26 18:56:48 +09:00
banjjoknim
e80e676546 feat : CORS, JWT Security 설정 추가 2022-03-26 18:55:48 +09:00
banjjoknim
ba26a7d138 feat : JwtUser Entity 추가 2022-03-26 17:54:07 +09:00
banjjoknim
bb1945b669 feat : home API 추가 2022-03-25 03:10:02 +09:00
banjjoknim
16512c7d81 chore : database, jpa 설정 추가 2022-03-25 03:03:40 +09:00
banjjoknim
b9c7cb13cb build : jwt 라이브러리 추가 2022-03-25 02:42:57 +09:00
banjjoknim
18fe7b9fc2 refactor : 파일 이름 변경 및 @EnableGlobalMethodSecurity 에 대한 주석 일부 추가 2022-03-25 02:36:11 +09:00
Colt
512960493f Merge pull request #5 from banjjoknim/jackson
ContextualSerializer 를 사용하는 직렬화 예제 코드 추가
2022-03-13 22:33:38 +09:00
banjjoknim
d942818cb7 feat : ContextualSerializer 를 사용하는 직렬화 예제 코드 추가 2022-03-13 22:32:40 +09:00
Colt
1a7b52c5b0 Merge pull request #4 from banjjoknim/jackson
Jackson 커스텀 직렬화 예제 코드 추가
2022-03-13 22:09:24 +09:00
banjjoknim
3af79e6619 feat : BeanSerializerModifier 를 이용한 직렬화 설정 방법 예제 추가 2022-03-13 22:07:57 +09:00
banjjoknim
2e63b9b128 test : Kotlin + Spring Boot 환경에서 AnnotationIntrospector 를 새로 추가할 경우 주의할 점 테스트 추가 2022-03-13 21:55:43 +09:00
banjjoknim
18aa61f5d5 refactor : 직렬화시 어노테이션에 선언된 값을 사용할 수 있도록 변경 2022-03-13 21:53:23 +09:00
banjjoknim
9f9a5644fc feat : 어노테이션 기반의 직렬화 예제 코드 추가 2022-03-13 20:45:06 +09:00
banjjoknim
abcf7da69e refactor : 불필요한 어노테이션 삭제 2022-03-13 13:39:49 +09:00
banjjoknim
20ca7b3e17 refactor : 패키지 변경 2022-03-13 13:39:24 +09:00
banjjoknim
160781432f feat : 커스텀 직렬화에 사용할 Secret 어노테이션 추가 2022-03-13 13:31:07 +09:00
banjjoknim
27f91fd501 docs : 주석 내용 추가 2022-03-12 15:20:55 +09:00
banjjoknim
c72d0ff106 feat : 직렬화 제어를 위한 어노테이션 추가 2022-03-12 13:23:18 +09:00
banjjoknim
0d66541289 feat : Car 와 Owner 의 직렬화에 사용되는 Serializer 추가 2022-03-12 13:20:38 +09:00
banjjoknim
889ff261a8 feat : Owner 클래스 추가 2022-03-12 13:05:21 +09:00
banjjoknim
e62b1dd7aa refactor : 패키지 변경 2022-03-12 13:01:39 +09:00
banjjoknim
5869f3b20a feat : Car Serializer 추가 2022-03-12 12:48:37 +09:00
banjjoknim
1e0696deba feat : jaskson 프로젝트 추가 2022-03-11 21:26:01 +09:00
Colt
5ed4f8153a Merge pull request #3 from banjjoknim/spring-security-11
네이버 로그인 기능 샘플 코드 추가
2022-03-08 13:00:23 +09:00
banjjoknim
5e55eeb462 style(user) : 들여쓰기 변경 2022-03-08 12:58:16 +09:00
banjjoknim
5891775b15 feat : Naver 로그인 타입 추가 2022-03-08 12:53:05 +09:00
banjjoknim
a62fbb7f71 chore : merge main(from spring-security-10) 2022-03-08 02:38:49 +09:00
banjjoknim
35a89db2d5 feat : naver OAuth2 설정 추가 2022-03-08 02:37:10 +09:00
Colt
3f64ef2977 Merge pull request #2 from banjjoknim/spring-security-10
Spring Security 예제 코드 설정 파일 일부 변경
2022-03-06 03:23:44 +09:00
banjjoknim
4fddf60d87 chore : 설정파일 일부 변경 2022-03-06 03:22:14 +09:00
Colt
b07abb8703 Merge pull request #1 from banjjoknim/spring-security-10
스프링부트 시큐리티 10강 - 페이스북 로그인 완료
2022-03-06 03:13:01 +09:00
banjjoknim
1f1275948a refactor : OAuth2 로그인시 OAuth2Type 별로 회원 정보 얻는 기능 추상화 2022-03-06 02:51:28 +09:00
banjjoknim
639d51eb28 feat(auth) : OAuth2 인증 관련 컴포넌트 추가 2022-03-06 02:18:27 +09:00
banjjoknim
897f023ef5 fix : provider에 할당되는 값 수정 2022-03-06 01:58:18 +09:00
banjjoknim
769587e24c refactor : 스프링 시큐리티 예제 코드 security 브랜치로부터 이관 2022-03-04 10:20:31 +09:00
banjjoknim
842814eeb3 refactor : 스프링 이벤트 처리 예제 코드 spring-event 브랜치로부터 이관 2022-03-02 21:55:41 +09:00
181 changed files with 7378 additions and 0 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

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

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

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

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

@@ -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,44 @@
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")
}
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.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

View 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

View File

@@ -0,0 +1 @@
rootProject.name = "jackson"

View File

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

View File

@@ -0,0 +1,51 @@
package com.banjjoknim.playground.jackson.common
import com.banjjoknim.playground.jackson.jsonserialize.ContextualCarSerializer
import com.banjjoknim.playground.jackson.jsonserialize.UsingJsonSerializeAnnotationCarSerializer
import com.fasterxml.jackson.databind.annotation.JsonSerialize
data class Car(
val name: String,
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingNoAnnotation(
val name: String = "banjjoknim",
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingJsonSerializeAnnotationCarSerializer(
val name: String = "banjjoknim",
@JsonSerialize(using = UsingJsonSerializeAnnotationCarSerializer::class)
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingContextualSerializerWithSecretAnnotation(
val name: String = "banjjoknim",
@JsonSerialize(using = ContextualCarSerializer::class)
@field:Secret("hello world!!")
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingContextualSerializerWithNoSecretAnnotation(
val name: String = "banjjoknim",
@JsonSerialize(using = ContextualCarSerializer::class)
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingSecretAnnotation(
val name: String = "banjjoknim",
@field:Secret("****")
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.playground.jackson.common
data class Owner(val name: String = "ban", val age: Int = 30)

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.playground.jackson.common
import com.fasterxml.jackson.annotation.JacksonAnnotation
/**
* [jackson-annotations](https://www.baeldung.com/jackson-annotations) 참고.
*
* @see com.fasterxml.jackson.annotation.JacksonAnnotation
* @see com.fasterxml.jackson.annotation.JacksonAnnotationsInside
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD) // 현재 상황에서는 PROPERTY 로 적용할 경우 제대로 적용되지 않는다. 아마 어노테이션 자체가 자바 기반으로 사용되어 PROPERTY 를 인식하지 못하는 것 같다(자바에서는 PROPERTY 타입을 사용할 수 없음).
@JacksonAnnotation // NOTE: important; MUST be considered a 'Jackson' annotation to be seen(or recognized otherwise via AnnotationIntrospect.isHandled())
annotation class Secret(
val substituteValue: String = ""
)

View File

@@ -0,0 +1,179 @@
package com.banjjoknim.playground.jackson.jsonserialize
import com.banjjoknim.playground.jackson.common.Car
import com.banjjoknim.playground.jackson.common.Secret
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
import com.fasterxml.jackson.databind.ser.ContextualSerializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
/**
* [jackson-object-mapper-tutorial](https://www.baeldung.com/jackson-object-mapper-tutorial) 참고.
*
* [how-to-(de)serialize-field-from-object-based-on-annotation-using-jackson](https://stackoverflow.com/questions/18659835/how-to-deserialize-field-from-object-based-on-annotation-using-jackson)
*
* Custom Serializer 를 만들기 위해서는 아래와 같이 StdSerializer<T> 를 상속해야 한다.
*
* 만약 어노테이션을 이용한 설정 또는 프로퍼티마다 다르게 작동하는 Serializer 를 만들고 싶다면 JsonSerializer 의 add-on interface 인 ContextualSerializer 를 구현하면 된다.
*
* JsonSerializer<T> 만 확장할 경우엔 애노테이션 정보를 얻을 수 없다. 추가적으로 ContextualSerializer 인터페이스를 구현해주면 createContextual() 메서드를 구현해줘야 하는데 두번째 인자로 넘어오는 BeanProperty 를 이용해 애노테이션 정보를 구할 수 있다.
*
* ContexutalSerializer 를 사용하는 방법은 [TestContextualSerialization](https://github.com/FasterXML/jackson-databind/blob/master/src/test/java/com/fasterxml/jackson/databind/contextual/TestContextualSerialization.java) 참고하도록 한다.
*
* Custom Serializer 가 JsonSerializer<T> 와 ContextualSerialier 를 모두 구현할 경우 createContextual() 함수가 먼저 호출된다.
*
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer
* @see com.fasterxml.jackson.databind.ser.std.StringSerializer
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer
*/
class CarSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeNumberField("price", value.price)
gen.writeEndObject()
}
}
class CarNameSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeEndObject()
}
}
class CarPriceSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeNumberField("price", value.price)
gen.writeEndObject()
}
}
class CarNameOwnerSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeObjectField("owner", value.owner)
gen.writeEndObject()
}
}
class CarNameOwnerNameSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeObjectFieldStart("owner")
gen.writeStringField("name", value.owner.name)
gen.writeEndObject()
}
}
class UsingJsonSerializeAnnotationCarSerializer : StdSerializer<String>(String::class.java) {
override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString("****")
}
}
class ContextualCarSerializer(
private val substituteValue: String = "this is default value in ContextualCarSerializer"
) : StdSerializer<String>(String::class.java), ContextualSerializer {
override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString(substituteValue)
}
override fun createContextual(provider: SerializerProvider, property: BeanProperty): JsonSerializer<*> {
val annotation = property.getAnnotation(Secret::class.java)
if (annotation != null) {
return ContextualCarSerializer(annotation.substituteValue)
}
return ContextualCarSerializer()
// JsonMappingException: Can not write a field name, expecting a value (through reference chain: com.banjjoknim.playground.jackson.common.CarUsingContextualSerializerWithNoSecretAnnotation["secret"])
// return provider.findKeySerializer(property.type, property)
}
}
/**
* AnnotationIntrospector 를 상속한 JacksonAnnotationIntrospector 은 Jackson 라이브러리가 직렬화/역직렬화시 `JacksonAnnotation` 정보를 어떻게 처리할지에 대한 정보가 정의되어 있는 클래스다.
*
* 따라서 어노테이션별로 어떻게 처리할지 재정의하고 싶다면 이 녀석을 override 해준뒤 ObjectMapper 에 등록해주면 된다.
*
* 등록할 때는 `ObjectMapper#setAnnotationIntrospector()` 를 사용한다.
*
* [FasterXML - AnnotationIntrospector](https://github.com/FasterXML/jackson-docs/wiki/AnnotationIntrospector)
*
* @see com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector#
* @see com.fasterxml.jackson.databind.ObjectMapper
*
*/
class SecretAnnotationIntrospector : JacksonAnnotationIntrospector() {
/**
*
* `@JsonIgnore` 를 적용했을 때 무시할지 여부를 판단하는 함수이다.
*
* 따라서 직렬화 / 역직렬화시 무시하고 싶은 프로퍼티가 있다면 이 함수를 override 하면 된다.
*/
override fun hasIgnoreMarker(m: AnnotatedMember): Boolean {
return super.hasIgnoreMarker(m)
}
/**
* `@JsonSerailize` 가 붙은 어노테이션의 처리를 재정의할 때 override 하는 함수이다.
*
* 자세한 내용은 JacksonAnnotationIntrospector#findSerializer() 의 구현을 살펴보도록 한다.
*
* 특정 프로퍼티에 대해 어떤 Serializer 를 사용할 것인지 결정하는 함수이다.
*
* 따라서 특정 조건에 따라 직렬화 처리에 사용할 Serializer 를 정의하고 싶다면 이 함수를 override 하면 된다.
*/
override fun findSerializer(a: Annotated): Any? {
val annotation = a.getAnnotation(Secret::class.java)
if (annotation != null) {
return SecretAnnotationSerializer(annotation.substituteValue)
}
return super.findSerializer(a) // 기존 JacksonAnnotationIntrospector 의 것을 사용한다.
}
}
class SecretAnnotationSerializer(private val substituteValue: String) : StdSerializer<String>(String::class.java) {
override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString(substituteValue)
}
}
/**
* 직렬화될 프로퍼티를 수정하도록 하는 방법이다.
*
* BeanSerializerModifier#changeProperties() 를 재정의한 뒤,
*
* SimpleModule 을 이용해서 ObjectMapper 에 등록한다.
*
* 아래처럼 하면 직렬화 대상에서 완전히 제외된다(`@JsonIgnore`와 동일한 효과).
*
* @see com.fasterxml.jackson.databind.ser.BeanSerializerModifier
* @see com.fasterxml.jackson.databind.module.SimpleModule
*/
class SecretBeanSerializerModifier : BeanSerializerModifier() {
override fun changeProperties(
config: SerializationConfig,
beanDesc: BeanDescription,
beanProperties: MutableList<BeanPropertyWriter>
): MutableList<BeanPropertyWriter> {
SimpleModule()
return beanProperties
.filter { property -> property.getAnnotation(Secret::class.java) == null }
.toMutableList()
// return super.changeProperties(config, beanDesc, beanProperties)
}
}

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.playground
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class JacksonApplicationTests {
@Test
fun contextLoads() {
}
}

View File

@@ -0,0 +1,279 @@
package com.banjjoknim.playground.jackson.jsonserialize
import com.banjjoknim.playground.jackson.common.Car
import com.banjjoknim.playground.jackson.common.CarUsingContextualSerializerWithNoSecretAnnotation
import com.banjjoknim.playground.jackson.common.CarUsingContextualSerializerWithSecretAnnotation
import com.banjjoknim.playground.jackson.common.CarUsingJsonSerializeAnnotationCarSerializer
import com.banjjoknim.playground.jackson.common.CarUsingNoAnnotation
import com.banjjoknim.playground.jackson.common.CarUsingSecretAnnotation
import com.banjjoknim.playground.jackson.common.Owner
import com.fasterxml.jackson.databind.AnnotationIntrospector
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
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
/**
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.module.SimpleModule
* @see com.fasterxml.jackson.databind.ser.DefaultSerializerProvider
* @see com.fasterxml.jackson.databind.ser.BeanSerializer -> 객체를 직렬화할때 사용되는 Serializer
* @see com.fasterxml.jackson.core.json.WriterBasedJsonGenerator
* @see com.fasterxml.jackson.databind.ser.std.BeanSerializerBase
* @see com.fasterxml.jackson.databind.ser.BeanPropertyWriter
*/
class CarSerializersTest {
private lateinit var mapper: ObjectMapper
companion object {
private val owner = Owner("ban", 30)
private val car = Car("banjjoknim", 10_000_000, owner)
private val carUsingNoAnnotation = CarUsingNoAnnotation()
private val carUsingJsonSerializeAnnotationCarSerializer = CarUsingJsonSerializeAnnotationCarSerializer()
private val carUsingContextualSerializerWithSecretAnnotation =
CarUsingContextualSerializerWithSecretAnnotation()
private val carUsingContextualSerializerWithNoSecretAnnotation =
CarUsingContextualSerializerWithNoSecretAnnotation()
private val carUsingSecretAnnotation = CarUsingSecretAnnotation()
}
@BeforeEach
fun setup() {
mapper = ObjectMapper().registerKotlinModule()
}
@Test
fun `기본 ObjectMapper의 동작을 테스트한다`() {
// given
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@DisplayName("등록된 커스텀 직렬화기의 동작을 테스트한다")
@Nested
inner class CarSerializerTestCases {
@Test
fun `자동차의 모든 필드만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","price":10000000}""")
}
@Test
fun `자동차의 이름만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim"}""")
}
@Test
fun `자동차의 가격만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarPriceSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"price":10000000}""")
}
@Test
fun `자동차의 이름과 오너의 모든 필드를 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameOwnerSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","owner":{"name":"ban","age":30}}""")
}
@Test
fun `자동차의 이름과 오너의 이름만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameOwnerNameSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","owner":{"name":"ban"}}""")
}
@Test
fun `아무 어노테이션도 적용하지 않고 직렬화한다`() {
// given
// when
val actual = mapper.writeValueAsString(carUsingNoAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"secret","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@JsonSerialize 어노테이션을 적용하여 직렬화한다`() {
// given
// when
val actual = mapper.writeValueAsString(carUsingJsonSerializeAnnotationCarSerializer)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"****","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `ContextualSerializer 를 상속 받은 구현체를 이용해서 직렬화한다`() {
// given
// when
val actualWithSecretAnnotation = mapper.writeValueAsString(carUsingContextualSerializerWithSecretAnnotation)
val actualWithNoSecretAnnotation =
mapper.writeValueAsString(carUsingContextualSerializerWithNoSecretAnnotation)
// then
assertThat(actualWithSecretAnnotation).isEqualTo("""{"name":"banjjoknim","secret":"hello world!!","price":10000000,"owner":{"name":"ban","age":30}}""")
assertThat(actualWithNoSecretAnnotation).isEqualTo("""{"name":"banjjoknim","secret":"this is default value in ContextualCarSerializer","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@Secret 어노테이션, AnnotationIntrospector 을 적용하여 직렬화한다`() {
// given
mapper.setAnnotationIntrospector(SecretAnnotationIntrospector())
// when
val actual = mapper.writeValueAsString(carUsingSecretAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"****","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@Secret 어노테이션, BeanSerializerModifier 를 적용하여 직렬화한다`() {
// given
val module = object : SimpleModule() {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.addBeanSerializerModifier(SecretBeanSerializerModifier())
}
}
mapper.registerModule(module)
// when
val actual = mapper.writeValueAsString(carUsingSecretAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","price":10000000,"owner":{"name":"ban","age":30}}""")
}
/**
*
* Kotlin + Spring Boot 를 사용한다면 `com.fasterxml.jackson.module:jackson-module-kotlin` 의존성을 사용할 것이다.
*
* 이를 사용하면 기본 생성자 없이도 `@RequestBody` 에서 json 을 객체로 역직렬화 할 수 있다.
*
* `com.fasterxml.jackson.module:jackson-module-kotlin` 에서 이러한 역할을 해주는 것이 KotlinAnnotationIntrospector 이다.
*
* 하지만 새로운 AnnotationIntrospector 를 등록하면 KotlinAnnotationIntrospector 가 무시되어 기본생성자 없이는 `@RequestBody` 객체를 만들지 못하게 된다.
*
* 따라서 아래와 같이 기존의 AnnotationIntrospector 도 등록해주어야 한다.
*
* 이는 AnnotationIntrospector.Pair 도우미 클래스를 사용해서 할 수 있다.
*
* 이때, 순서대로 기본 Introspector, 보조 Introspector 로 등록된다.
*
* [AnnotationIntrospector](https://github.com/FasterXML/jackson-docs/wiki/AnnotationIntrospector)
*
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair
* @see com.fasterxml.jackson.databind.AnnotationIntrospector
* @see com.fasterxml.jackson.module.kotlin.KotlinAnnotationIntrospector
*/
@Test
fun `Kotlin + Spring Boot를 사용하면 기본적으로 3가지의 AnnotationIntrospector 가 ObjectMapper 에 존재한다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(originalAnnotationIntrospector.allIntrospectors()).hasSize(3)
assertThat(allIntrospectorNames[0]).isEqualTo("KotlinAnnotationIntrospector")
assertThat(allIntrospectorNames[1]).isEqualTo("JacksonAnnotationIntrospector")
assertThat(allIntrospectorNames[2]).isEqualTo("KotlinNamesAnnotationIntrospector")
}
@Test
fun `Kotlin + Spring Boot 를 사용할 시 ObjectMapper 에 새로운 AnnotationIntrospector 를 추가하면 KotlinAnnotationIntrospector 가 무시된다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
mapper.setAnnotationIntrospector(SecretAnnotationIntrospector())
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(originalAnnotationIntrospector.allIntrospectors()).hasSize(3)
assertThat(allIntrospectorNames).hasSize(1)
assertThat(allIntrospectorNames[0]).isEqualTo("SecretAnnotationIntrospector")
}
@Test
fun `Kotlin + Spring Boot 를 사용할 시 ObjectMapper 에 새로운 AnnotationIntrospector 를 추가할 때 Pair 로 추가하면 KotlinAnnotationIntrospector 가 무시되지 않는다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
mapper.setAnnotationIntrospector(
AnnotationIntrospector.pair(
SecretAnnotationIntrospector(),
originalAnnotationIntrospector
) // 내부 구현은 아래와 같다.
// AnnotationIntrospectorPair(SecretAnnotationIntrospector(), originalAnnotationIntrospector)
)
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(allIntrospectorNames).hasSize(4)
assertThat(allIntrospectorNames[0]).isEqualTo("SecretAnnotationIntrospector")
assertThat(allIntrospectorNames[1]).isEqualTo("KotlinAnnotationIntrospector")
assertThat(allIntrospectorNames[2]).isEqualTo("JacksonAnnotationIntrospector")
assertThat(allIntrospectorNames[3]).isEqualTo("KotlinNamesAnnotationIntrospector")
}
}
}

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,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/)
![Hexagonal Architecture](images/Hexagonal Architecture.png)
![Hexagonal Architecture Process](images/Hexagonal%20Architecture%20Process.png)
- `포트(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/)

View File

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

View 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

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

View 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

View File

@@ -0,0 +1 @@
rootProject.name = "learn-with-making-clean-architecture"

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.cleanarchitecture.user.adapter.`in`.web
data class ChangeNicknameResponse(val userId: Long)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package com.banjjoknim.cleanarchitecture.user.adapter.out.persistence
import org.springframework.data.jpa.repository.JpaRepository
interface UserEntityRepository: JpaRepository<UserEntity, Long> {
}

View File

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

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
data class ChangeNicknameRequestData(val userId: Long, val newNickname: String)

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
data class ChangeNicknameResponseData(val userId: Long)

View File

@@ -0,0 +1,5 @@
package com.banjjoknim.cleanarchitecture.user.application.port.`in`
interface ChangeNicknameUseCase {
fun changeNickname(data: ChangeNicknameRequestData): ChangeNicknameResponseData
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.cleanarchitecture.user.pojo
data class Nickname(val value: String)

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
<html>
<header>
<meta lang="ko" charset="UTF-8">
<title>만들면서 배우는 클린 아키텍처</title>
</header>
<body>
<h1>만들면서 배우는 클린 아키텍처</h1>
</body>
</html>

View File

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

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

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

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,25 @@
# Spring Cloud Open Feign
## 프로젝트 구성
- [Spring initializr](https://start.spring.io/) 에서 프로젝트 생성.
- 아래 사진은 프로젝트 구성 Dependencies.
![img.png](img.png)
## 요구사항
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/) 에서 게시글 목록을 조회한다.
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/) 에서 게시글을 조회한다.
## 참고자료
- [Introduction to Spring Cloud OpenFeign](https://www.baeldung.com/spring-cloud-openfeign)
- [Spring Cloud OpenFeign](https://spring.io/projects/spring-cloud-openfeign)
- [Spring Cloud OpenFeign](https://cloud.spring.io/spring-cloud-openfeign/reference/html/)
- [우아한 Feign 적용기](https://techblog.woowahan.com/2630/)
- [Feign 좀 더 나아가기](https://techblog.woowahan.com/2657/)
- [GitHub - OpenFeign/feign](https://github.com/OpenFeign/feign)
- [Java Jackson JSON Library](https://kwonnam.pe.kr/wiki/java/jackson)
- [{JSON} Placeholder](https://jsonplaceholder.typicode.com/)
- [Spring Cloud Openfeign](https://brunch.co.kr/@springboot/202)

View File

@@ -0,0 +1,47 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.7.5"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
kotlin("plugin.jpa") version "1.6.21"
}
group = "com.banjjoknim"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
extra["springCloudVersion"] = "2021.0.4"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -0,0 +1 @@
rootProject.name = "spring-cloud-open-feign"

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.springcloudopenfeign
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.openfeign.EnableFeignClients
@EnableFeignClients
@SpringBootApplication
class SpringCloudOpenFeignApplication
fun main(args: Array<String>) {
runApplication<SpringCloudOpenFeignApplication>(*args)
}

View File

@@ -0,0 +1,56 @@
package com.banjjoknim.springcloudopenfeign.configuration
import com.fasterxml.jackson.databind.DeserializationFeature
import feign.Logger
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
@Configuration
class FeignConfiguration : Jackson2ObjectMapperBuilderCustomizer {
override fun customize(jacksonObjectMapperBuilder: Jackson2ObjectMapperBuilder) {
jacksonObjectMapperBuilder
.featuresToEnable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}
}
/**
* For each Feign client, a logger is created by default.
*
* To enable logging, we should declare it in the application.properties file using the package name of the client interfaces:
*
* > logging.level.com.baeldung.cloud.openfeign.client: DEBUG
*
* Or, if we want to enable logging only for one particular client in a package, we can use the full class name:
*
* > logging.level.com.baeldung.cloud.openfeign.client.JSONPlaceHolderClient: DEBUG
*
* **Note that Feign logging responds only to the DEBUG level.**
*
* The ***Logger.Level*** that we may configure per client indicates how much to log:
* ```java
* public class ClientConfiguration {
*
* @Bean
* Logger.Level feignLoggerLevel() {
* return Logger.Level.BASIC;
* }
* }
* ```
*
* There are four logging levels to choose from:
*
* - NONE no logging, which is the default
* - BASIC log only the request method, URL and response status
* - HEADERS log the basic information together with request and response headers
* - FULL log the body, headers and metadata for both request and response
*/
@Configuration
class LoggerConfiguration {
@Bean
fun feignLoggerLevel(): Logger.Level {
return Logger.Level.FULL
}
}

View File

@@ -0,0 +1,8 @@
package com.banjjoknim.springcloudopenfeign.domain
data class Post(
val userId: Long,
val id: Long,
val title: String,
val completed: Boolean
)

View File

@@ -0,0 +1,28 @@
package com.banjjoknim.springcloudopenfeign.domain
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
/**
* Feign is a declarative web service client. It makes writing web service clients easier.
* To use Feign create an interface and annotate it.
* It has pluggable annotation support including Feign annotations and JAX-RS annotations.
* Feign also supports pluggable encoders and decoders.
* Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web.
* Spring Cloud integrates Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
*
* 쉽게 말하면 RestTemplate, RestClient 등의 `HTTP Client`를 어노테이션으로 선언하여 사용하는 것이라 생각하면 된다.
* 인터페이스를 만들고 @FeignClient 를 선언하기만 하면 구현체는 런타임에 알아서 만들어진다.
*
* @see org.springframework.cloud.openfeign.FeignClient
*/
@FeignClient(value = "post", url = "https://jsonplaceholder.typicode.com/")
interface PostClient {
@GetMapping("/posts")
fun findPosts(): List<Post>
@GetMapping("/posts/{postId}")
fun findPost(@PathVariable postId: Long): Post
}

View File

@@ -0,0 +1,21 @@
package com.banjjoknim.springcloudopenfeign.domain
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class PostController(
private val postClient: PostClient
) {
@GetMapping("/posts")
fun posts(): List<Post> {
return postClient.findPosts()
}
@GetMapping("/posts/{postId}")
fun post(@PathVariable postId: Long): Post {
return postClient.findPost(postId)
}
}

View File

@@ -0,0 +1,21 @@
spring:
datasource:
url: jdbc:h2:mem:testdb;
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: create-drop
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
show-sql: true
logging:
level:
com:
banjjoknim:
springcloudopenfeign: DEBUG

View File

@@ -0,0 +1,13 @@
package com.banjjoknim.springcloudopenfeign
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class SpringCloudOpenFeignApplicationTests {
@Test
fun contextLoads() {
}
}

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,39 @@
### 🚀 step1
- 현재 코드를 분석 하면서 어떤 점을 고치고 싶은지 니즈를 파악해본다.
### 🚀 step2
- 상속 기반의 이벤트로 변경 해본다.
- 이벤트 객체를 만들기 위해 ApplicationEvent 를 상속 받아 데이터 전달을 위해 객체를 만들어본다.
- 이벤트 객체로 부터 전달 받을 EventListener 구현체를 만들어본다.
- ApplicationEventPublisher 를 주입 받아서 이벤트를 발행 시킨다.
- 테스트 코드 작성
> Mockito 활용
### 📖 살펴볼 객체
- `AbstractApplicationContext` 를 intellij 의 diagram 을 통해서 구조를 살펴보면 `ApplicationEventPublisher` 를 상속 받고 있습니다.
- `AbstractApplicationContext`::publishEvent 메소드를 보면 이벤트의 발행이 어떻게 되는지 알 수 있습니다.
> 조금 설명을 드리면 위의 메소드 안에서 ApplicationEventMulticaster 를 주입 받아 이벤트를 실행 시키는데 구현체 중 `SimpleApplicationEventMulticater` 를 살펴보면 (위와 같이 다이어그램으로 표현) multicastEvent 메소드를 통해서 빈으로 등록된 이벤트 객체를 실행을 합니다
### 🚀 step3
- 추가 기능(어드민에서 쿠폰 및 환영 메시지를 보낼 수 있는 기능이 있다.)
- `@EventListener` 어노테이션을 활용해 코드를 변경 해보자.
- > 스프링 4.2 부터 ApplicationEvent 를 상속 받아 객체를 생성하지 않아도 이벤트 객체 처럼 활용할 수 있다. `ApplicationListenerMethodAdapter` 객체에서 어노테이션을 찾아서 실행
- 어드민에 알람을 보내는 중에 예외처리를 해보자
- > `@TransactionalEventListener` 를 활용해 트랜잭션 단위를 제어해보자.
- 비동기 활용
- > `@EnableAsync``@Async` 를 활용해 비동기 처리를 해보자.
### 🚀 step4
- Domain Event
- > AbstractAggregateRoot 를 상속 받아 이벤트를 제공
- > registerEvent 를 통해 jpa의 save 를 명시적으로 할 때 마다 이벤트를 발행
---
#### 출처 : https://github.com/tongnamuu/SpringEvent

View File

@@ -0,0 +1,24 @@
# 프로젝트 세팅 #
Java 11을 사용합니다.
처음부터 세팅하고 싶다면
![img.png](images/img.png)
이후 annotation processing 옵션을 켜주세요
![img_1.png](images/img_1.png)
이후 프로젝트 repository의 커밋을 따라오시면 그대로 실습하실 수 있습니다.
# 우리가 실습해볼 것 #
- 회원 가입 api를 이벤트를 활용해 개선해 봅니다.
- 회원가입 플로우는 아래와 같습니다.
- 회원가입을 하면 유저를 저장합니다
- 알람이 나갑니다
- 쿠폰을 발급해 줍니다
- 이메일과 SMS로 환영 메시지를 보냅니다
---
#### 출처 : https://github.com/tongnamuu/SpringEvent

View File

@@ -0,0 +1,46 @@
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-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")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.named("jar") {
enabled = false
}

View 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

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

View 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: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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