├── .github ├── ISSUE_TEMPLATE │ └── question.md └── issuse-question.png ├── .gitignore ├── README.md ├── ch2 ├── README.md ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ └── main │ ├── java │ └── com │ │ └── code │ │ └── design │ │ ├── CodeDesignApplication.java │ │ ├── domain │ │ ├── member │ │ │ ├── api │ │ │ │ └── MemberApi.java │ │ │ └── domain │ │ │ │ └── Member.java │ │ ├── model │ │ │ ├── Address.java │ │ │ └── Email.java │ │ └── order │ │ │ ├── item │ │ │ ├── application │ │ │ │ └── OrderItemCalculation.java │ │ │ └── domain │ │ │ │ └── OrderItem.java │ │ │ └── order │ │ │ ├── api │ │ │ └── OrderApi.java │ │ │ ├── application │ │ │ ├── OrderCalculation.java │ │ │ └── OrderRegistrationService.java │ │ │ ├── dao │ │ │ ├── OrderCustomRepository.java │ │ │ ├── OrderCustomRepositoryImpl.java │ │ │ ├── OrderFindService.java │ │ │ └── OrderRepository.java │ │ │ ├── domain │ │ │ ├── Order.java │ │ │ ├── Orderer.java │ │ │ └── Tracking.java │ │ │ ├── dto │ │ │ └── OrderRegistrationRequest.java │ │ │ └── exception │ │ │ └── OrderNotFoundException.java │ │ ├── global │ │ ├── common │ │ │ ├── ErrorResponse.java │ │ │ └── PageResponse.java │ │ ├── config │ │ │ └── JpaConfiguration.java │ │ └── error │ │ │ └── GlobalExceptionHandler.java │ │ └── infra │ │ ├── AmazonSmsClient.java │ │ ├── AmazonSmsSenderService.java │ │ ├── KtSmsSenderClient.java │ │ ├── KtSmsSenderService.java │ │ ├── SmsMessageRequest.java │ │ └── SmsSender.java │ └── resources │ ├── application.yml │ ├── test.csv │ └── test.sql ├── ch3 ├── README.md ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── settings.gradle.kts └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── code │ │ │ └── design │ │ │ ├── CodeDesignApplication.java │ │ │ ├── lombok │ │ │ ├── Coupon.java │ │ │ ├── Member.java │ │ │ └── Student.java │ │ │ └── object │ │ │ ├── Account.java │ │ │ ├── Address.java │ │ │ ├── CreditCard.java │ │ │ ├── Order.java │ │ │ └── Refund.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── code │ └── design │ ├── lombok │ ├── MemberTest.java │ └── StudentTest.java │ └── object │ ├── AccountTest.java │ ├── AddressTest.java │ ├── CreditCardTest.java │ └── RefundTest.java ├── ch4 ├── README.md ├── build.gradle.kts ├── client │ └── api.http ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ └── main │ ├── java │ └── com │ │ └── code │ │ └── design │ │ ├── Ch4Application.java │ │ ├── Exception1.java │ │ ├── Exception2.java │ │ ├── Member.java │ │ ├── MemberApi.java │ │ ├── MemberRepository.java │ │ └── MemberService.java │ └── resources │ └── application.yml ├── ch5 ├── README.md ├── build.gradle.kts ├── client │ ├── http-client.env.json │ └── order-api-test.http ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── code │ │ └── design │ │ ├── Ch5Application.java │ │ ├── ErrorCode.java │ │ ├── ErrorResponse.java │ │ ├── GlobalExceptionHandler.java │ │ ├── exception │ │ ├── BusinessException.java │ │ └── EntityNotFoundException.java │ │ ├── member │ │ ├── Member.java │ │ ├── MemberApi.java │ │ ├── MemberRepository.java │ │ └── SignUpRequest.java │ │ ├── order │ │ ├── OrderApi.java │ │ ├── OrderSheetForm.java │ │ ├── OrderSheetFormValidator.java │ │ ├── OrderSheetRequest.java │ │ └── PaymentMethod.java │ │ └── validation │ │ ├── EmailDuplicationValidator.java │ │ └── EmailUnique.java │ └── test │ └── java │ └── com │ └── code │ └── design │ ├── member │ └── MemberApiTest.java │ └── order │ └── OrderApiTest.java ├── ch6 ├── README.md ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── code │ │ └── design │ │ ├── Ch6Application.java │ │ ├── part1 │ │ ├── ByAuthChangePasswordService.java │ │ ├── ByPasswordChangePasswordService.java │ │ ├── CardPaymentService.java │ │ ├── ChangePasswordService.java │ │ ├── Member.java │ │ ├── MemberFindService.java │ │ ├── MemberRepository.java │ │ ├── MemberService.java │ │ ├── MemberServiceImpl.java │ │ ├── PasswordChangeRequest.java │ │ └── ShinhanCardPaymentService.java │ │ ├── part2 │ │ ├── MessageType.java │ │ ├── Order.java │ │ ├── OrderApi.java │ │ ├── OrderLegacy.java │ │ └── OrderMessage.java │ │ └── part3 │ │ ├── Coupon.java │ │ ├── CouponLegacy.java │ │ ├── FirstOrderCoupon.java │ │ └── FirstOrderCouponLegacy.java │ └── test │ └── java │ └── com │ └── code │ └── design │ ├── part2 │ ├── OrderLegacyTest.java │ ├── OrderMessageTest.java │ └── OrderTest.java │ └── part3 │ └── CouponTest.java ├── ch7 ├── README.md ├── build.gradle.kts ├── client │ ├── MemberApi.http │ └── OrderApi.http ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ └── main │ ├── java │ └── com │ │ └── code │ │ └── design │ │ ├── AppRunner.java │ │ ├── Ch7Application.java │ │ ├── EmailSenderService.java │ │ ├── cart │ │ ├── Cart.java │ │ ├── CartApi.java │ │ ├── CartRepository.java │ │ └── CartService.java │ │ ├── coupon │ │ ├── Coupon.java │ │ ├── CouponIssueService.java │ │ └── CouponRepository.java │ │ ├── member │ │ ├── Member.java │ │ ├── MemberApi.java │ │ ├── MemberEventHandler.java │ │ ├── MemberRepository.java │ │ ├── MemberSignUpRequest.java │ │ ├── MemberSignUpService.java │ │ └── MemberSignedUpEvent.java │ │ └── order │ │ ├── Order.java │ │ ├── OrderApi.java │ │ ├── OrderCompletedEvent.java │ │ ├── OrderEventHandler.java │ │ ├── OrderRepository.java │ │ ├── OrderRequest.java │ │ ├── OrderService.java │ │ └── Orderer.java │ └── resources │ └── application.yml └── ch8 ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── java │ └── com │ │ └── code │ │ └── design │ │ ├── Ch8Application.java │ │ ├── Coupon.java │ │ ├── Member.java │ │ ├── MemberApi.java │ │ ├── MemberFindService.java │ │ ├── MemberRepository.java │ │ └── SignUpRequest.java └── resources │ └── application.yml └── test ├── java └── com │ └── code │ └── design │ ├── test_1 │ ├── Junit5_1.java │ ├── Junit5_2.java │ └── Junit5_3.java │ ├── test_2 │ ├── Junit5.java │ └── SpringBoot.java │ ├── test_3 │ ├── Test_1.java │ └── Test_2.java │ └── test_4 │ ├── CouponTest.java │ ├── IntegrationTestSupport.java │ ├── MemberApiTest.java │ ├── MemberFindServiceMockTestSupport.java │ ├── MemberFindServiceTest.java │ ├── MockTestSupport.java │ └── RepositoryTest.java └── resources ├── member-set-up.sql └── member-signup.json /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: question 3 | about: 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 질문 4 | title: 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 질문 5 | labels: question 6 | assignees: cheese10yun 7 | 8 | --- 9 | 10 | ## Part를 선택해주세요 11 | 12 | 13 | 14 | - [ ] P3. 유지보수하기 좋은 코드 디자인 - Ch 01. 엔지니어링에 대해서 15 | - [ ] P3. 유지보수하기 좋은 코드 디자인 - Ch 02. 프로젝트 Setting 16 | - [ ] P3. 유지보수하기 좋은 코드 디자인 - Ch 03. 객체를 풍부하게 표현하기 17 | - [ ] 공통 질문 18 | 19 | ## Chapter: 03. 패키지 구조 20 | 21 | 22 | ## 질문: 프로젝트 규모에 따른 선택 23 | 24 | 25 | 프로젝트 규모에 따라 레이어 형과 도메인형을 나눈 기준이 될까요? 예를 들어 규모가 큰 프로젝트에서는 도메인형이 더 적합한지 이런 것들이 궁금합니다. 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/issuse-question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/.github/issuse-question.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fcheese10yun%2Fcode-design&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) 2 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 3 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 4 | 5 | 6 | # Q&A 7 | 8 | ![](.github/issuse-question.png) 9 | 10 | Q&A는 [Issues question](https://githubcom/cheese10yun/code-design/issues)을 통해서 작성해 주시면 답변드리겠습니다. 11 | -------------------------------------------------------------------------------- /ch2/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | 3 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 4 | 5 | ## Part 1: Project Setting 6 | 7 | ## 1. IntelliJ 꿀팁 공유 8 | 9 | 자세한 내용은 [IntelliJ 사용법](https://github.com/cheese10yun/IntelliJ) 참고 10 | 11 | 1. 플러그인 12 | 1. [CSV](https://plugins.jetbrains.com/plugin/10037-csv) 13 | 2. [Git Toolbox](https://plugins.jetbrains.com/plugin/7499-gittoolbox) 14 | 3. [JPA Buddy](https://plugins.jetbrains.com/plugin/15075-jpa-buddy) 15 | 4. [String Manipulation](https://plugins.jetbrains.com/plugin/2162-string-manipulation) 16 | 2. 사용 팁 17 | 1. Settings Repository: Github Repositroy에 IntelliJ 설정 파일 동기화 18 | 2. Tab Limit 19 | 3. 문자열 20 | 1. 동일 문자열 변경 21 | 2. 동일한 위치 문자열 변경 22 | 4. Live Template 23 | 4. Execute Gradle Task 사용하기 -------------------------------------------------------------------------------- /ch2/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch2/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch2/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch2/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /ch2/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch2/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch2" 2 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/CodeDesignApplication.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class CodeDesignApplication { 7 | 8 | public static void main(String[] args) { 9 | SpringApplication.run(CodeDesignApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/member/api/MemberApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.member.api; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController 7 | @RequestMapping("/api/members") 8 | public class MemberApi { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/member/domain/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.member.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.Id; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @Entity 12 | public class Member { 13 | 14 | @Id 15 | private Long id; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/model/Address.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.model; 2 | 3 | public class Address { 4 | 5 | private String address; 6 | private String zip; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/model/Email.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.model; 2 | 3 | public class Email { 4 | 5 | private String value; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/item/application/OrderItemCalculation.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.item.application; 2 | 3 | // OrderItem 금액을 계산한다 4 | public class OrderItemCalculation { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/item/domain/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.item.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @Entity 13 | public class OrderItem { 14 | 15 | @Id 16 | private Long id; 17 | 18 | @Column(name = "name") 19 | private String name; 20 | 21 | } -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/api/OrderApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.api; 2 | 3 | import org.springframework.web.bind.annotation.PostMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController("/api/orders") 7 | public class OrderApi { 8 | 9 | 10 | @PostMapping() 11 | public void doOrder() { 12 | 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/application/OrderCalculation.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.application; 2 | 3 | /** 4 | * 주문 금액에 대한 계산 로직을 담당 5 | */ 6 | public class OrderCalculation { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/application/OrderRegistrationService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.application; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | /** 6 | * Order 주문 등록을 진행하는 서비스 7 | */ 8 | @Service 9 | public class OrderRegistrationService { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/dao/OrderCustomRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.dao; 2 | 3 | public interface OrderCustomRepository { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/dao/OrderCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.dao; 2 | 3 | public class OrderCustomRepositoryImpl implements OrderCustomRepository { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/dao/OrderFindService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.dao; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | @Service 6 | public class OrderFindService { 7 | 8 | // OrderRepository 에대한 로직 구현.. 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/dao/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.dao; 2 | 3 | import com.code.design.domain.order.order.domain.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface OrderRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/domain/Order.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.domain; 2 | 3 | import com.code.design.domain.model.Address; 4 | import com.code.design.domain.order.item.domain.OrderItem; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import javax.persistence.CollectionTable; 8 | import javax.persistence.ElementCollection; 9 | import javax.persistence.Embedded; 10 | import javax.persistence.Entity; 11 | import javax.persistence.GeneratedValue; 12 | import javax.persistence.GenerationType; 13 | import javax.persistence.Id; 14 | import javax.persistence.JoinColumn; 15 | import javax.persistence.Table; 16 | import lombok.AccessLevel; 17 | import lombok.Getter; 18 | import lombok.NoArgsConstructor; 19 | 20 | @Entity 21 | @Table(name = "orders") 22 | @Getter 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | public class Order { 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | @ElementCollection 31 | @CollectionTable(name = "orders_item", joinColumns = @JoinColumn(name = "id")) 32 | private List orderItems = new ArrayList<>(); 33 | 34 | @Embedded 35 | private Address address; 36 | 37 | } -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/domain/Orderer.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.domain; 2 | 3 | import javax.persistence.Embeddable; 4 | import lombok.Getter; 5 | 6 | @Embeddable 7 | @Getter 8 | public class Orderer { 9 | 10 | private Long id; 11 | private String name; 12 | } 13 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/domain/Tracking.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.domain; 2 | 3 | import com.code.design.domain.model.Address; 4 | import javax.persistence.Embeddable; 5 | import lombok.Getter; 6 | 7 | @Embeddable 8 | @Getter 9 | public class Tracking { 10 | 11 | private Address address; 12 | } 13 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/dto/OrderRegistrationRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.dto; 2 | 3 | import com.code.design.domain.order.order.domain.Orderer; 4 | 5 | /** 6 | * 주문등록시 필요한 Request Body 7 | */ 8 | public class OrderRegistrationRequest { 9 | 10 | private Orderer orderer; 11 | } 12 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/domain/order/order/exception/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.code.design.domain.order.order.exception; 2 | 3 | /** 4 | * 특정 키로 못찾는 경우 발생하는 Exception 5 | */ 6 | public class OrderNotFoundException extends RuntimeException{ 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/global/common/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.code.design.global.common; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | /** 6 | * ErrorResponse 4xx ~ 5xx 관련 Response 7 | */ 8 | public class ErrorResponse { 9 | 10 | private String message; 11 | private LocalDateTime timestamp; 12 | } -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/global/common/PageResponse.java: -------------------------------------------------------------------------------- 1 | package com.code.design.global.common; 2 | 3 | /** 4 | * 페이징 응답에 사용 5 | */ 6 | public class PageResponse { 7 | 8 | private int size; 9 | private int page; 10 | } 11 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/global/config/JpaConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.code.design.global.config; 2 | 3 | /** 4 | * JPA 설정 관련 로직.. 5 | */ 6 | public class JpaConfiguration { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/global/error/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.code.design.global.error; 2 | 3 | /** 4 | * 모든 예외에 대한 글로벌 핸들러 로직 5 | */ 6 | public class GlobalExceptionHandler { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/AmazonSmsClient.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | public class AmazonSmsClient { 9 | 10 | public SendResponse send(final SendRequest dto) { 11 | 12 | final ResponseEntity response = new RestTemplate().postForEntity( 13 | "https://xxxx.xxxx.xxx", dto, SendResponse.class); 14 | 15 | return response.getBody(); 16 | } 17 | 18 | @Getter 19 | @AllArgsConstructor 20 | public static class SendRequest { 21 | 22 | final String message; 23 | final String receiver; 24 | final String itu; 25 | 26 | } 27 | 28 | @Getter 29 | @AllArgsConstructor 30 | public static class SendResponse { 31 | 32 | final String status; // SUCCESS, FAILED_1(없는 전화번호), FAILED_2(수신거부), FAILED_3(실패 사유 알 수 없음) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/AmazonSmsSenderService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | import com.code.design.infra.AmazonSmsClient.SendRequest; 4 | import com.code.design.infra.AmazonSmsClient.SendResponse; 5 | 6 | public class AmazonSmsSenderService implements SmsSender { 7 | 8 | @Override 9 | public boolean send(final SmsMessageRequest dto) { 10 | 11 | final SendResponse response = new AmazonSmsClient().send( 12 | new SendRequest(dto.getMessage(), dto.getReceiverNumber(), dto.getItc())); 13 | 14 | return response.status.equals("SUCCESS"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/KtSmsSenderClient.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | public class KtSmsSenderClient { 9 | 10 | public SendResponse send(final SendRequest dto) { 11 | final ResponseEntity response = new RestTemplate().postForEntity( 12 | "https://xxxx.xxxx.xxx", dto, SendResponse.class); 13 | 14 | return response.getBody(); 15 | } 16 | 17 | @Getter 18 | @AllArgsConstructor 19 | public static class SendRequest { 20 | 21 | final String message; 22 | final String receiver; 23 | 24 | } 25 | 26 | @Getter 27 | @AllArgsConstructor 28 | public static class SendResponse { 29 | 30 | final boolean success; 31 | } 32 | } -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/KtSmsSenderService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | import com.code.design.infra.KtSmsSenderClient.SendRequest; 4 | import com.code.design.infra.KtSmsSenderClient.SendResponse; 5 | 6 | public class KtSmsSenderService implements SmsSender { 7 | 8 | @Override 9 | public boolean send(final SmsMessageRequest dto) { 10 | 11 | final KtSmsSenderClient client = new KtSmsSenderClient(); 12 | final SendResponse response = client.send(new SendRequest(dto.getMessage(), dto.getReceiverNumber())); 13 | return response.success; 14 | } 15 | } -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/SmsMessageRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class SmsMessageRequest { 7 | 8 | private String message; 9 | private String senderNumber; 10 | private String receiverNumber; 11 | private String itc; 12 | } 13 | -------------------------------------------------------------------------------- /ch2/src/main/java/com/code/design/infra/SmsSender.java: -------------------------------------------------------------------------------- 1 | package com.code.design.infra; 2 | 3 | public interface SmsSender { 4 | 5 | boolean send(final SmsMessageRequest smsMessageRequest); 6 | } -------------------------------------------------------------------------------- /ch2/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8181 3 | 4 | spring: 5 | jpa: 6 | database: h2 7 | hibernate: 8 | ddl-auto: create 9 | show-sql: true 10 | open-in-view: false 11 | 12 | management: 13 | endpoints: 14 | web: 15 | exposure: 16 | include: 17 | - "*" 18 | 19 | logging: 20 | level: 21 | root: info -------------------------------------------------------------------------------- /ch2/src/main/resources/test.csv: -------------------------------------------------------------------------------- 1 | JOB_INSTANCE_ID,VERSION,JOB_NAME,JOB_KEY 2 | 1,0,readerPerformanceJob,853d3449e311f40366811cbefb3d93d7 3 | 2,0,readerPerformanceJob,e070bff4379694c0210a51d9f6c6a564 4 | 3,0,readerPerformanceJob,a3364faf893276dea0caacefbf618db5 5 | 4,0,readerPerformanceJob,47c0a8118b74165a864b66d37c7b6cf5 6 | 5,0,bulkInsertJob,853d3449e311f40366811cbefb3d93d7 7 | 6,0,bulkInsertJob,e070bff4379694c0210a51d9f6c6a564 8 | 7,0,bulkInsertJob,a3364faf893276dea0caacefbf618db5 9 | 8,0,bulkInsertJob,47c0a8118b74165a864b66d37c7b6cf5 10 | 9,0,bulkInsertJob,ce148f5f9c2bf4dc9bd44a7a5f64204c 11 | 10,0,readerPerformanceJob,ce148f5f9c2bf4dc9bd44a7a5f64204c 12 | 11,0,readerPerformanceJob,bd0034040292bc81e6ccac0e25d9a578 13 | 12,0,readerPerformanceJob,597815c7e4ab1092c1b25130aae725cb 14 | 13,0,readerPerformanceJob,f55a96b11012be4fcfb6cf005435182d 15 | 14,0,readerPerformanceJob,96a5ed9bac43e779455f3e71c0f64840 16 | 15,0,readerPerformanceJob,1aac4f3e74894b78fa3ce5d8a25e1ef0 17 | 16,0,readerPerformanceJob,604bbfc4c68cb1f903780c2853ad4801 18 | 17,0,readerPerformanceJob,556ebe34220b4032509f2581356ba47c 19 | 18,0,readerPerformanceJob,edc440efb5ddd2a3b2622f16a12bf105 20 | 19,0,readerPerformanceJob,f3d5e568c384ee72cba8bc6a51057fe4 21 | 20,0,readerPerformanceJob,378ef1ecb81cf9edac4ab119bdab9d9d 22 | 21,0,readerPerformanceJob,e073471cc312cadef424c3be7915c0af 23 | 22,0,readerPerformanceJob,46ba78a99abf1e2fba4a8861749d7572 24 | 23,0,readerPerformanceJob,b88d31b704adf9f94fe9d4ccff795708 25 | 24,0,readerPerformanceJob,64d4e6d635ee3ad949314224afce46c2 26 | 25,0,readerPerformanceJob,75c16c09800a944220a789de10278de0 27 | 26,0,readerPerformanceJob,1b759d32440acdcbf90da6919b5d16ad 28 | 27,0,readerPerformanceJob,1f995cec4b562af773a2e473c369f069 29 | 28,0,readerPerformanceJob,42106293a859255c2b210d04a51240ca 30 | 29,0,readerPerformanceJob,9799b3a84f4d6f15a5e8c11360e7387b 31 | 30,0,readerPerformanceJob,6eecb29840a845c35cfa9b2da21862f9 32 | 31,0,readerPerformanceJob,e465389b77512db6f30ed6a3b7a9682c 33 | 32,0,readerPerformanceJob,19be35489361f0d498838921e450c4cb 34 | 33,0,readerPerformanceJob,20744cb6ca7f8dc12940aa7fd8f89763 35 | 34,0,readerPerformanceJob,a3bcf78496166aaf18ec0c14120074d6 36 | -------------------------------------------------------------------------------- /ch2/src/main/resources/test.sql: -------------------------------------------------------------------------------- 1 | insert into member(id, name, email) 2 | values (1, 'test1', 'test1@test.com'), 3 | (1, 'test1', 'test1@test.com'), 4 | (1, 'test1', 'test1@test.com'), 5 | (1, 'test1', 'test1@test.com'), 6 | (1, 'test1', 'test1@test.com'), 7 | (1, 'test1', 'test1@test.com'), 8 | (1, 'test1', 'test1@test.com'), 9 | (1, 'test1', 'test1@test.com'), 10 | (1, 'test1', 'test1@test.com'), 11 | (1, 'test1', 'test1@test.com'), 12 | (1, 'test1', 'test1@test.com'), 13 | (1, 'test1', 'test1@test.com'), 14 | (1, 'test1', 'test1@test.com'); 15 | 16 | insert into orders(id, member_id) 17 | values (1, 1), 18 | (1, 1), 19 | (1, 1), 20 | (1, 1), 21 | (1, 1), 22 | (1, 1), 23 | (1, 1), 24 | (1, 1), 25 | (1, 1), 26 | (1, 1), 27 | (1, 1), 28 | (1, 1), 29 | (1, 1), 30 | (1, 1) 31 | ; -------------------------------------------------------------------------------- /ch3/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | 3 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 4 | 5 | ## Ch3. 객체를 풍부하게 표현하기 6 | 7 | 1. Lombok을 잘 사용해야 객체 디지인을 망치지 않는다. 8 | 2. 객체를 도메인 요구사항에 맞게 풍부하게 표현 9 | 3. 횐불 객체를 도메인 요구사항에 맞게 풍부하게 표현 -------------------------------------------------------------------------------- /ch3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch3/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch3/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch3/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /ch3/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch3/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.setter.flagUsage=ERROR 2 | lombok.toString.flagUsage=WARNING -------------------------------------------------------------------------------- /ch3/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch3" 2 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/CodeDesignApplication.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class CodeDesignApplication { 7 | 8 | public static void main(String[] args) { 9 | SpringApplication.run(CodeDesignApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/lombok/Coupon.java: -------------------------------------------------------------------------------- 1 | package com.code.design.lombok; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.JoinColumn; 10 | import javax.persistence.ManyToOne; 11 | import javax.persistence.Table; 12 | import lombok.Data; 13 | import org.hibernate.annotations.CreationTimestamp; 14 | import org.hibernate.annotations.UpdateTimestamp; 15 | 16 | @Entity 17 | @Table(name = "coupon") 18 | @Data 19 | public class Coupon { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id", updatable = false) 24 | private Long id; 25 | 26 | @Column(name = "used", nullable = false) 27 | private boolean used; 28 | 29 | @ManyToOne 30 | @JoinColumn(name = "member_id", updatable = false) 31 | private Member member; 32 | 33 | @CreationTimestamp 34 | @Column(name = "create_at", nullable = false, updatable = false) 35 | private LocalDateTime createAt; 36 | 37 | @UpdateTimestamp 38 | @Column(name = "update_at", nullable = false) 39 | private LocalDateTime updateAt; 40 | } -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/lombok/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design.lombok; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.JoinColumn; 12 | import javax.persistence.OneToMany; 13 | import javax.persistence.Table; 14 | import lombok.Data; 15 | import lombok.ToString.Exclude; 16 | import org.hibernate.annotations.CreationTimestamp; 17 | import org.hibernate.annotations.UpdateTimestamp; 18 | 19 | @Entity 20 | @Table(name = "member") 21 | @Data 22 | public class Member { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | private Long id; 27 | 28 | @Column(name = "email", nullable = false, updatable = false, unique = true) 29 | private String email; 30 | 31 | @Column(name = "name", nullable = false) 32 | private String name; 33 | 34 | @OneToMany 35 | @JoinColumn(name = "coupon_id") 36 | @Exclude 37 | private List coupons = new ArrayList<>(); 38 | 39 | @CreationTimestamp 40 | @Column(name = "create_at", nullable = false, updatable = false) 41 | private LocalDateTime createAt; 42 | 43 | @UpdateTimestamp 44 | @Column(name = "update_at", nullable = false) 45 | private LocalDateTime updateAt; 46 | } -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/lombok/Student.java: -------------------------------------------------------------------------------- 1 | package com.code.design.lombok; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import lombok.Builder; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | import org.hibernate.annotations.CreationTimestamp; 14 | import org.hibernate.annotations.UpdateTimestamp; 15 | 16 | @Entity 17 | @Table(name = "student") 18 | public class Student { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @Column(name = "email", nullable = false, updatable = false, unique = true) 25 | private String email; 26 | 27 | @Column(name = "name", nullable = false) 28 | private String name; 29 | 30 | @CreationTimestamp 31 | @Column(name = "create_at", nullable = false, updatable = false) 32 | private LocalDateTime createAt; 33 | 34 | @UpdateTimestamp 35 | @Column(name = "update_at", nullable = false) 36 | private LocalDateTime updateAt; 37 | 38 | @Builder 39 | public Student(final String email, final String name) { 40 | this.email = email; 41 | this.name = name; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "Student{" + 47 | "id=" + id + 48 | ", email='" + email + '\'' + 49 | ", name='" + name + '\'' + 50 | ", createAt=" + createAt + 51 | ", updateAt=" + updateAt + 52 | '}'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/object/Account.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import lombok.AccessLevel; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.util.Assert; 10 | 11 | @Embeddable 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class Account { 15 | 16 | @Column(name = "bank_name", nullable = false) 17 | private String bankName; 18 | 19 | @Column(name = "account_number", nullable = false) 20 | private String accountNumber; 21 | 22 | @Column(name = "account_holder", nullable = false) 23 | private String accountHolder; 24 | 25 | // 불안전한 객채 생성 패턴 26 | // 그냥 단순하게 검증 아니라, 객체의 본인의 책임을 다하는 코드로 변경 했음 27 | @Builder 28 | public Account(final String bankName, final String accountNumber, final String accountHolder) { 29 | Assert.hasText(bankName, "bankName mut not be empty"); 30 | Assert.hasText(accountNumber, "accountNumber mut not be empty"); // 특수문자 제거 or "-" 제거 31 | Assert.hasText(accountHolder, "accountHolder mut not be empty"); 32 | 33 | this.bankName = bankName; 34 | this.accountNumber = accountNumber; 35 | this.accountHolder = accountHolder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/object/Address.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import lombok.AccessLevel; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.util.Assert; 10 | 11 | @Embeddable 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class Address { 15 | 16 | @Column(name = "address1", nullable = false) 17 | private String address1; 18 | 19 | @Column(name = "address2", nullable = false) 20 | private String address2; 21 | 22 | @Column(name = "zip", nullable = false) 23 | private String zip; 24 | 25 | @Builder 26 | public Address(final String address1, final String address2, final String zip) { 27 | Assert.hasText(address1, "address1 must not be empty"); 28 | Assert.hasText(address2, "address2 must not be empty"); 29 | Assert.hasText(zip, "zip must not be empty"); 30 | 31 | this.address1 = address1; 32 | this.address2 = address2; 33 | this.zip = zip; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/object/CreditCard.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import lombok.AccessLevel; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.util.Assert; 10 | 11 | @Embeddable 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class CreditCard { 15 | 16 | @Column(name = "credit_number", nullable = false) 17 | private String creditNumber; 18 | 19 | @Column(name = "credit__holder", nullable = false) 20 | private String creditHolder; 21 | 22 | @Builder 23 | public CreditCard(final String creditNumber, final String creditHolder) { 24 | Assert.hasText(creditNumber, "creditNumber must not be empty"); 25 | Assert.hasText(creditHolder, "creditHolder must not be empty"); 26 | 27 | this.creditNumber = creditNumber; 28 | this.creditHolder = creditHolder; 29 | } 30 | } -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/object/Order.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import javax.persistence.Embedded; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | import lombok.AccessLevel; 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import org.springframework.util.Assert; 14 | 15 | @Entity 16 | @Table(name = "orders") 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | public class Order { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | 25 | @Embedded 26 | private Address address; 27 | 28 | @Builder 29 | public Order(Address address) { 30 | Assert.notNull(address, "address must not be null"); 31 | 32 | this.address = address; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch3/src/main/java/com/code/design/object/Refund.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import javax.persistence.Embedded; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.OneToOne; 10 | import javax.persistence.Table; 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | import org.springframework.util.Assert; 15 | 16 | @Entity 17 | @Table(name = "refund") 18 | @Getter 19 | @NoArgsConstructor 20 | public class Refund { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private long id; 25 | 26 | @Embedded 27 | private Account account; 28 | 29 | @Embedded 30 | private CreditCard creditCard; 31 | 32 | @OneToOne 33 | @JoinColumn(name = "order_id", nullable = false) 34 | private Order order; 35 | 36 | 37 | @Builder(builderClassName = "ByAccountBuilder", builderMethodName = "ByAccountBuilder") // 계좌 번호 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확 38 | public Refund(Account account, Order order) { 39 | Assert.notNull(account, "account must not be null"); 40 | Assert.notNull(order, "order must not be null"); 41 | 42 | this.order = order; 43 | this.account = account; 44 | } 45 | 46 | @Builder(builderClassName = "ByCreditBuilder", builderMethodName = "ByCreditBuilder") // 신용 카드 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확 47 | public Refund(CreditCard creditCard, Order order) { 48 | Assert.notNull(creditCard, "creditCard must not be null"); 49 | Assert.notNull(order, "order must not be null"); 50 | 51 | this.order = order; 52 | this.creditCard = creditCard; 53 | } 54 | } -------------------------------------------------------------------------------- /ch3/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8181 3 | 4 | spring: 5 | jpa: 6 | database: h2 7 | hibernate: 8 | ddl-auto: create 9 | show-sql: true 10 | open-in-view: false 11 | 12 | management: 13 | endpoints: 14 | web: 15 | exposure: 16 | include: 17 | - "*" 18 | 19 | logging: 20 | level: 21 | root: info -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/lombok/MemberTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.lombok; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class MemberTest { 8 | 9 | @Test 10 | public void setter_남용의_문제() { 11 | // 우리는 회원에 대한 이메일 변경 기능을 제공하지 않는다. 12 | final Member member = new Member(); 13 | 14 | // bean 방식 15 | member.setEmail("asd@asd.com"); 16 | member.setName("name"); 17 | // 객체 생성 완료 18 | // 앞으로 추가적인 이메일 변경은 불가능 해야한다. 19 | 20 | member.setEmail("new@asd.com"); 21 | } 22 | 23 | @Test 24 | public void toString_양방향_순한_참조_문제() { 25 | final Member member = new Member(); 26 | member.setEmail("asd@asd.com"); 27 | member.setName("name"); 28 | 29 | final Coupon coupon = new Coupon(); 30 | coupon.setMember(member); 31 | 32 | final List coupons = new ArrayList<>(); 33 | coupons.add(coupon); 34 | member.setCoupons(coupons); 35 | 36 | System.out.println(member); // toString 순한 참조 발생, java.la 37 | } 38 | 39 | @Test 40 | public void EqualsAndHashCode_의_문제() { 41 | } 42 | 43 | // @Test 44 | // public void 클래스_상단의_Builder_의_문제() { 45 | // 46 | // // id, createdAt, updatedAt은 데이터베이스에서 지정하기로 했는데 설정이 가능하다. 47 | // // email, name은 필수 값인데 48 | // Member.builder() 49 | // .id(1L) 50 | // .createAt(LocalDateTime.of(2021, 12, 12, 12, 12)) 51 | // .updateAt(LocalDateTime.of(2010, 12, 12, 12, 12)) 52 | // .build(); 53 | // 54 | // // 이미 사용한 쿠폰을 만들 수 있다. 55 | // Coupon.builder() 56 | // .used(false) 57 | // .build(); 58 | // } 59 | // 60 | // @Test 61 | // public void Builder_default_는_지양하자() { 62 | // final Member member = Member.builder() 63 | // .build(); 64 | // 65 | // then(member.getEmail()).isEqualTo("test@test.com"); // POJO 값 그대로 예상 66 | // then(member.getName()).isEqualTo("yun"); // POJO 값 그대로 예상 67 | // } 68 | // 69 | // @Test 70 | // public void 생성자위_Builder_의_적적한_책임_부여() { 71 | // final Member member = Member.builder() 72 | // .name("asd") 73 | // .email("asd@asd.com") 74 | // .build(); 75 | // 76 | //// Coupon.builder() 77 | //// .member(member) 78 | //// .build(); 79 | // 80 | // } 81 | // 82 | // @Test 83 | // public void 생성자_접근_지시자는_최소한_으로() { 84 | // final Coupon coupon = new Coupon(); // 필수값, 비지니스로직을 모두 무시하고 객체 생성 가능 85 | // final Member member = new Member(); // 필수값, 비지니스로직을 모두 무시하고 객체 생성 가능 86 | // } 87 | } -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/lombok/StudentTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.lombok; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | class StudentTest { 6 | 7 | @Test 8 | // Student 자기 자신의 객체가, 본인의 생성되는 플로우에서 보다 명확하게 필요한 값과 필요하지 않은 값을 구분해서 받게 한다. 9 | public void 클래스_상단의_Builder의_단점() { 10 | final Student student = Student.builder() 11 | .email("asd@asd.com") 12 | .name("asd") 13 | .build(); 14 | 15 | System.out.println(student); 16 | } 17 | } -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/object/AccountTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class AccountTest { 9 | 10 | @Test 11 | public void Account_account_holder_비어있으면_exception() { 12 | thenThrownBy(() -> Account.builder() 13 | .accountHolder("") 14 | .bankName("KB") 15 | .accountNumber("11010101010") 16 | .build() 17 | ).isInstanceOf(IllegalArgumentException.class); 18 | } 19 | 20 | @Test 21 | public void Account_account_holder_null_exception() { 22 | thenThrownBy(() -> Account.builder() 23 | .accountHolder(null) 24 | .bankName("KB") 25 | .accountNumber("11010101010") 26 | .build() 27 | ).isInstanceOf(IllegalArgumentException.class); 28 | } 29 | 30 | @Test 31 | public void Account_bank_name_비어있으면_exception() { 32 | thenThrownBy(() -> Account.builder() 33 | .accountHolder("yun") 34 | .bankName("") 35 | .accountNumber("11010101010") 36 | .build() 37 | ).isInstanceOf(IllegalArgumentException.class); 38 | } 39 | 40 | @Test 41 | public void Account_bank_name_null_exception() { 42 | thenThrownBy(() -> Account.builder() 43 | .accountHolder("yun") 44 | .bankName(null) 45 | .accountNumber("11010101010") 46 | .build() 47 | ).isInstanceOf(IllegalArgumentException.class); 48 | } 49 | 50 | @Test 51 | public void Account_account_number_비어있으면_exception() { 52 | thenThrownBy(() -> Account.builder() 53 | .accountHolder("yun") 54 | .bankName("KB") 55 | .accountNumber("") 56 | .build() 57 | ).isInstanceOf(IllegalArgumentException.class); 58 | } 59 | 60 | @Test 61 | public void Account_account_number_null_exception() { 62 | thenThrownBy(() -> Account.builder() 63 | .accountHolder("yun") 64 | .bankName("KB") 65 | .accountNumber(null) 66 | .build() 67 | ).isInstanceOf(IllegalArgumentException.class); 68 | } 69 | 70 | @Test 71 | public void Account_모든_필수값을_입력_하면_성공() { 72 | final Account account = Account.builder() 73 | .accountHolder("yun") 74 | .bankName("KB") 75 | .accountNumber("11010101010") 76 | .build(); 77 | 78 | then(account.getAccountHolder()).isEqualTo("yun"); 79 | then(account.getBankName()).isEqualTo("KB"); 80 | then(account.getAccountNumber()).isEqualTo("11010101010"); 81 | } 82 | } -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/object/AddressTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class AddressTest { 9 | 10 | @Test 11 | public void Address_address1_비어있으면_exception() { 12 | thenThrownBy(() -> Address.builder() 13 | .address1("") 14 | .address2("address 2") 15 | .zip("zip") 16 | .build()) 17 | .isInstanceOf(IllegalArgumentException.class); 18 | } 19 | 20 | @Test 21 | public void Address_address2_비어있으면_exception() { 22 | thenThrownBy(() -> Address.builder() 23 | .address1("address 1") 24 | .address2("") 25 | .zip("zip") 26 | .build()) 27 | .isInstanceOf(IllegalArgumentException.class); 28 | } 29 | 30 | @Test 31 | public void Address_zip_비어있으면_exception() { 32 | thenThrownBy(() -> Address.builder() 33 | .address1("address 1") 34 | .address2("address 2") 35 | .zip("") 36 | .build()) 37 | .isInstanceOf(IllegalArgumentException.class); 38 | } 39 | 40 | @Test 41 | public void Address_test() { 42 | final Address address = Address.builder() 43 | .address1("address 1") 44 | .address2("address 2") 45 | .zip("zip") 46 | .build(); 47 | 48 | then(address.getAddress1()).isEqualTo("address 1"); 49 | then(address.getAddress2()).isEqualTo("address 2"); 50 | then(address.getZip()).isEqualTo("zip"); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/object/CreditCardTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 4 | import static org.assertj.core.api.Java6Assertions.assertThat; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class CreditCardTest { 9 | 10 | @Test 11 | public void Account_creditNumber_비어있으면_exception() { 12 | thenThrownBy(() -> CreditCard.builder() 13 | .creditNumber("") 14 | .creditHolder("홍길동") 15 | .build() 16 | ).isInstanceOf(IllegalArgumentException.class); 17 | } 18 | 19 | 20 | @Test 21 | public void Account_creditHolder_비어있으면_exception() { 22 | thenThrownBy(() -> CreditCard.builder() 23 | .creditNumber("10-22345-22345") 24 | .creditHolder("") 25 | .build() 26 | ).isInstanceOf(IllegalArgumentException.class); 27 | } 28 | 29 | @Test 30 | public void Account_test() { 31 | final CreditCard address = CreditCard.builder() 32 | .creditNumber("110-22345-22345") 33 | .creditHolder("홍길동") 34 | .build(); 35 | 36 | assertThat(address.getCreditHolder()).isEqualTo("홍길동"); 37 | assertThat(address.getCreditNumber()).isEqualTo("110-22345-22345"); 38 | } 39 | } -------------------------------------------------------------------------------- /ch3/src/test/java/com/code/design/object/RefundTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.object; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class RefundTest { 10 | 11 | private Order order; 12 | private Account account; 13 | private CreditCard creditCard; 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | final Address address = Address.builder() 18 | .address1("서울시 관악구 293-1") 19 | .address2("201호") 20 | .zip("503-23") 21 | .build(); 22 | 23 | account = Account.builder() 24 | .accountHolder("홍길동") 25 | .accountNumber("110-2304-22344") 26 | .bankName("신한은행") 27 | .build(); 28 | 29 | creditCard = CreditCard.builder() 30 | .creditNumber("110-22345-22345") 31 | .creditHolder("홍길동") 32 | .build(); 33 | 34 | order = Order.builder() 35 | .address(address) 36 | .build(); 37 | } 38 | 39 | @Test 40 | public void ByAccountBuilder_test_account_null이면_exception() { 41 | 42 | thenThrownBy(() -> Refund.ByAccountBuilder() 43 | .account(null) 44 | .order(order) 45 | .build() 46 | ) 47 | .isInstanceOf(IllegalArgumentException.class); 48 | } 49 | 50 | @Test 51 | public void ByAccountBuilder_testorder_null이면_exception() { 52 | thenThrownBy(() -> Refund.ByAccountBuilder() 53 | .account(account) 54 | .order(null) 55 | .build() 56 | ) 57 | .isInstanceOf(IllegalArgumentException.class); 58 | } 59 | 60 | @Test 61 | public void ByAccountBuilder_test() { 62 | final Refund refund = Refund.ByAccountBuilder() 63 | .account(account) 64 | .order(order) 65 | .build(); 66 | 67 | then(refund.getAccount()).isEqualTo(account); 68 | then(refund.getOrder()).isEqualTo(order); 69 | } 70 | 71 | 72 | @Test 73 | public void ByCreditBuilder_test_account_null이면_exception() { 74 | thenThrownBy(() -> Refund.ByAccountBuilder() 75 | .account(null) 76 | .order(order) 77 | .build() 78 | ) 79 | .isInstanceOf(IllegalArgumentException.class); 80 | } 81 | 82 | @Test 83 | public void ByCreditBuilder_test_order_null이면_exception() { 84 | thenThrownBy(() -> Refund.ByCreditBuilder() 85 | .creditCard(creditCard) 86 | .order(null) 87 | .build() 88 | ) 89 | .isInstanceOf(IllegalArgumentException.class); 90 | } 91 | } -------------------------------------------------------------------------------- /ch4/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 3 | 4 | 5 | 1. Exception 처리를 왜 해야할까요? 6 | 2. Check Exception VS UnChecked Exception 7 | 8 | 9 | ## 오류 코드 코드보다 예외를 사용하라 10 | 11 | > [클린 코드 내](http://www.yes24.com/Product/Goods/11681152) 12 | 13 | ```java 14 | public class DeviceController { 15 | ... 16 | public void sendShutDown() { 17 | DeviceHandle handle = getHandle(DEV1); 18 | // 디바이스 상태를 점검한댜. 19 | if (handle != DeviceHandle.INVALID) { 20 | // 레코드 필드에 디바이스 상태를 저장한다. 21 | retrieveDeviceRecord(handle); 22 | // 디바이스가 일시정지 상태가 아니라면 종료한다. 23 | if (record.getStatus() != DEVICE_SUSPENDED) { 24 | pauseDevice(handle); 25 | clearDeviceWorkQueue(handle); 26 | closeDevice(handle); 27 | } else { 28 | logger.log("Device suspended. Unable to shut down"); 29 | } 30 | } else { 31 | logger.log("Invalid handle for: " + DEV1.toString()); 32 | } 33 | } 34 | ... 35 | } 36 | ``` 37 | * 비지니스 로직과 오류 처리 코드가 함께 있어 코드가 복잡하하다 38 | * 무슨 오류가 있는지 명확하게 파악이 힘들다. 39 | 40 | ```java 41 | public class DeviceController { 42 | ... 43 | public void sendShutDown() { 44 | try { 45 | tryToShutDown(); 46 | } catch (DeviceShutDownError e) { 47 | // 적절한 Exception을 발생시키는것이 더 바람직하다. 48 | logger.log(e); 49 | } 50 | } 51 | 52 | private void tryToShutDown() throws DeviceShutDownError { 53 | DeviceHandle handle = getHandle(DEV1); 54 | DeviceRecord record = retrieveDeviceRecord(handle); 55 | pauseDevice(handle); 56 | clearDeviceWorkQueue(handle); 57 | closeDevice(handle); 58 | } 59 | 60 | private DeviceHandle getHandle(DeviceID id) { 61 | ... 62 | throw new DeviceShutDownError("Invalid handle for: " + id.toString()); 63 | ... 64 | } 65 | 66 | ``` 67 | * **비지니스 코드와 오류 처리 코드가 분리되어 가독성이 좋다** 68 | * **무슨 예외가 왜 발생하는지 명확해짐** -------------------------------------------------------------------------------- /ch4/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch4/client/api.http: -------------------------------------------------------------------------------- 1 | GET localhost:8080/members 2 | 3 | ### 4 | 5 | POST localhost:8080/members/unchekced 6 | 7 | ### 8 | 9 | POST localhost:8080/members/chekced 10 | 11 | ### -------------------------------------------------------------------------------- /ch4/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch4/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch4/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /ch4/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch4/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch4" 2 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/Ch4Application.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Ch4Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Ch4Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/Exception1.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | @RequiredArgsConstructor 9 | @Slf4j 10 | public class Exception1 { 11 | 12 | private final MemberRepository memberRepository; 13 | 14 | // 예외가 발생핬지만, 아무 처리도 하지 않고 로직을 진행한다. 15 | public void doXXX1(final Long id) { 16 | String name = null; 17 | try { 18 | final Member member = memberRepository.findById(id).get(); 19 | name = member.getName(); 20 | } catch (Exception e) { 21 | // 추가적인 작업을 진행하기 위해... 22 | log.error(e.getMessage()); 23 | } 24 | System.out.println(name); 25 | } 26 | 27 | // 예외가 발생하면 예외 로그라도 찍는다. 28 | public void doXXX2(final Long id) { 29 | String name = null; 30 | try { 31 | final Member member = memberRepository.findById(id).get(); 32 | name = member.getName(); 33 | } catch (Exception e) { 34 | e.printStackTrace(); 35 | log.error(e.getMessage()); 36 | } 37 | System.out.println(name); 38 | } 39 | 40 | // 예외가 발생하면 로그를 찍고, 더 구체적인 예외를 발생 시킨다. 41 | // 예외가 발생하면 로직을 이어갈 수 없는 경우는 Exception을 발생시켜 코드의 흐름을 끊는다. 42 | public void doXXX3(final Long id) { 43 | String name = null; 44 | try { 45 | final Member member = memberRepository.findById(id).get(); 46 | name = member.getName(); 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | throw new IllegalArgumentException("해당 id: " + id + " 의 member의 name은 null 입니다."); // 더 구체적인 예외를 직접 정의해서 사용해도 무방 50 | } 51 | System.out.println(name); 52 | } 53 | 54 | // 예외 처리가 가능하다면, Exception을 발생시키지 않고 로직적으로 풀어낸다 55 | public void doXXX4(final Long id) { 56 | String name = null; 57 | 58 | final Member member = memberRepository.findById(id).get(); 59 | // exchange api 신한, 하나 -> 다른 대안을 찾을 수 있는지 검토 60 | 61 | if (member != null) { 62 | name = member.getName(); 63 | } else { 64 | name = "Default Name"; 65 | } 66 | System.out.println(name); 67 | } 68 | 69 | // 1. try catch를 최대한 지양해라.(로직으로 예외 처리가 가능하다면) 70 | // 2. try catch를 하는데 아무런 처리가 없다면 로그라도 추가하자 71 | // 3. try catch를 사용하게 된다면, 더 구체적인 예외를 발생시키는것이 좋다. (Exception 직접 정의 or Error Message를 명확하게) 72 | } 73 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/Exception2.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.stereotype.Service; 6 | 7 | // Check Exception VS UnChecked Exception 8 | @Service 9 | public class Exception2 { 10 | 11 | public void signUp(final String name) { 12 | final Member member = new Member(name); 13 | // printJson1(member); // printJson1 사용시 예외처리에 대한 위임을 받아 처리 해야한다. 14 | // printJson2(member); // printJson2 사용시 예외처리를 위임 받지 않아도 된다. 15 | 16 | } 17 | 18 | 19 | // throws를 통해서 호출한 메서드로 예외처리를 위임한다 20 | private void printJson1(final Member member) throws JsonProcessingException { 21 | final ObjectMapper objectMapper = new ObjectMapper(); 22 | final String valueAsString = objectMapper.writeValueAsString(member); 23 | System.out.println(valueAsString); 24 | } 25 | 26 | // 예외 발생시 본인의 로직에서 예외를 발생시켜 예외처리를 호출한 쪽으로 위임하지 않는다. 27 | private void printJson2(final Member member) { 28 | final ObjectMapper objectMapper = new ObjectMapper(); 29 | final String valueAsString; 30 | try { 31 | valueAsString = objectMapper.writeValueAsString(member); 32 | } catch (JsonProcessingException e) { 33 | e.printStackTrace(); 34 | throw new RuntimeException(); 35 | } 36 | System.out.println(valueAsString); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @Entity 16 | public class Member { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | 22 | @Column(name = "name") 23 | private String name; 24 | 25 | public Member(String name) { 26 | this.name = name; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/MemberApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/members") 13 | @RequiredArgsConstructor 14 | public class MemberApi { 15 | 16 | private final MemberService memberService; 17 | private final MemberRepository memberRepository; 18 | 19 | @GetMapping 20 | public List getAll() { 21 | return memberRepository.findAll(); 22 | } 23 | 24 | // rollback 진행 O 25 | @PostMapping("/unchekced") 26 | public Member unchekced() { 27 | final Member member = memberService.createUncheckedException(); 28 | return member; 29 | } 30 | 31 | // rollback 진행 X 32 | @PostMapping("/chekced") 33 | public Member chekced() throws IOException { 34 | final Member member = memberService.createCheckedException(); 35 | return member; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface MemberRepository extends JpaRepository { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ch4/src/main/java/com/code/design/MemberService.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import java.io.IOException; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | @Service 9 | @RequiredArgsConstructor 10 | @Transactional 11 | public class MemberService { 12 | 13 | private final MemberRepository memberRepository; 14 | 15 | public Member createUncheckedException() { 16 | final Member member = memberRepository.save(new Member("yun")); 17 | if (true) { 18 | throw new RuntimeException(); 19 | } 20 | return member; 21 | } 22 | 23 | public Member createCheckedException() throws IOException { 24 | final Member member = memberRepository.save(new Member("wan")); 25 | if (true) { 26 | throw new IOException(); 27 | } 28 | return member; 29 | } 30 | 31 | public Member findById(long id) { 32 | return memberRepository.findById(id).get(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ch4/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true -------------------------------------------------------------------------------- /ch5/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | 3 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 4 | 5 | # API Server Error 처리 6 | 7 | 1. 통일된 Error Response를 가져야하는 이유 8 | 2. @ControllerAdvice를 활용한 일관된 예외 핸들링 9 | 3. 클라이언트에게 Error Message를 어떻게 노출 시켜야할까? 10 | 4. 계층화를 통한 Business Exception 처리 방법 11 | 5. 효율적인 Validation -------------------------------------------------------------------------------- /ch5/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch5/client/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "host": "http://localhost:8080" 4 | } 5 | } -------------------------------------------------------------------------------- /ch5/client/order-api-test.http: -------------------------------------------------------------------------------- 1 | 2 | # 무통장 결제 주문 3 | POST {{host}}/orders 4 | Content-Type: application/json 5 | 6 | { 7 | "price": 100.00, 8 | "payment": { 9 | "paymentMethod": "BANK_TRANSFER", 10 | "account": { 11 | "number": "110-202034-2234", 12 | "bankCode": "2003", 13 | "holder": "홍길동" 14 | } 15 | }, 16 | "address": { 17 | "city": "NOWON-GU", 18 | "state": "SEOUL", 19 | "zipCode": "09876" 20 | } 21 | } 22 | 23 | ### 24 | 25 | 26 | # 카드 결제 주문 27 | POST {{host}}/orders 28 | Content-Type: application/json 29 | 30 | 31 | { 32 | "price": 100.00, 33 | "payment": { 34 | "paymentMethod": "CARD", 35 | "card": { 36 | "number": "25523-22394", 37 | "brand": "323", 38 | "csv": "삼성카드" 39 | } 40 | }, 41 | "address": { 42 | "city": "NOWON-GU", 43 | "state": "SEOUL", 44 | "zipCode": "09876?" 45 | } 46 | } 47 | 48 | ### 49 | 50 | # 무통장 결제 주문 51 | # 계좌번호 없는 경 52 | POST {{host}}/orders 53 | Content-Type: application/json 54 | 55 | { 56 | "price": 100.00, 57 | "payment": { 58 | "paymentMethod": "BANK_TRANSFER", 59 | "account": { 60 | "bankCode": "2003", 61 | "holder": "홍길동" 62 | } 63 | }, 64 | "address": { 65 | "city": "NOWON-GU", 66 | "state": "SEOUL", 67 | "zipCode": "09876" 68 | } 69 | } 70 | 71 | ### 72 | 73 | # 무통장 결제 주문 74 | # 무통장 결제인데 카드 정보 입력하는 경우 75 | POST {{host}}/orders 76 | Content-Type: application/json 77 | 78 | { 79 | "price": 100.00, 80 | "payment": { 81 | "paymentMethod": "BANK_TRANSFER", 82 | "card": { 83 | "number": "25523-22394", 84 | "brand": "323", 85 | "csv": "삼성카드" 86 | } 87 | }, 88 | "address": { 89 | "city": "NOWON-GU", 90 | "state": "SEOUL", 91 | "zipCode": "09876" 92 | } 93 | } 94 | 95 | ### 96 | 97 | # 카드 결제 주문 98 | # 카드 결제인데 무통장 정보 입력하는 경우 99 | POST {{host}}/orders 100 | Content-Type: application/json 101 | 102 | { 103 | "price": 100.00, 104 | "payment": { 105 | "paymentMethod": "CARD", 106 | "account": { 107 | "number": "110-202034-2234", 108 | "bankCode": "2003", 109 | "holder": "홍길동" 110 | } 111 | }, 112 | "address": { 113 | "city": "NOWON-GU", 114 | "state": "SEOUL", 115 | "zipCode": "09876" 116 | } 117 | } 118 | 119 | ### 120 | -------------------------------------------------------------------------------- /ch5/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch5/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch5/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch5/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch5" 2 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/Ch5Application.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Ch5Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Ch5Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | public enum ErrorCode { 4 | 5 | // Common 6 | INVALID_INPUT_VALUE(400, "C001", " Invalid Input Value"), 7 | METHOD_NOT_ALLOWED(405, "C002", " Invalid Input Value"), 8 | ENTITY_NOT_FOUND(400, "C003", " Entity Not Found"), 9 | INTERNAL_SERVER_ERROR(500, "C004", "Server Error"), 10 | INVALID_TYPE_VALUE(400, "C005", " Invalid Type Value"), 11 | HANDLE_ACCESS_DENIED(403, "C006", "Access is Denied"), 12 | 13 | 14 | // Member 15 | EMAIL_DUPLICATION(400, "M001", "Email is Duplication"), 16 | LOGIN_INPUT_INVALID(400, "M002", "Login input is invalid"), 17 | 18 | // Coupon 19 | COUPON_ALREADY_USE(400, "CO001", "Coupon was already used"), 20 | COUPON_EXPIRE(400, "CO002", "Coupon was already expired"); 21 | private final String code; 22 | private final String message; 23 | private int status; 24 | 25 | ErrorCode(final int status, final String code, final String message) { 26 | this.status = status; 27 | this.message = message; 28 | this.code = code; 29 | } 30 | 31 | public String getMessage() { 32 | return this.message; 33 | } 34 | 35 | public String getCode() { 36 | return code; 37 | } 38 | 39 | public int getStatus() { 40 | return status; 41 | } 42 | 43 | 44 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.validation.BindingResult; 10 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 11 | 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class ErrorResponse { 15 | 16 | private String message; 17 | private int status; 18 | private List errors; 19 | private String code; 20 | 21 | 22 | private ErrorResponse(final ErrorCode code, final List errors) { 23 | this.message = code.getMessage(); 24 | this.status = code.getStatus(); 25 | this.errors = errors; 26 | this.code = code.getCode(); 27 | } 28 | 29 | private ErrorResponse(final ErrorCode code) { 30 | this.message = code.getMessage(); 31 | this.status = code.getStatus(); 32 | this.code = code.getCode(); 33 | this.errors = new ArrayList<>(); 34 | } 35 | 36 | 37 | public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) { 38 | return new ErrorResponse(code, FieldError.of(bindingResult)); 39 | } 40 | 41 | public static ErrorResponse of(final ErrorCode code) { 42 | return new ErrorResponse(code); 43 | } 44 | 45 | public static ErrorResponse of(final ErrorCode code, final List errors) { 46 | return new ErrorResponse(code, errors); 47 | } 48 | 49 | public static ErrorResponse of(MethodArgumentTypeMismatchException e) { 50 | final String value = e.getValue() == null ? "" : e.getValue().toString(); 51 | final List errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode()); 52 | return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors); 53 | } 54 | 55 | 56 | @Getter 57 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 58 | public static class FieldError { 59 | private String field; 60 | private String value; 61 | private String reason; 62 | 63 | private FieldError(final String field, final String value, final String reason) { 64 | this.field = field; 65 | this.value = value; 66 | this.reason = reason; 67 | } 68 | 69 | public static List of(final String field, final String value, final String reason) { 70 | List fieldErrors = new ArrayList<>(); 71 | fieldErrors.add(new FieldError(field, value, reason)); 72 | return fieldErrors; 73 | } 74 | 75 | private static List of(final BindingResult bindingResult) { 76 | final List fieldErrors = bindingResult.getFieldErrors(); 77 | return fieldErrors.stream() 78 | .map(error -> new FieldError( 79 | error.getField(), 80 | error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), 81 | error.getDefaultMessage())) 82 | .collect(Collectors.toList()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import com.code.design.exception.BusinessException; 4 | import java.nio.file.AccessDeniedException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.validation.BindException; 9 | import org.springframework.web.HttpRequestMethodNotSupportedException; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ControllerAdvice; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 14 | 15 | @ControllerAdvice 16 | @Slf4j 17 | public class GlobalExceptionHandler { 18 | 19 | /** 20 | * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. 21 | * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생 22 | * 주로 @RequestBody, @RequestPart 어노테이션에서 발생 23 | */ 24 | @ExceptionHandler(MethodArgumentNotValidException.class) 25 | protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { 26 | log.error("handleMethodArgumentNotValidException", e); 27 | final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult()); 28 | return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); 29 | } 30 | 31 | /** 32 | * @ModelAttribut 으로 binding error 발생시 BindException 발생한다. 33 | * ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args 34 | */ 35 | @ExceptionHandler(BindException.class) 36 | protected ResponseEntity handleBindException(BindException e) { 37 | log.error("handleBindException", e); 38 | final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult()); 39 | return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); 40 | } 41 | 42 | /** 43 | * enum type 일치하지 않아 binding 못할 경우 발생 44 | * 주로 @RequestParam enum으로 binding 못했을 경우 발생 45 | */ 46 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 47 | protected ResponseEntity handleMethodArgumentTypeMismatchException( 48 | MethodArgumentTypeMismatchException e) { 49 | log.error("handleMethodArgumentTypeMismatchException", e); 50 | final ErrorResponse response = ErrorResponse.of(e); 51 | return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); 52 | } 53 | 54 | /** 55 | * 지원하지 않은 HTTP method 호출 할 경우 발생 56 | */ 57 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 58 | protected ResponseEntity handleHttpRequestMethodNotSupportedException( 59 | HttpRequestMethodNotSupportedException e) { 60 | log.error("handleHttpRequestMethodNotSupportedException", e); 61 | final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED); 62 | return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED); 63 | } 64 | 65 | /** 66 | * Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생합 67 | */ 68 | @ExceptionHandler(AccessDeniedException.class) 69 | protected ResponseEntity handleAccessDeniedException(AccessDeniedException e) { 70 | log.error("handleAccessDeniedException", e); 71 | final ErrorResponse response = ErrorResponse.of(ErrorCode.HANDLE_ACCESS_DENIED); 72 | return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.HANDLE_ACCESS_DENIED.getStatus())); 73 | } 74 | 75 | @ExceptionHandler(BusinessException.class) 76 | protected ResponseEntity handleBusinessException(final BusinessException e) { 77 | log.error("handleEntityNotFoundException", e); 78 | final ErrorCode errorCode = e.getErrorCode(); 79 | final ErrorResponse response = ErrorResponse.of(errorCode); 80 | return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus())); 81 | } 82 | 83 | 84 | @ExceptionHandler(Exception.class) 85 | protected ResponseEntity handleException(Exception e) { 86 | log.error("handleEntityNotFoundException", e); 87 | final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR); 88 | return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.code.design.exception; 2 | 3 | import com.code.design.ErrorCode; 4 | 5 | public class BusinessException extends RuntimeException { 6 | 7 | private ErrorCode errorCode; 8 | 9 | public BusinessException(String message, ErrorCode errorCode) { 10 | super(message); 11 | this.errorCode = errorCode; 12 | } 13 | 14 | public BusinessException(ErrorCode errorCode) { 15 | super(errorCode.getMessage()); 16 | this.errorCode = errorCode; 17 | } 18 | 19 | public ErrorCode getErrorCode() { 20 | return errorCode; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/exception/EntityNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.code.design.exception; 2 | 3 | import com.code.design.ErrorCode; 4 | 5 | public class EntityNotFoundException extends BusinessException { 6 | 7 | public EntityNotFoundException(String message) { 8 | super(message, ErrorCode.ENTITY_NOT_FOUND); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/member/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | import javax.validation.constraints.Email; 10 | import lombok.AccessLevel; 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | import lombok.ToString; 15 | 16 | @Entity 17 | @Table(name = "member") 18 | @Getter 19 | @ToString 20 | public class Member { 21 | 22 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private long id; 24 | 25 | @Email 26 | @Column(name = "email", nullable = false, updatable = false, unique = true) 27 | private String email; 28 | 29 | @Builder 30 | public Member(String email) { 31 | this.email = email; 32 | } 33 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/member/MemberApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import javax.validation.Valid; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/members") 12 | @RequiredArgsConstructor 13 | public class MemberApi { 14 | 15 | private final MemberRepository memberRepository; 16 | 17 | @PostMapping 18 | public Member create(@RequestBody @Valid final SignUpRequest dto) { 19 | return memberRepository.save(Member.builder() 20 | .email(dto.getEmail()) 21 | .build()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/member/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface MemberRepository extends JpaRepository { 6 | 7 | boolean existsByEmail(String email); 8 | 9 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/member/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import com.code.design.validation.EmailUnique; 4 | import javax.validation.constraints.Email; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class SignUpRequest { 12 | 13 | @EmailUnique 14 | @Email 15 | private String email; 16 | 17 | public SignUpRequest(String email) { 18 | this.email = email; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/order/OrderApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import javax.validation.Valid; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/orders") 11 | public class OrderApi { 12 | 13 | @PostMapping 14 | public OrderSheetRequest order(@RequestBody @Valid final OrderSheetRequest dto) { 15 | return dto; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/order/OrderSheetForm.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | import javax.validation.Constraint; 9 | import javax.validation.Payload; 10 | 11 | @Documented 12 | @Constraint(validatedBy = OrderSheetFormValidator.class) 13 | @Target({ElementType.TYPE}) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface OrderSheetForm { 16 | 17 | String message() default "Order sheet form is invalid"; 18 | 19 | Class[] groups() default {}; 20 | 21 | Class[] payload() default {}; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/order/OrderSheetFormValidator.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import com.code.design.order.OrderSheetRequest.Account; 4 | import com.code.design.order.OrderSheetRequest.Card; 5 | import javax.validation.ConstraintValidator; 6 | import javax.validation.ConstraintValidatorContext; 7 | import org.springframework.util.ObjectUtils; 8 | 9 | public class OrderSheetFormValidator implements ConstraintValidator { 10 | 11 | @Override 12 | public void initialize(OrderSheetForm constraintAnnotation) { 13 | 14 | } 15 | 16 | @Override 17 | public boolean isValid(OrderSheetRequest value, ConstraintValidatorContext context) { 18 | int invalidCount = 0; 19 | 20 | if (value.getPayment().getAccount() == null && value.getPayment().getCard() == null) { 21 | addConstraintViolation(context, "카드 정보 혹은 계좌정보는 필수입니다.", "payment"); 22 | invalidCount += 1; 23 | } 24 | 25 | if (value.getPayment().getPaymentMethod() == PaymentMethod.CARD) { 26 | final Card card = value.getPayment().getCard(); 27 | 28 | if (card == null) { 29 | addConstraintViolation(context, "카드 필수입니다.", "payment", "card"); 30 | invalidCount += 1; 31 | } else { 32 | if (ObjectUtils.isEmpty(card.getBrand())) { 33 | addConstraintViolation(context, "카드 브렌드는 필수입니다.", "payment", "card", "brand"); 34 | invalidCount += 1; 35 | } 36 | if (ObjectUtils.isEmpty(card.getCsv())) { 37 | addConstraintViolation(context, "CSV 값은 필수 입니다.", "payment", "card", "csv"); 38 | invalidCount += 1; 39 | } 40 | if (ObjectUtils.isEmpty(card.getNumber())) { 41 | addConstraintViolation(context, "카드 번호는 필수 입니다.", "payment", "card", "number"); 42 | invalidCount += 1; 43 | } 44 | } 45 | } 46 | 47 | if (value.getPayment().getPaymentMethod() == PaymentMethod.BANK_TRANSFER) { 48 | final Account account = value.getPayment().getAccount(); 49 | 50 | if (account == null) { 51 | addConstraintViolation(context, "계좌정보는 필수입니다.", "payment", "account"); 52 | invalidCount += 1; 53 | } else { 54 | if (ObjectUtils.isEmpty(account.getBankCode())) { 55 | addConstraintViolation(context, "은행코드는 필수입니다.", "payment", "account", "bankCode"); 56 | invalidCount += 1; 57 | } 58 | if (ObjectUtils.isEmpty(account.getHolder())) { 59 | addConstraintViolation(context, "계좌주는 값은 필수 입니다.", "payment", "account", "holder"); 60 | invalidCount += 1; 61 | } 62 | if (ObjectUtils.isEmpty(account.getNumber())) { 63 | addConstraintViolation(context, "계좌번호는 필수값입니다.", "payment", "account", "number"); 64 | invalidCount += 1; 65 | } 66 | } 67 | } 68 | 69 | return invalidCount == 0; 70 | } 71 | 72 | private void addConstraintViolation( 73 | final ConstraintValidatorContext context, 74 | final String errorMessage, 75 | final String firstNode, 76 | final String secondNode, 77 | final String thirdNode 78 | ) { 79 | context.disableDefaultConstraintViolation(); 80 | context.buildConstraintViolationWithTemplate(errorMessage) 81 | .addPropertyNode(firstNode) 82 | .addPropertyNode(secondNode) 83 | .addPropertyNode(thirdNode) 84 | .addConstraintViolation(); 85 | } 86 | 87 | private void addConstraintViolation( 88 | final ConstraintValidatorContext context, 89 | final String errorMessage, 90 | final String firstNode 91 | ) { 92 | context.disableDefaultConstraintViolation(); 93 | context.buildConstraintViolationWithTemplate(errorMessage) 94 | .addPropertyNode(firstNode) 95 | .addConstraintViolation(); 96 | } 97 | 98 | private void addConstraintViolation( 99 | final ConstraintValidatorContext context, 100 | final String errorMessage, 101 | final String firstNode, 102 | final String secondNode 103 | ) { 104 | context.disableDefaultConstraintViolation(); 105 | context.buildConstraintViolationWithTemplate(errorMessage) 106 | .addPropertyNode(firstNode) 107 | .addPropertyNode(secondNode) 108 | .addConstraintViolation(); 109 | } 110 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/order/OrderSheetRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import java.math.BigDecimal; 5 | import javax.validation.Valid; 6 | import javax.validation.constraints.Min; 7 | import javax.validation.constraints.NotNull; 8 | import lombok.AccessLevel; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 14 | @Getter 15 | @OrderSheetForm 16 | public class OrderSheetRequest { 17 | 18 | @Min(1) 19 | private BigDecimal price; 20 | 21 | @NotNull 22 | @Valid 23 | private Payment payment; 24 | 25 | @NotNull 26 | @Valid 27 | private Address address; 28 | 29 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 30 | @Getter 31 | @ToString 32 | public static class Payment { 33 | 34 | @NotNull 35 | private PaymentMethod paymentMethod; 36 | private Account account; 37 | private Card card; 38 | 39 | 40 | } 41 | 42 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 43 | @Getter 44 | @ToString 45 | public static class Address { 46 | 47 | private String city; 48 | private String state; 49 | private String zipCode; 50 | } 51 | 52 | 53 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 54 | @Getter 55 | @ToString 56 | public static class Account { 57 | 58 | private String number; 59 | private String bankCode; 60 | private String holder; 61 | } 62 | 63 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 64 | @Getter 65 | @ToString 66 | public static class Card { 67 | 68 | private String number; 69 | private String brand; 70 | private String csv; 71 | } 72 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/order/PaymentMethod.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | enum PaymentMethod { 9 | 10 | CARD("카드"), 11 | BANK_TRANSFER("무통장 입금"); 12 | 13 | private final String description; 14 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/validation/EmailDuplicationValidator.java: -------------------------------------------------------------------------------- 1 | package com.code.design.validation; 2 | 3 | import com.code.design.member.MemberRepository; 4 | import java.text.MessageFormat; 5 | import javax.validation.ConstraintValidator; 6 | import javax.validation.ConstraintValidatorContext; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @RequiredArgsConstructor 12 | public class EmailDuplicationValidator implements ConstraintValidator { 13 | 14 | private final MemberRepository memberRepository; 15 | 16 | @Override 17 | public void initialize(EmailUnique emailUnique) { 18 | 19 | } 20 | 21 | @Override 22 | public boolean isValid(String email, ConstraintValidatorContext cxt) { 23 | 24 | boolean isExistEmail = memberRepository.existsByEmail(email); 25 | 26 | if (isExistEmail) { 27 | cxt.disableDefaultConstraintViolation(); 28 | cxt.buildConstraintViolationWithTemplate( 29 | MessageFormat.format("Email {0} already exists!", email)) 30 | .addConstraintViolation(); 31 | } 32 | return !isExistEmail; 33 | } 34 | } -------------------------------------------------------------------------------- /ch5/src/main/java/com/code/design/validation/EmailUnique.java: -------------------------------------------------------------------------------- 1 | package com.code.design.validation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | import javax.validation.Constraint; 9 | import javax.validation.Payload; 10 | 11 | @Documented 12 | @Constraint(validatedBy = EmailDuplicationValidator.class) 13 | @Target({ElementType.METHOD, ElementType.FIELD}) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface EmailUnique { 16 | 17 | String message() default "Email is Duplication"; 18 | 19 | Class[] groups() default {}; 20 | 21 | Class[] payload() default {}; 22 | } 23 | -------------------------------------------------------------------------------- /ch5/src/test/java/com/code/design/member/MemberApiTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.ResultActions; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | 18 | @SpringBootTest 19 | @AutoConfigureMockMvc 20 | @Transactional 21 | public class MemberApiTest { 22 | 23 | @Autowired 24 | private MockMvc mockMvc; 25 | 26 | @Autowired 27 | private ObjectMapper objectMapper; 28 | 29 | @Autowired 30 | private MemberRepository memberRepository; 31 | 32 | @Test 33 | public void signUp_test_이메일이_중복_아닌_경우() throws Exception { 34 | //given 35 | final SignUpRequest dto = new SignUpRequest("asd@asd.com"); 36 | 37 | //when 38 | final ResultActions resultActions = requestSignUp(dto); 39 | 40 | //then 41 | resultActions 42 | .andExpect(status().isOk()); 43 | } 44 | 45 | @Test 46 | public void signUp_test_이메일이_형식_아닌_경우() throws Exception { 47 | //given 48 | final SignUpRequest dto = new SignUpRequest("asdasd.com"); 49 | 50 | //when 51 | final ResultActions resultActions = requestSignUp(dto); 52 | 53 | //then 54 | resultActions 55 | .andExpect(status().isBadRequest()); 56 | } 57 | 58 | @Test 59 | public void signUp_test_이메일이_중복된_경우() throws Exception { 60 | //given 61 | final String email = "yun@test.com"; 62 | memberRepository.save(new Member(email)); 63 | final SignUpRequest dto = new SignUpRequest(email); 64 | 65 | //when 66 | final ResultActions resultActions = requestSignUp(dto); 67 | 68 | //then 69 | resultActions 70 | .andExpect(status().isBadRequest()); 71 | } 72 | 73 | private ResultActions requestSignUp(SignUpRequest dto) throws Exception { 74 | return mockMvc.perform(post("/members") 75 | .contentType(MediaType.APPLICATION_JSON) 76 | .content(objectMapper.writeValueAsString(dto))) 77 | .andDo(print()); 78 | } 79 | } -------------------------------------------------------------------------------- /ch5/src/test/java/com/code/design/order/OrderApiTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestInstance; 10 | import org.junit.jupiter.api.TestInstance.Lifecycle; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.ResultActions; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | import org.springframework.transaction.annotation.Transactional; 19 | import org.springframework.web.context.WebApplicationContext; 20 | import org.springframework.web.filter.CharacterEncodingFilter; 21 | 22 | @SpringBootTest 23 | @AutoConfigureMockMvc 24 | @Transactional 25 | @TestInstance(Lifecycle.PER_CLASS) 26 | class OrderApiTest { 27 | 28 | private MockMvc mockMvc; 29 | 30 | @Autowired 31 | private WebApplicationContext webApplicationContext; 32 | 33 | @BeforeAll 34 | public void setup() { 35 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) 36 | .addFilters(new CharacterEncodingFilter("UTF-8", true)) 37 | .alwaysDo(print()) 38 | .build(); 39 | } 40 | 41 | @Test 42 | public void 무통장_결제_주문() throws Exception { 43 | final String requestBody = "{\n" 44 | + " \"price\": 100.00,\n" 45 | + " \"payment\": {\n" 46 | + " \"paymentMethod\": \"BANK_TRANSFER\",\n" 47 | + " \"account\": {\n" 48 | + " \"number\": \"110-202034-2234\",\n" 49 | + " \"bankCode\": \"2003\",\n" 50 | + " \"holder\" : \"홍길동\"\n" 51 | + " }\n" 52 | + " },\n" 53 | + " \"address\": {\n" 54 | + " \"city\": \"NOWON-GU\",\n" 55 | + " \"state\": \"SEOUL\",\n" 56 | + " \"zipCode\": \"09876\"\n" 57 | + " }\n" 58 | + "}"; 59 | 60 | requestOrder(requestBody) 61 | .andExpect(status().isOk()); 62 | } 63 | 64 | @Test 65 | public void 카드_결제_주문() throws Exception { 66 | final String requestBody = "" 67 | + "{\n" 68 | + " \"price\": 100.00,\n" 69 | + " \"payment\": {\n" 70 | + " \"paymentMethod\": \"CARD\",\n" 71 | + " \"card\": {\n" 72 | + " \"number\": \"25523-22394\",\n" 73 | + " \"brand\": \"삼성카드\",\n" 74 | + " \"csv\" : \"322\"\n" 75 | + " }\n" 76 | + " },\n" 77 | + " \"address\": {\n" 78 | + " \"city\": \"NOWON-GU\",\n" 79 | + " \"state\": \"SEOUL\",\n" 80 | + " \"zipCode\": \"09876\"\n" 81 | + " }\n" 82 | + "}"; 83 | 84 | requestOrder(requestBody) 85 | .andExpect(status().isOk()); 86 | } 87 | 88 | private ResultActions requestOrder(final String requestBody) throws Exception { 89 | return mockMvc.perform( 90 | post("/orders") 91 | .contentType(MediaType.APPLICATION_JSON) 92 | .content(requestBody) 93 | ); 94 | } 95 | } -------------------------------------------------------------------------------- /ch6/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 3 | 4 | 5 | # Ch6. 자신의 책임과 의도가 명확한 객체 디자인 6 | 7 | 1. 적절한 객체의 크기를 찾아가는 여정 8 | 2. 객체는 협력 관계를 유지해야 한다 9 | 3. 묻지 말고 시켜라! -------------------------------------------------------------------------------- /ch6/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch6/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch6/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch6/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch6/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch6" 2 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/Ch6Application.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Ch6Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Ch6Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/ByAuthChangePasswordService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | @AllArgsConstructor 8 | public class ByAuthChangePasswordService implements ChangePasswordService { 9 | 10 | private final MemberFindService memberFindService; 11 | 12 | @Override 13 | public void change(Long id, PasswordChangeRequest dto) { 14 | 15 | if (dto.getAuthCode().equals("인증 코드가 적합한지 로직 추가...")) { 16 | final Member member = memberFindService.findById(id); 17 | final String newPassword = dto.getNewPassword(); 18 | member.changePassword(newPassword); 19 | // 로직 추가... 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/ByPasswordChangePasswordService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | @AllArgsConstructor 8 | public class ByPasswordChangePasswordService implements ChangePasswordService { 9 | 10 | private MemberFindService memberFindService; 11 | 12 | @Override 13 | public void change(final Long id, PasswordChangeRequest dto) { 14 | if (dto.getPassword().equals("비밀번호가 일치하는지 판단 로직...")) { 15 | final Member member = memberFindService.findById(id); 16 | final String newPassword = dto.getNewPassword(); 17 | member.changePassword(newPassword); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/CardPaymentService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | public interface CardPaymentService { 4 | void pay(); 5 | } 6 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/ChangePasswordService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | public interface ChangePasswordService { 4 | public void change(Long id, PasswordChangeRequest dto); 5 | } 6 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | 12 | @Entity 13 | @Table(name = "member") 14 | @Getter 15 | @ToString 16 | public class Member { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private long id; 21 | 22 | @Column(name = "email", nullable = false, updatable = false, unique = true) 23 | private String email; 24 | 25 | @Column(name = "name", nullable = false) 26 | private String name; 27 | 28 | @Column(name = "password", nullable = false) 29 | private String password; 30 | 31 | public Member(String email, String name, String password) { 32 | this.email = email; 33 | this.name = name; 34 | this.password = password; 35 | } 36 | 37 | public void changePassword(String password){ 38 | this.password = password; 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/MemberFindService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | @Service 8 | @Transactional(readOnly = true) 9 | @RequiredArgsConstructor 10 | public class MemberFindService { 11 | 12 | private final MemberRepository memberRepository; 13 | 14 | public Member findById(final Long id) { 15 | final Member member = memberRepository.findById(id) 16 | .orElseThrow(() -> new IllegalArgumentException("id: " + id + " not found")); 17 | return member; 18 | } 19 | 20 | public Member findByEmail(final String email) { 21 | final Member member = memberRepository.findByEmail(email); 22 | if (member == null) { 23 | throw new IllegalArgumentException("email: " + email + " not found"); 24 | } 25 | return member; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface MemberRepository extends JpaRepository { 6 | 7 | public Member findByEmail(String email); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/MemberService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | public interface MemberService { 4 | 5 | Member findById(Long id); 6 | 7 | Member findByEmail(String email); 8 | 9 | Member create(Member member); 10 | 11 | void changePassword(PasswordChangeRequest dto); 12 | 13 | Member updateName(Long id, String name); 14 | 15 | } -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/MemberServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | @Service 7 | @Transactional 8 | public class MemberServiceImpl implements MemberService { 9 | 10 | @Override 11 | public Member findById(Long id) { 12 | // 로직 구현... 13 | return null; 14 | } 15 | 16 | @Override 17 | public Member findByEmail(String email) { 18 | // 로직 구현... 19 | return null; 20 | } 21 | 22 | @Override 23 | public Member create(Member member) { 24 | // 로직 구현... 25 | return null; 26 | } 27 | 28 | @Override 29 | public void changePassword(PasswordChangeRequest dto) { 30 | // 로직 구현... 31 | } 32 | 33 | @Override 34 | public Member updateName(Long id, String name) { 35 | // 로직 구현... 36 | return null; 37 | } 38 | } -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/PasswordChangeRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | public class PasswordChangeRequest { 10 | 11 | private String authCode; 12 | private String password; 13 | private String newPassword; 14 | } 15 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part1/ShinhanCardPaymentService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part1; 2 | 3 | public class ShinhanCardPaymentService implements CardPaymentService { 4 | 5 | @Override 6 | public void pay() { 7 | // 결제를 위한 비즈니스 로직 실행.... 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part2/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | public enum MessageType { 4 | EMAIL, SMS, KAKAO; 5 | } -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part2/Order.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class Order { 7 | 8 | private OrderMessage message; 9 | 10 | public Order(OrderMessage orderMessage) { 11 | this.message = orderMessage; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part2/OrderApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | import java.util.Set; 4 | import javax.validation.Valid; 5 | import javax.validation.constraints.NotNull; 6 | import lombok.Getter; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/orders") 14 | public class OrderApi { 15 | 16 | 17 | @PostMapping 18 | public Order create(@RequestBody @Valid OrderRequest request) { 19 | final Order order = new Order(OrderMessage.of(request.getMessageType())); 20 | 21 | return order; 22 | } 23 | 24 | @Getter 25 | public static class OrderRequest { 26 | 27 | @NotNull 28 | private Set messageType; 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part2/OrderLegacy.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | public class OrderLegacy { 4 | 5 | private long id; 6 | // KAKAO, SMS, EMAIL 등 메세지 플랫폼등이 있음 7 | private String messageTypes; 8 | 9 | public OrderLegacy(String messageTypes) { 10 | this.messageTypes = messageTypes; 11 | } 12 | 13 | public String[] getMessageTypes() { 14 | return messageTypes.split(","); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part2/OrderMessage.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import org.springframework.util.ObjectUtils; 9 | 10 | public class OrderMessage { 11 | 12 | private String type; 13 | 14 | private OrderMessage(String type) { 15 | this.type = ObjectUtils.isEmpty(type) ? null : type; 16 | } 17 | 18 | public static OrderMessage of(Set types) { 19 | return new OrderMessage(joining(types)); 20 | } 21 | 22 | public List getTypes() { 23 | if (ObjectUtils.isEmpty(type)) { 24 | return new ArrayList<>(); 25 | } 26 | 27 | return new ArrayList<>(doSplit()); 28 | } 29 | 30 | private static String joining(Set types) { 31 | return types.stream() 32 | .map(Enum::name) 33 | .collect(Collectors.joining(",")); 34 | } 35 | 36 | private Set doSplit() { 37 | final String[] split = this.type.split(","); 38 | return Arrays.stream(split) 39 | .map(MessageType::valueOf) 40 | .collect(Collectors.toSet()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part3/Coupon.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part3; 2 | 3 | 4 | import java.time.LocalDate; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class Coupon { 14 | 15 | private long id; 16 | 17 | private boolean used; 18 | 19 | private double amount; 20 | 21 | private LocalDate expirationDate; 22 | 23 | public Coupon(double amount, LocalDate expirationDate) { 24 | this.amount = amount; 25 | this.expirationDate = expirationDate; 26 | this.used = false; 27 | } 28 | 29 | public void apply() { 30 | verifyCouponIsAvailable(); 31 | this.used = true; 32 | } 33 | 34 | private void verifyCouponIsAvailable() { 35 | verifyExpiration(); 36 | verifyUsed(); 37 | } 38 | 39 | private boolean isExpiration() { 40 | return LocalDate.now().isAfter(expirationDate); 41 | } 42 | 43 | private void verifyExpiration() { 44 | if (isExpiration()) { 45 | throw new IllegalArgumentException("만료된 쿠폰입니다."); 46 | } 47 | } 48 | 49 | private void verifyUsed() { 50 | if (this.used) { 51 | throw new IllegalArgumentException("이미 사용한 쿠폰입니다."); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part3/CouponLegacy.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part3; 2 | 3 | 4 | import java.time.LocalDate; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class CouponLegacy { 14 | 15 | private long id; 16 | 17 | private boolean used; 18 | 19 | private double amount; 20 | 21 | private LocalDate expirationDate; 22 | 23 | public CouponLegacy(double amount, LocalDate expirationDate) { 24 | this.amount = amount; 25 | this.expirationDate = expirationDate; 26 | this.used = false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part3/FirstOrderCoupon.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part3; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class FirstOrderCoupon { 6 | 7 | /** 8 | * 좋은 패턴 9 | * 10 | * 묻지 말고 시켜라. 쿠폰 객체의 apply() 메서드를 통해서 묻지 말고 쿠폰을 적용하고 있습니다. 11 | */ 12 | public void apply(final long couponId) { 13 | if (canIssued()) { 14 | final Coupon coupon = getCoupon(couponId); 15 | coupon.apply(); 16 | } 17 | } 18 | 19 | // 실제는 데이터베이스 조회.. 20 | private Coupon getCoupon(final Long id) { 21 | return new Coupon(1000, LocalDate.now().plusDays(3)); 22 | } 23 | 24 | private boolean canIssued() { 25 | // TODO: 첫 구매인지 확인 하는 로직 ... 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ch6/src/main/java/com/code/design/part3/FirstOrderCouponLegacy.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part3; 2 | 3 | import java.time.LocalDate; 4 | 5 | /** 6 | * 7 | */ 8 | public class FirstOrderCouponLegacy { 9 | 10 | /** 11 | * 안티 패턴 12 | * 13 | * 꼬치꼬치 캐묻고 있습니다. 14 | * 15 | * 1. 개체간의 협력관계에서는 상대 객체에 대한 정보를 꼬치꼬치 묻지 않아합니다. 묻지말고 시켜라 -> 16 | */ 17 | public void apply(final long couponId) { 18 | 19 | if (canIssued()) { 20 | final CouponLegacy coupon = getCoupon(couponId); 21 | 22 | if (LocalDate.now().isAfter(coupon.getExpirationDate())) { 23 | throw new IllegalStateException("사용 기간이 만료된 쿠폰입니다."); 24 | } 25 | 26 | if (coupon.isUsed()) { 27 | throw new IllegalStateException("이미 사용한 쿠폰입니다."); 28 | } 29 | } 30 | } 31 | 32 | // 실제는 데이터베이스 조회.. 33 | private CouponLegacy getCoupon(final Long id) { 34 | return new CouponLegacy(1000, LocalDate.now().plusDays(3)); 35 | } 36 | 37 | private boolean canIssued() { 38 | // TODO: 첫 구매인지 확인 하는 로직 ... 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ch6/src/test/java/com/code/design/part2/OrderLegacyTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | 9 | class OrderLegacyTest { 10 | 11 | // Order의 getMessageTypes 메서드를 사용 할 때 불편하다 12 | // 안좋은 캡슐화 13 | @Test 14 | public void anti_message_test_01() { 15 | final OrderLegacy orderLegacy = new OrderLegacy("KAKAO,EMAIL,SMS"); 16 | final String[] split = orderLegacy.getMessageTypes(); 17 | 18 | then(split).contains("KAKAO", "EMAIL", "SMS"); 19 | } 20 | 21 | @Test 22 | public void KAKAO를_KAOKO_라고_잘못_입력했을_경우() { 23 | final OrderLegacy orderLegacy = new OrderLegacy("KAOKO,EMAIL,SMS"); 24 | final String[] split = orderLegacy.getMessageTypes(); 25 | 26 | then(split).doesNotContain("KAKAO"); 27 | then(split).contains("EMAIL", "SMS"); 28 | } 29 | 30 | @Test 31 | public void 메시지에_KAKAO_EMAIL_SMS_처럼_공백이_들어_간다면_실패한다() { 32 | final OrderLegacy orderLegacy = new OrderLegacy("KAKAO, EMAIL, SMS"); 33 | final String[] split = orderLegacy.getMessageTypes(); 34 | 35 | then(split).contains("KAKAO"); 36 | then(split).doesNotContain("EMAIL", "SMS"); 37 | } 38 | 39 | @Test 40 | public void 메시지가_없을_때_빈문자열을_보낼_경우() { 41 | final OrderLegacy orderLegacy = new OrderLegacy(""); 42 | final String[] split = orderLegacy.getMessageTypes(); 43 | 44 | then(split).contains(""); 45 | } 46 | 47 | @Test 48 | public void 메시지가_없을_때_null_보내는_경우() { 49 | final OrderLegacy orderLegacy = new OrderLegacy(null); 50 | thenThrownBy(() -> orderLegacy.getMessageTypes()) 51 | .isInstanceOf(NullPointerException.class); 52 | 53 | } 54 | 55 | @Test 56 | public void 메시지가_중복으로_올경우() { 57 | final OrderLegacy orderLegacy = new OrderLegacy("KAKAO, KAKAO, KAKAO"); 58 | final String[] split = orderLegacy.getMessageTypes(); 59 | 60 | then(split).contains("KAKAO"); 61 | then(split).hasSize(3); 62 | } 63 | } -------------------------------------------------------------------------------- /ch6/src/test/java/com/code/design/part2/OrderMessageTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | 4 | 5 | import static org.assertj.core.api.BDDAssertions.then; 6 | 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class OrderMessageTest { 13 | 14 | @Test 15 | public void 메시지_타입이_EMAIL_KAKAO_SMS_일경우() { 16 | final Set types = new HashSet<>(); 17 | types.add(MessageType.EMAIL); 18 | types.add(MessageType.KAKAO); 19 | types.add(MessageType.SMS); 20 | 21 | final OrderMessage message = OrderMessage.of(types); 22 | 23 | then(message.getTypes()).contains(MessageType.EMAIL, MessageType.KAKAO, MessageType.SMS); 24 | then(message.getTypes()).hasSize(3); 25 | } 26 | 27 | @Test 28 | public void 메시지_타입이_EMAIL_KAKAO일경우() { 29 | final Set types = new HashSet<>(); 30 | types.add(MessageType.EMAIL); 31 | types.add(MessageType.KAKAO); 32 | 33 | final OrderMessage message = OrderMessage.of(types); 34 | 35 | then(message.getTypes()).contains(MessageType.EMAIL,MessageType.KAKAO); 36 | then(message.getTypes()).doesNotContain(MessageType.SMS); 37 | then(message.getTypes()).hasSize(2); 38 | } 39 | 40 | @Test 41 | public void 메시지_타입이_없을경우() { 42 | final Set types = Collections.emptySet(); 43 | final OrderMessage message = OrderMessage.of(types); 44 | 45 | then(message.getTypes()).hasSize(0); 46 | } 47 | 48 | @Test 49 | public void 메시지_타입이_중복되는경우() { 50 | final Set types = new HashSet<>(); 51 | types.add(MessageType.EMAIL); 52 | types.add(MessageType.EMAIL); 53 | types.add(MessageType.EMAIL); 54 | 55 | final OrderMessage message = OrderMessage.of(types); 56 | 57 | then(message.getTypes()).contains(MessageType.EMAIL); 58 | then(message.getTypes()).doesNotContain(MessageType.SMS, MessageType.KAKAO); 59 | then(message.getTypes()).hasSize(1); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /ch6/src/test/java/com/code/design/part2/OrderTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part2; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class OrderTest { 9 | 10 | @Test 11 | public void 객체_디자인() { 12 | final Set types = new HashSet<>(); 13 | types.add(MessageType.EMAIL); 14 | types.add(MessageType.KAKAO); 15 | types.add(MessageType.SMS); 16 | 17 | // 1. Order 객체 생성시 주문에 대한 메시지 플랫폼의 책임을 온전히 OrderMessage에게 이관 했다 18 | final OrderMessage message = OrderMessage.of(types); 19 | final Order order = new Order(message); 20 | 21 | // 2. 실제 데이터베이스에서는 "SMS,KAKAO,EMAL" 이런 평문자열로 저장되어 있지만 22 | // 사용하는 곳에서는 데이터베이스에 평문자열로 저장되었는지 신경쓰지 23 | // 않고 List객체로 안전하게 사용 할수 있다. 24 | final OrderMessage orderMessage = order.getMessage(); 25 | final List messageTypes = orderMessage.getTypes(); 26 | 27 | for (MessageType messageType : messageTypes) { 28 | System.out.println(messageType.name()); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ch6/src/test/java/com/code/design/part3/CouponTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.part3; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import java.time.LocalDate; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class CouponTest { 10 | 11 | @Test 12 | public void 쿠폰생성() { 13 | final double amount = 10D; 14 | final Coupon coupon = buildCoupon(amount, 10); 15 | 16 | then(coupon.isUsed()).isFalse(); 17 | then(coupon.getAmount()).isEqualTo(amount); 18 | then(coupon.isExpiration()).isFalse(); 19 | } 20 | 21 | 22 | @Test 23 | public void 쿠폰할인적용() { 24 | final double amount = 10D; 25 | final Coupon coupon = buildCoupon(amount, 10); 26 | 27 | coupon.apply(); 28 | then(coupon.isUsed()).isTrue(); 29 | } 30 | 31 | @Test 32 | public void 쿠폰할인적용시_이미사용했을경우() { 33 | final double amount = 10D; 34 | final Coupon coupon = buildCoupon(amount, 10); 35 | 36 | // 쿠폰생성시 쿠폰 사용 여부를 생성할 수 없어 apply() 두번 호출 37 | coupon.apply(); 38 | 39 | thenThrownBy(() -> coupon.apply()) 40 | .isInstanceOf(IllegalStateException.class); 41 | 42 | } 43 | 44 | @Test 45 | public void 쿠폰할인적용시_쿠폰기간만료했을경우() { 46 | final double amount = 10D; 47 | final Coupon coupon = buildCoupon(amount, -10); 48 | 49 | // 쿠폰생성시 쿠폰 사용 여부를 생성할 수 없어 apply() 두번 호출 50 | thenThrownBy(() -> coupon.apply()) 51 | .isInstanceOf(IllegalStateException.class); 52 | } 53 | 54 | private Coupon buildCoupon(double amount, int daysToAdd) { 55 | return new Coupon(amount, LocalDate.now().plusDays(daysToAdd)); 56 | } 57 | } -------------------------------------------------------------------------------- /ch7/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 3 | 4 | 5 | -------------------------------------------------------------------------------- /ch7/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | } 6 | 7 | group = "com.code.design" 8 | version = "0.0.1-SNAPSHOT" 9 | 10 | java.sourceCompatibility = JavaVersion.VERSION_1_8 11 | java.targetCompatibility = JavaVersion.VERSION_1_8 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom(configurations.annotationProcessor.get()) 16 | } 17 | } 18 | 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("org.springframework.boot:spring-boot-starter-web") 26 | implementation("org.springframework.boot:spring-boot-starter-actuator") 27 | implementation("org.springframework.boot:spring-boot-starter-validation") 28 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 29 | compileOnly("org.projectlombok:lombok") 30 | runtimeOnly("com.h2database:h2") 31 | annotationProcessor("org.projectlombok:lombok") 32 | testImplementation("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /ch7/client/MemberApi.http: -------------------------------------------------------------------------------- 1 | POST localhost:8080/members 2 | Content-Type: application/json 3 | 4 | { 5 | "name": "test" 6 | } 7 | 8 | ### 9 | 10 | GET localhost:8080/members 11 | Content-Type: application/json 12 | 13 | ### -------------------------------------------------------------------------------- /ch7/client/OrderApi.http: -------------------------------------------------------------------------------- 1 | POST localhost:8080/orders 2 | Content-Type: application/json 3 | 4 | { 5 | "productAmount": 100, 6 | "productId": 1, 7 | "orderer": { 8 | "memberId": 1, 9 | "email": "yun@asd.com" 10 | } 11 | } 12 | 13 | ### 14 | 15 | GET localhost:8080/carts 16 | Content-Type: application/json 17 | 18 | ### 19 | 20 | GET localhost:8080/orders 21 | Content-Type: application/json 22 | 23 | ### -------------------------------------------------------------------------------- /ch7/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch7/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch7/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch7/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch7" 2 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/AppRunner.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import com.code.design.cart.Cart; 4 | import com.code.design.cart.CartRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.boot.ApplicationArguments; 7 | import org.springframework.boot.ApplicationRunner; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @RequiredArgsConstructor 12 | public class AppRunner implements ApplicationRunner { 13 | 14 | private final CartRepository cartRepository; 15 | 16 | @Override 17 | public void run(ApplicationArguments args) { 18 | cartRepository.save(new Cart(1L)); 19 | cartRepository.save(new Cart(2L)); 20 | cartRepository.save(new Cart(3L)); 21 | cartRepository.save(new Cart(4L)); 22 | cartRepository.save(new Cart(5L)); 23 | cartRepository.save(new Cart(6L)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/Ch7Application.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | 7 | @SpringBootApplication 8 | @EnableAsync 9 | public class Ch7Application { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(Ch7Application.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/EmailSenderService.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import com.code.design.member.Member; 4 | import com.code.design.order.Order; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class EmailSenderService { 9 | 10 | /** 11 | * 외부 인프라 서비스를 호출한다고 가정한다 12 | */ 13 | public void sendOrderEmail(Order order) { 14 | System.out.println("주문자 이메일: " + order.getOrderer().getEmail()); 15 | System.out.println("주문 가격: " + order.getProductAmount()); 16 | } 17 | 18 | /** 19 | * 외부 인프라 서비스를 호출한다고 가정한다 20 | */ 21 | public void sendSignUpEmail(Member member) { 22 | throw new RuntimeException("회원 가입 이메일 전송 실패"); 23 | // System.out.println(member.getName() + " 님 회원가입을 축하드립니다."); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/cart/Cart.java: -------------------------------------------------------------------------------- 1 | package com.code.design.cart; 2 | 3 | 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import lombok.AccessLevel; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.ToString; 14 | 15 | @Entity 16 | @Table(name = "cart") 17 | @Getter 18 | @ToString 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | public class Cart { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | @Column(name = "product_id", nullable = false) 27 | private Long productId; 28 | 29 | public Cart(Long productId) { 30 | this.productId = productId; 31 | } 32 | } -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/cart/CartApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.cart; 2 | 3 | import java.util.List; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/carts") 11 | @RequiredArgsConstructor 12 | public class CartApi { 13 | 14 | private final CartRepository cartRepository; 15 | 16 | @GetMapping 17 | public List getCarts() { 18 | return cartRepository.findAll(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/cart/CartRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.cart; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface CartRepository extends JpaRepository { 6 | void deleteByProductId(Long productId); 7 | } 8 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/cart/CartService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.cart; 2 | 3 | import com.code.design.order.Order; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.springframework.transaction.support.TransactionSynchronizationManager; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class CartService { 12 | private final CartRepository cartRepository; 13 | 14 | @Transactional 15 | public void deleteCart(Order order) { 16 | System.out.println("CurrentTransactionName: " + TransactionSynchronizationManager.getCurrentTransactionName()); 17 | cartRepository.deleteByProductId(order.getProductId()); 18 | try { 19 | Thread.sleep(2000); 20 | } catch (InterruptedException e) { 21 | e.printStackTrace(); 22 | } 23 | // throw new RuntimeException("runtime exception ...."); // 예외 발생 24 | } 25 | } -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/coupon/Coupon.java: -------------------------------------------------------------------------------- 1 | package com.code.design.coupon; 2 | 3 | 4 | import java.math.BigDecimal; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import lombok.Getter; 12 | 13 | @Entity 14 | @Table(name = "coupon") 15 | @Getter 16 | public class Coupon { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | 22 | @Column(name = "amount", nullable = false) 23 | private BigDecimal amount; 24 | 25 | @Column(name = "member_id", nullable = false, updatable = false) 26 | private Long memberId; 27 | 28 | public Coupon(BigDecimal amount, Long memberId) { 29 | this.amount = amount; 30 | this.memberId = memberId; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/coupon/CouponIssueService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.coupon; 2 | 3 | import java.math.BigDecimal; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | @Service 9 | @RequiredArgsConstructor 10 | public class CouponIssueService { 11 | 12 | private final CouponRepository couponRepository; 13 | 14 | @Transactional 15 | public void issueSignUpCoupon(Long memberId) { 16 | couponRepository.save(new Coupon(BigDecimal.TEN, memberId)); 17 | // throw new RuntimeException("RuntimeException...."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/coupon/CouponRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.coupon; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | interface CouponRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import lombok.AccessLevel; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Entity 15 | @Table(name = "member") 16 | @Getter 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | public class Member { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @Column(name = "name", nullable = false) 25 | private String name; 26 | 27 | public Member(String name) { 28 | this.name = name; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import java.util.List; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/members") 13 | @RequiredArgsConstructor 14 | public class MemberApi { 15 | 16 | private final MemberSignUpService memberSignUpService; 17 | private final MemberRepository memberRepository; 18 | 19 | @GetMapping 20 | public List getMembers(){ 21 | return memberRepository.findAll(); 22 | } 23 | 24 | @PostMapping 25 | public void signUp(@RequestBody MemberSignUpRequest dto) { 26 | memberSignUpService.signUp(dto); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import com.code.design.EmailSenderService; 4 | import com.code.design.coupon.CouponIssueService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.context.event.EventListener; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.transaction.event.TransactionalEventListener; 9 | 10 | @Component 11 | @RequiredArgsConstructor 12 | public class MemberEventHandler { 13 | 14 | private final EmailSenderService emailSenderService; 15 | 16 | // @EventListener 17 | @TransactionalEventListener 18 | public void memberSignedUpEventListener(MemberSignedUpEvent dto){ 19 | emailSenderService.sendSignUpEmail(dto.getMember()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | interface MemberRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberSignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class MemberSignUpRequest { 7 | private String name; 8 | 9 | public Member toEntity() { 10 | return new Member(name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberSignUpService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import com.code.design.coupon.CouponIssueService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.ApplicationEventPublisher; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class MemberSignUpService { 12 | 13 | private final MemberRepository memberRepository; 14 | private final CouponIssueService couponIssueService; 15 | // private final EmailSenderService emailSenderService; 16 | private final ApplicationEventPublisher eventPublisher; 17 | 18 | @Transactional 19 | public void signUp(final MemberSignUpRequest dto) { 20 | final Member member = memberRepository.save(dto.toEntity()); // 1. member 엔티티 영속화 21 | // emailSenderService.sendSignUpEmail(member); // 2. 외부 시스템 이메일 호출 22 | eventPublisher.publishEvent(new MemberSignedUpEvent(member)); 23 | couponIssueService.issueSignUpCoupon(member.getId()); // 3. 회원가입 쿠폰 발급 -> 예외 발생, 회원, 쿠폰 모두 롤백, 문제는 회원 가입 이메일 전송 완료... 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/member/MemberSignedUpEvent.java: -------------------------------------------------------------------------------- 1 | package com.code.design.member; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public class MemberSignedUpEvent { 9 | 10 | private final Member member; 11 | } 12 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/Order.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import java.math.BigDecimal; 4 | import javax.persistence.Column; 5 | import javax.persistence.Embedded; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import lombok.AccessLevel; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | 15 | @Entity 16 | @Table(name = "orders") 17 | @Getter 18 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 19 | public class Order { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | 25 | @Column(name = "product_id", nullable = false) 26 | private Long productId; 27 | 28 | @Column(name = "product_amount", nullable = false) 29 | private BigDecimal productAmount; 30 | 31 | @Embedded 32 | private Orderer orderer; 33 | 34 | public Order(Long productId, BigDecimal productAmount, Orderer orderer) { 35 | this.productId = productId; 36 | this.productAmount = productAmount; 37 | this.orderer = orderer; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import java.util.List; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/orders") 13 | @RequiredArgsConstructor 14 | public class OrderApi { 15 | 16 | private final OrderService orderService; 17 | private final OrderRepository orderRepository; 18 | 19 | @PostMapping 20 | public void doOrder(@RequestBody OrderRequest dto) { 21 | orderService.doOrder(dto); 22 | } 23 | 24 | @GetMapping 25 | public List getOrders(){ 26 | return orderRepository.findAll(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public class OrderCompletedEvent { 9 | 10 | private final Order order; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import com.code.design.cart.CartService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.event.EventListener; 6 | import org.springframework.scheduling.annotation.Async; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @RequiredArgsConstructor 11 | public class OrderEventHandler { 12 | 13 | private final CartService cartService; 14 | 15 | @Async 16 | @EventListener 17 | public void orderCompletedEventListener(OrderCompletedEvent event) { 18 | cartService.deleteCart(event.getOrder()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface OrderRepository extends JpaRepository { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import java.math.BigDecimal; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class OrderRequest { 8 | 9 | private BigDecimal productAmount; 10 | private Long productId; 11 | private Orderer orderer; 12 | 13 | public Order toEntity() { 14 | return new Order(productId, productAmount, orderer); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/OrderService.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import com.code.design.cart.CartService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.ApplicationEventPublisher; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.springframework.transaction.support.TransactionSynchronizationManager; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class OrderService { 13 | 14 | private final OrderRepository orderRepository; 15 | private final ApplicationEventPublisher eventPublisher; 16 | // private final CartService cartService; 17 | 18 | @Transactional 19 | public void doOrder(OrderRequest dto) { 20 | System.out.println("CurrentTransactionName: " 21 | + TransactionSynchronizationManager.getCurrentTransactionName()); // 현재 트랜잭션 정보 출력 22 | final Order order = orderRepository.save(dto.toEntity()); // 1. order 엔티티 영속화 23 | // cartService.deleteCart(order); // 2. 주문상품 장바구니 제거, 2초 대기, 예외 발생 -> cart, order rollback 진행.. 24 | 25 | eventPublisher.publishEvent(new OrderCompletedEvent(order)); 26 | } 27 | } -------------------------------------------------------------------------------- /ch7/src/main/java/com/code/design/order/Orderer.java: -------------------------------------------------------------------------------- 1 | package com.code.design.order; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import lombok.Getter; 6 | 7 | @Embeddable 8 | @Getter 9 | public class Orderer { 10 | 11 | @Column(name = "member_id", nullable = false, updatable = false) 12 | private Long memberId; 13 | 14 | @Column(name = "email", nullable = false, updatable = false) 15 | private String email; 16 | 17 | } -------------------------------------------------------------------------------- /ch7/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true -------------------------------------------------------------------------------- /ch8/README.md: -------------------------------------------------------------------------------- 1 | # 패스트 캠퍼스 유지보수하기 좋은 코드 디자인 2 | 3 | [패스트 캠퍼스 유지보수하기 좋은 코드 디자인](https://fastcampus.co.kr/dev_online_spring) 예제 코드 4 | 5 | 1. Junit5 특징 6 | 2. Junit5 기초 사용방법 7 | 3. Junit5 AssertJ 사용 방법 8 | 4. 스프링 테스팅 - 스프링의 다양한 종류 9 | 5. 스프링 Web Support - Rest API Test Code 작성 10 | 6. 테스트 커버리지 - Pull Request & Test build 11 | 7. 테스트 커버리지 - Test Coverage 특정 -------------------------------------------------------------------------------- /ch8/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "2.5.2" 3 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 4 | id("java") 5 | id("jacoco") 6 | } 7 | 8 | group = "com.code.design" 9 | version = "0.0.1-SNAPSHOT" 10 | 11 | java.sourceCompatibility = JavaVersion.VERSION_1_8 12 | java.targetCompatibility = JavaVersion.VERSION_1_8 13 | 14 | configurations { 15 | compileOnly { 16 | extendsFrom(configurations.annotationProcessor.get()) 17 | } 18 | } 19 | 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | implementation("org.springframework.boot:spring-boot-starter-web") 27 | implementation("org.springframework.boot:spring-boot-starter-actuator") 28 | implementation("org.springframework.boot:spring-boot-starter-validation") 29 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 30 | compileOnly("org.projectlombok:lombok") 31 | runtimeOnly("com.h2database:h2") 32 | annotationProcessor("org.projectlombok:lombok") 33 | testImplementation("org.springframework.boot:spring-boot-starter-test") 34 | } 35 | 36 | tasks.test { 37 | useJUnitPlatform() 38 | finalizedBy(tasks.jacocoTestReport) 39 | } 40 | 41 | tasks.jacocoTestReport { 42 | reports { 43 | xml.required.set(true) 44 | csv.required.set(false) 45 | html.required.set(true) 46 | html.outputLocation.set(file("$buildDir/reports/jacoco")) 47 | } 48 | } 49 | 50 | tasks.jacocoTestCoverageVerification { 51 | violationRules { 52 | rule { 53 | limit { 54 | counter = "INSTRUCTION" 55 | value = "COVEREDRATIO" 56 | minimum = "0.74".toBigDecimal() 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /ch8/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheese10yun/code-design/d66142d4d303d9be633cca96b86d9de6c02b9a06/ch8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ch8/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ch8/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ch8/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ch8" 2 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/Ch8Application.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Ch8Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Ch8Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/Coupon.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | 4 | import java.time.LocalDate; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class Coupon { 14 | 15 | private long id; 16 | 17 | private boolean used; 18 | 19 | private double amount; 20 | 21 | private LocalDate expirationDate; 22 | 23 | public Coupon(double amount, LocalDate expirationDate) { 24 | this.amount = amount; 25 | this.expirationDate = expirationDate; 26 | this.used = false; 27 | } 28 | 29 | public boolean isExpiration() { 30 | return LocalDate.now().isAfter(expirationDate); 31 | } 32 | 33 | public void apply() { 34 | verifyCouponIsAvailable(); 35 | this.used = true; 36 | } 37 | 38 | private void verifyCouponIsAvailable() { 39 | verifyExpiration(); 40 | verifyUsed(); 41 | } 42 | 43 | private void verifyUsed() { 44 | if (used) { 45 | throw new IllegalStateException("이미 사용한 쿠폰입니다."); 46 | } 47 | } 48 | 49 | private void verifyExpiration() { 50 | if (LocalDate.now().isAfter(getExpirationDate())) { 51 | throw new IllegalStateException("사용 기간이 만료된 쿠폰입니다."); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/Member.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import org.springframework.util.Assert; 13 | 14 | @Entity 15 | @Table(name = "member") 16 | @Getter 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | public class Member { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @Column(name = "name", nullable = false) 25 | private String name; 26 | 27 | public Member(String name) { 28 | Assert.hasText(name, "name must not be empty"); 29 | this.name = name; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "Member{" + 35 | "id=" + id + 36 | ", name='" + name + '\'' + 37 | '}'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/MemberApi.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/members") 11 | @RequiredArgsConstructor 12 | public class MemberApi { 13 | private final MemberRepository memberRepository; 14 | 15 | @PostMapping 16 | public void create(@RequestBody SignUpRequest dto) { 17 | memberRepository.save(dto.toEntity()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/MemberFindService.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | @Service 8 | @Transactional(readOnly = true) 9 | @RequiredArgsConstructor 10 | public class MemberFindService { 11 | private final MemberRepository memberRepository; 12 | 13 | public Member findById(Long id) { 14 | return memberRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("not found")); 15 | } 16 | 17 | public Member findByName(String name) { 18 | return memberRepository.findByName(name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface MemberRepository extends JpaRepository { 6 | 7 | Member findByName(String name); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ch8/src/main/java/com/code/design/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.code.design; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 10 | public class SignUpRequest { 11 | 12 | @NotEmpty 13 | private String name; 14 | 15 | // 테스트 코드에서 밖에 사용하지 않는 코드... 16 | // public SignUpRequest(String name) { 17 | // this.name = name; 18 | // } 19 | 20 | public Member toEntity() { 21 | return new Member(name); 22 | } 23 | } -------------------------------------------------------------------------------- /ch8/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_1/Junit5_1.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_1; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.TestInstance; 5 | import org.junit.jupiter.api.TestInstance.Lifecycle; 6 | 7 | @TestInstance(Lifecycle.PER_CLASS) 8 | public class Junit5_1 { 9 | 10 | private int count = 0; 11 | 12 | // 테스트 코드가 실행될떄 마다, 매번 새롭게 인스턴스가 만들어진다. 13 | // 테스트 코드간의 디펜던시를 줄이기 위함. 14 | 15 | @Test 16 | void count_add_1() { 17 | count = count + 1; 18 | System.out.println("count: " + count); 19 | System.out.println("Junit5_1: " + this); 20 | } 21 | 22 | @Test 23 | void count_add_2() { 24 | count = count + 1; 25 | System.out.println("count: " + count); 26 | System.out.println("Junit5_1: " + this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_1/Junit5_2.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_1; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.AfterEach; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.TestInstance; 9 | 10 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 11 | public class Junit5_2 { 12 | 13 | /** 14 | * @BeforeAll 테스트 실행되기 전 한번 실행됨 15 | * @BeforeEach 모든 테스트 마다 실행되기 전에 실행됨 16 | * @AfterEach 모든 테스트 마다 실행된후 전에 실행됨 17 | * @AfterAll 테스트 실행된 후 한 번 실행됨 18 | */ 19 | 20 | @BeforeAll 21 | void beforeAll() { 22 | System.out.println("BeforeAll : 테스트 실행되기 이전 단 한 번만 실행"); 23 | } 24 | 25 | @AfterAll 26 | void afterAll() { 27 | System.out.println("AfterAll : 테스트 실행되기 이전 단 한 번만 실행"); 28 | } 29 | 30 | @BeforeEach 31 | void beforeEach() { 32 | System.out.println("BeforeEach: 모든 테스트 마다 실행되기 이전 실행 "); 33 | } 34 | 35 | @AfterEach 36 | void afterEach() { 37 | System.out.println("AfterEach : 모든 테스트 마다 실행 이후 실행"); 38 | } 39 | 40 | @Test 41 | void test_1() { 42 | System.out.println("test_1"); 43 | } 44 | 45 | @Test 46 | void test_2() { 47 | System.out.println("test_2"); 48 | } 49 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_1/Junit5_3.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_1; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.Order; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestInstance; 7 | import org.junit.jupiter.api.TestInstance.Lifecycle; 8 | import org.junit.jupiter.api.TestMethodOrder; 9 | 10 | @TestInstance(Lifecycle.PER_CLASS) 11 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 12 | public class Junit5_3 { 13 | 14 | @Test 15 | @Order(2) 16 | void test_1() { 17 | System.out.println("test_1"); 18 | } 19 | 20 | @Test 21 | @Order(1) 22 | void test_2() { 23 | System.out.println("test_2"); 24 | } 25 | 26 | @Test 27 | @Order(99) 28 | void test_3() { 29 | System.out.println("test_3"); 30 | } 31 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_2/Junit5.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_2; 2 | 3 | 4 | import static org.assertj.core.api.BDDAssertions.then; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.CsvSource; 11 | import org.junit.jupiter.params.provider.EnumSource; 12 | import org.junit.jupiter.params.provider.MethodSource; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | public class Junit5 { 16 | 17 | @ParameterizedTest 18 | @ValueSource(strings = {"1", "2"}) 19 | void valueSource_1(String value) { 20 | System.out.println(value); 21 | } 22 | 23 | @ParameterizedTest 24 | @ValueSource(booleans = {true, false, false}) 25 | void valueSource_2(boolean value) { 26 | System.out.println(value); 27 | } 28 | 29 | @ParameterizedTest 30 | @EnumSource(Quarter.class) 31 | void enumSource_1(Quarter quarter) { 32 | then(quarter.getValue()).isIn(1, 2, 3, 4); 33 | } 34 | 35 | @ParameterizedTest 36 | @EnumSource(value = Quarter.class, names = {"Q1", "Q2"}) 37 | void enumSource_2(Quarter quarter) { 38 | then(quarter.getValue()).isIn(1, 2); 39 | then(quarter.getValue()).isNotIn(3, 4); 40 | } 41 | 42 | @ParameterizedTest 43 | @CsvSource( 44 | value = { 45 | "010-1234-1234,01012341234", 46 | "010-2333-2333,01023332333", 47 | "02-223-1232,022231232" 48 | } 49 | ) 50 | void csvSource(String value, String expected) { 51 | final String number = value.replace("-", ""); 52 | then(number).isEqualTo(expected); 53 | } 54 | 55 | @ParameterizedTest 56 | @MethodSource("providerOrder") 57 | void methodSource(Order order, int expectedTotalPrice) { 58 | then(order.getTotalPrice()).isEqualTo(expectedTotalPrice); 59 | } 60 | 61 | static List providerOrder() { 62 | final List arguments = new ArrayList<>(); 63 | 64 | arguments.add(Arguments.of(new Order(100, 2), 200)); 65 | arguments.add(Arguments.of(new Order(100, 3), 300)); 66 | 67 | return arguments; 68 | } 69 | } 70 | 71 | enum Quarter { 72 | Q1(1, "1분기"), 73 | Q2(2, "2분기"), 74 | Q3(3, "3분기"), 75 | Q4(4, "4분기"); 76 | 77 | private final int value; 78 | private final String description; 79 | 80 | Quarter(int value, String description) { 81 | this.value = value; 82 | this.description = description; 83 | } 84 | 85 | public int getValue() { 86 | return value; 87 | } 88 | 89 | public String getDescription() { 90 | return description; 91 | } 92 | } 93 | 94 | class Order { 95 | 96 | private int price; 97 | private int quantity; 98 | 99 | public Order(int price, int quantity) { 100 | this.price = price; 101 | this.quantity = quantity; 102 | } 103 | 104 | public int getTotalPrice() { 105 | return price * quantity; 106 | } 107 | 108 | public int getPrice() { 109 | return price; 110 | } 111 | 112 | public int getQuantity() { 113 | return quantity; 114 | } 115 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_2/SpringBoot.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_2; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | 5 | import com.code.design.Member; 6 | import com.code.design.MemberRepository; 7 | import java.util.List; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import org.springframework.test.context.TestConstructor; 12 | import org.springframework.test.context.jdbc.Sql; 13 | 14 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 15 | @ActiveProfiles("test") 16 | @DataJpaTest 17 | public class SpringBoot { 18 | 19 | private final MemberRepository memberRepository; 20 | 21 | public SpringBoot(MemberRepository memberRepository) { 22 | this.memberRepository = memberRepository; 23 | } 24 | 25 | @Test 26 | public void member_test() { 27 | final Member member = memberRepository.save(new Member("name")); 28 | } 29 | 30 | @Test 31 | @Sql("/member-set-up.sql") 32 | public void sql_test() { 33 | final List members = memberRepository.findAll(); 34 | then(members).hasSize(7); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_3/Test_1.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_3; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.Java6Assertions.assertThat; 5 | 6 | import com.code.design.Member; 7 | import com.code.design.MemberRepository; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import org.springframework.test.context.TestConstructor; 12 | 13 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 14 | @ActiveProfiles("test") 15 | @DataJpaTest 16 | public class Test_1 { 17 | 18 | private final MemberRepository memberRepository; 19 | 20 | public Test_1(MemberRepository memberRepository) { 21 | this.memberRepository = memberRepository; 22 | } 23 | 24 | @Test 25 | public void member_save_test() { 26 | //given 27 | final String name = "name"; 28 | 29 | //when 30 | final Member member = memberRepository.save(new Member(name)); 31 | 32 | //then 33 | // 기존 사용법 assertThat 34 | assertThat(member.getName()).isEqualTo(name); 35 | 36 | // BDD 사용법 37 | then(member.getName()).isEqualTo(name); 38 | } 39 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_3/Test_2.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_3; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | import static org.assertj.core.api.Java6Assertions.assertThat; 6 | 7 | import com.code.design.Member; 8 | import java.math.BigDecimal; 9 | import java.time.LocalDate; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class Test_2 { 15 | 16 | @Test 17 | public void 기존_matcher_불편한점() { 18 | // org.hamcrest.MatcherAssert.assertThat("aa", org.hamcrest.Matchers.is("aa")); 19 | 20 | } 21 | 22 | @Test 23 | public void 문장_검사() { 24 | final String title = "AssertJ is best matcher"; 25 | then(title) 26 | .isNotNull() 27 | .startsWith("AssertJ") 28 | .contains(" ") 29 | .endsWith("matcher") 30 | ; 31 | } 32 | 33 | @Test 34 | public void 다양한_기능_제공() { 35 | then(BigDecimal.ZERO).isEqualByComparingTo(BigDecimal.valueOf(0)); 36 | then(" ").isBlank(); 37 | then("").isEmpty(); 38 | then("YWJjZGVmZw==").isBase64(); 39 | then("AA").isIn("AA", "BB", "CC"); 40 | 41 | final LocalDate targetDate = LocalDate.of(2020, 5, 5); 42 | then(targetDate).isBetween(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 12, 12)); 43 | } 44 | 45 | @Test 46 | public void collection_검증() { 47 | final List members = new ArrayList<>(); 48 | members.add(new Member("Kim")); 49 | members.add(new Member("Joo")); 50 | members.add(new Member("Jin")); 51 | 52 | then(members).hasSize(3); 53 | then(members).allSatisfy(member -> { 54 | System.out.println(member); 55 | then(member.getName()).isIn("Kim", "Joo", "Jin"); 56 | then(member.getId()).isNull(); 57 | } 58 | ); 59 | } 60 | 61 | @Test 62 | public void thenThrownBy_사용법() { 63 | thenThrownBy(() -> new Member("")) 64 | .isInstanceOf(IllegalArgumentException.class); 65 | } 66 | 67 | @Test 68 | public void BDD_Style() { 69 | assertThat(10).isEqualTo(10); 70 | assertThat(true).isTrue(); 71 | 72 | then(10).isEqualTo(10); 73 | then(true).isTrue(); 74 | } 75 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/CouponTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.assertj.core.api.BDDAssertions.thenThrownBy; 5 | 6 | import com.code.design.Coupon; 7 | import java.time.LocalDate; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class CouponTest { 11 | 12 | @Test 13 | public void 쿠폰생성() { 14 | final double amount = 10D; 15 | final Coupon coupon = buildCoupon(amount, 10); 16 | 17 | then(coupon.isUsed()).isFalse(); 18 | then(coupon.getAmount()).isEqualTo(amount); 19 | then(coupon.isExpiration()).isFalse(); 20 | } 21 | 22 | 23 | @Test 24 | public void 쿠폰할인적용() { 25 | final double amount = 10D; 26 | final Coupon coupon = buildCoupon(amount, 10); 27 | 28 | coupon.apply(); 29 | then(coupon.isUsed()).isTrue(); 30 | } 31 | 32 | @Test 33 | public void 쿠폰할인적용시_이미사용했을경우() { 34 | final double amount = 10D; 35 | final Coupon coupon = buildCoupon(amount, 10); 36 | 37 | // 쿠폰생성시 쿠폰 사용 여부를 생성할 수 없어 apply() 두번 호출 38 | coupon.apply(); 39 | 40 | thenThrownBy(() -> coupon.apply()) 41 | .isInstanceOf(IllegalStateException.class); 42 | } 43 | 44 | @Test 45 | public void 쿠폰할인적용시_쿠폰기간만료했을경우() { 46 | final double amount = 10D; 47 | final Coupon coupon = buildCoupon(amount, -10); 48 | 49 | // 쿠폰생성시 쿠폰 사용 여부를 생성할 수 없어 apply() 두번 호출 50 | thenThrownBy(() -> coupon.apply()) 51 | .isInstanceOf(IllegalStateException.class); 52 | } 53 | 54 | private Coupon buildCoupon(double amount, int daysToAdd) { 55 | return new Coupon(amount, LocalDate.now().plusDays(daysToAdd)); 56 | } 57 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/IntegrationTestSupport.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.micrometer.core.instrument.util.IOUtils; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.TestInstance; 9 | import org.junit.jupiter.api.TestInstance.Lifecycle; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.core.io.ResourceLoader; 14 | import org.springframework.test.context.ActiveProfiles; 15 | import org.springframework.test.context.TestConstructor; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 18 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 19 | import org.springframework.transaction.annotation.Transactional; 20 | import org.springframework.web.context.WebApplicationContext; 21 | 22 | @SpringBootTest 23 | @AutoConfigureMockMvc 24 | @ActiveProfiles("test") 25 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 26 | @TestInstance(Lifecycle.PER_CLASS) 27 | @Transactional 28 | public class IntegrationTestSupport { 29 | 30 | @Autowired 31 | protected MockMvc mockMvc; 32 | @Autowired 33 | protected ObjectMapper objectMapper; 34 | @Autowired 35 | private ResourceLoader resourceLoader; 36 | 37 | @BeforeEach 38 | void setUp( 39 | final WebApplicationContext context 40 | ) { 41 | this.mockMvc = MockMvcBuilders.webAppContextSetup(context) 42 | .alwaysDo(MockMvcResultHandlers.print()) 43 | .build(); 44 | } 45 | 46 | protected String readJson(final String path) throws IOException { 47 | return IOUtils.toString( 48 | resourceLoader.getResource("classpath:" + path).getInputStream(), 49 | StandardCharsets.UTF_8 50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/MemberApiTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.http.MediaType; 9 | 10 | public class MemberApiTest extends IntegrationTestSupport { 11 | 12 | @Test 13 | void 회원가입_테스트_json_관리의_불편() throws Exception { 14 | this.mockMvc.perform( 15 | post("/members") 16 | .contentType(MediaType.APPLICATION_JSON) 17 | .content("{\n" 18 | + " \"name\": \"yun\"\n" 19 | + "}") 20 | ) 21 | .andExpect(status().isOk()); 22 | } 23 | 24 | @Test 25 | void 회원가입_테스트_객체기반으로_생성해야하는_단점_테스트_코드에서_밖에_사용_하지_않은_코드() throws Exception { 26 | // final String requestBody = objectMapper.writeValueAsString(new SignUpRequest("yun")); 27 | // this.mockMvc.perform( 28 | // post("/members") 29 | // .contentType(MediaType.APPLICATION_JSON) 30 | // .content(requestBody) 31 | // ) 32 | // .andExpect(status().isOk()); 33 | } 34 | 35 | @Test 36 | public void 회원가입_테스트() throws Exception { 37 | final String requestBody = readJson("/member-signup.json"); 38 | this.mockMvc.perform( 39 | post("/members") 40 | .contentType(MediaType.APPLICATION_JSON) 41 | .content(requestBody) 42 | ) 43 | .andExpect(status().isOk()); 44 | } 45 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/MemberFindServiceMockTestSupport.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | import static org.mockito.BDDMockito.given; 5 | 6 | import com.code.design.Member; 7 | import com.code.design.MemberFindService; 8 | import com.code.design.MemberRepository; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | 13 | class MemberFindServiceMockTestSupport extends MockTestSupport { 14 | 15 | // 외부 인프라 16 | @InjectMocks 17 | private MemberFindService memberFindService; 18 | 19 | @Mock 20 | private MemberRepository memberRepository; 21 | 22 | @Test 23 | void mock_test() { 24 | //given 25 | final Member member = new Member("yun"); 26 | 27 | given(memberRepository.findByName("yun")).willReturn(member); 28 | 29 | //when 30 | final Member findMember = memberFindService.findByName("yun"); 31 | 32 | //then 33 | then(findMember.getName()).isEqualTo("yun"); 34 | } 35 | } -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/MemberFindServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | 5 | import com.code.design.Member; 6 | import com.code.design.MemberFindService; 7 | import com.code.design.MemberRepository; 8 | import java.util.List; 9 | import org.junit.jupiter.api.AfterAll; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class MemberFindServiceTest extends IntegrationTestSupport { 13 | 14 | private final MemberRepository memberRepository; 15 | private final MemberFindService memberFindService; 16 | 17 | public MemberFindServiceTest(MemberRepository memberRepository, MemberFindService memberFindService) { 18 | this.memberRepository = memberRepository; 19 | this.memberFindService = memberFindService; 20 | } 21 | 22 | @Test 23 | public void findByName_정상조회_케이스() { 24 | //given 25 | memberRepository.save(new Member("yun")); 26 | 27 | //when 28 | final Member member = memberFindService.findByName("yun"); 29 | 30 | //then 31 | then(member.getName()).isEqualTo("yun"); 32 | } 33 | 34 | @AfterAll 35 | void afterAll() { 36 | final List members = memberRepository.findAll(); 37 | System.out.println("============="); 38 | System.out.println("members size: " + members.size()); 39 | System.out.println("============="); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/MockTestSupport.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | import org.mockito.junit.jupiter.MockitoExtension; 5 | 6 | 7 | @ExtendWith(MockitoExtension.class) 8 | public abstract class MockTestSupport { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ch8/src/test/java/com/code/design/test_4/RepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.code.design.test_4; 2 | 3 | import static org.assertj.core.api.BDDAssertions.then; 4 | 5 | import com.code.design.Member; 6 | import com.code.design.MemberRepository; 7 | import java.util.List; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import org.springframework.test.context.TestConstructor; 12 | import org.springframework.test.context.jdbc.Sql; 13 | 14 | @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) 15 | @ActiveProfiles("test") 16 | @DataJpaTest 17 | public class RepositoryTest { 18 | 19 | private final MemberRepository memberRepository; 20 | 21 | public RepositoryTest(MemberRepository memberRepository) { 22 | this.memberRepository = memberRepository; 23 | } 24 | 25 | @Test 26 | public void member_test() { 27 | //given 28 | memberRepository.save(new Member("yun")); 29 | 30 | //when 31 | final Member findMember = memberRepository.findByName("yun"); 32 | 33 | //then 34 | then(findMember.getName()).isEqualTo("yun"); 35 | } 36 | 37 | @Test 38 | @Sql("/member-set-up.sql") 39 | public void sql_test() { 40 | final List members = memberRepository.findAll(); 41 | then(members).hasSize(7); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ch8/src/test/resources/member-set-up.sql: -------------------------------------------------------------------------------- 1 | insert into member (`name`) 2 | values ('sample1'), 3 | ('sample2'), 4 | ('sample3'), 5 | ('sample4'), 6 | ('sample5'), 7 | ('sample6'), 8 | ('sample1') 9 | ; -------------------------------------------------------------------------------- /ch8/src/test/resources/member-signup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yun" 3 | } --------------------------------------------------------------------------------