diff --git "a/8\354\243\274\354\260\250/CQRS.md" "b/8\354\243\274\354\260\250/CQRS.md" new file mode 100644 index 00000000..42d491cf --- /dev/null +++ "b/8\354\243\274\354\260\250/CQRS.md" @@ -0,0 +1,41 @@ +# CQRS +## 1. CQRS란 +CQRS는 Command and Query Responsibility Segregation(명령과 조회의 책임 분리)를 나타낸다. +CQRS는 시스템에서 명령을 처리하는 책임과 조회를 처리하는 책임을 분리하는 것이 핵심이다. +- 명령은 시스템의 상태를 변경하는 작업을 의미 +- 조회는 시스템의 상태를 반환하는 작업을 의미 + +정리하면, **CQRS는 시스템의 상태를 변경하는 작업과 시스템의 상태를 반환하는 작업의 책임을 분리하는 것**이다. + +일반적인 애플리케이션은 쿼리와 업데이트에 동일한 데이터 모델을 사용한다. + +![](https://images.velog.io/images/minide/post/da85b570-1aef-49f5-939e-d51eebd9c39f/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-27%20%EC%98%A4%ED%9B%84%209.58.50.png) + +간단한 애플리케이션에는 이것이 적합하지만 애플리케이션이 복잡해질수록 위 방법은 어려워진다. 왜냐하면 애플리케이션은 읽기 쪽에서 다른 쿼리를 실행할 수 있고, 그러면 모양이 다른 DTO를 반환하기 때문에 개체 매핑이 복잡해지기 때문이다. 또한 동일한 데이터 집합에서 작업을 병렬로 수행할 때 데이터 경합이 발생할 수 있으며, 데이터 저장소 및 데이터 액세스 계층에 대한 로드 및 정보를 검색하는데 필요한 쿼리의 복잡성으로 성능에 부정적인 영향을 미칠 수 있다. 그리고 각 엔터티는 읽기 및 쓰기 작업의 대상이 되므로 잘못된 컨텍스트에서 데이터를 노출할 수 있으므로 보안 및 사용 권한 관리가 복잡해질 수 있다. + +반면 CQRS는 명령 및 쿼리의 책임을 분리하는 패턴으로, CQRS를 구현하면 성능, 확장성 보안을 극대화할 수 있다. + +![](https://images.velog.io/images/minide/post/a1b540ca-786a-43af-a204-98654fb6df0a/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-28%20%EC%98%A4%ED%9B%84%201.33.10.png) + +이 패턴에서 명령은 데이터 중심이 아닌 작업을 기반으로 해야하며, 비동기 처리를 위해 큐에 배치될 수도 있다. 또한 쿼리는 데이터베이스를 수정하지 않으며, 쿼리는 도메인 정보를 캡슐화지 않는 DTO를 반환한다. + +읽기 저장소는 쓰기 저장소의 읽기 전용 복제본이거나 읽기 및 쓰기 저장소가 전혀 다른 구조일 수도 있다. 여러 일긱 전용 복제본을 사용하며 쿼리의 성능이 당연히 향상될 수 있으며, 부하를 감안하여 각 저장소를 적절하게 확장할 수도 있다. + +![](https://images.velog.io/images/minide/post/054032dd-89cf-495b-8d75-7e2aa93e0cf8/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-28%20%EC%98%A4%ED%9B%84%201.44.27.png) + +메모리 내 모델은 동일한 데이터베이스를 공유할 수 있으며, 이 경우 데이터베이스는 두 모델 간의 통신 역할을 한다. 위와 같이 여러 형태로 변형을 하여 CQRS를 적용할 수 있다. 별도의 데이터베이스를 허용하며 쿼리측 데이터베이스를 실시간 리포팅 데이터베이스(Reporting Database)로 만들 수도 있다. 이 경우 두 모델 또는 데이터베이스 간에 통신 매커니즘이 추가되어야 한다. + +## 2. CQRS의 사용 +CQRS를 사용해야 하는 경우는 아래와 같다. + +1. 많은 사용자가 동일한 데이터에 병렬로 액세스하는 공동작업 도메인일 경우 +2. 개발자 중 한 팀은 쓰기 모델에 포함되는 복잡한 도메인 모델에 집중하고 다른 한 팀은 읽기 모델과 사용자 인터페이스에 집중할 수 있는 경우 +3. 시스템이 시간이 지나면서 진화할 것으로 예상되어 여러 버전의 모델을 포함할 수 있거나 비즈니스 규칙이 저기적으로 변하는 경우 +4. 가장 가치있는 시스템의 제한된 구역에 CQRS 적용을 고려해야 한다. + -> 도메인 또는 비즈니스 규칙이 간단하거나 간단한 CRUD 스타일의 사용자 인터페이스와 데이터 액세스 작업만으로 충분하다면 CQRS는 어울리지 않는다. 모든 상황에서 최상단에 패턴으로 CQRS를 위치하는 건 어디에서도 추천하지 않는 방법이다. + -> DDD 용어에서 말하는 시스템 전체가 아닌 시스템의 특정 부분(Bounded Context)에서만 사용해야한다. +5. CQRS는 이벤트 기간 프로그래밍 모델에 적합하다. + -> CQRS 시스템이 이벤트 콜라보레이션(Event Collaboration)과 통신하는 별도의 서비스로 분리되는 것이 일반적이며, 이를 통해 이벤트 소싱(Event Sourcing)을 쉽게 이용할 수 있다. + +> ### 이벤트 소싱(Event Sourcing) +> : 이벤트 전체를 하나의 데이터로 저장하는 방식 \ No newline at end of file diff --git "a/8\354\243\274\354\260\250/racingcar/build.gradle" "b/8\354\243\274\354\260\250/racingcar/build.gradle" new file mode 100644 index 00000000..353aec80 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/build.gradle" @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group 'org.javastudy' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git "a/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.jar" "b/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.jar" new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and "b/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.jar" differ diff --git "a/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.properties" "b/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.properties" new file mode 100644 index 00000000..be52383e --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/gradle/wrapper/gradle-wrapper.properties" @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git "a/8\354\243\274\354\260\250/racingcar/gradlew" "b/8\354\243\274\354\260\250/racingcar/gradlew" new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/gradlew" @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git "a/8\354\243\274\354\260\250/racingcar/gradlew.bat" "b/8\354\243\274\354\260\250/racingcar/gradlew.bat" new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/gradlew.bat" @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git "a/8\354\243\274\354\260\250/racingcar/settings.gradle" "b/8\354\243\274\354\260\250/racingcar/settings.gradle" new file mode 100644 index 00000000..64cf2835 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/settings.gradle" @@ -0,0 +1,2 @@ +rootProject.name = 'javastudy' + diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/Application.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/Application.java" new file mode 100644 index 00000000..c99612fd --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/Application.java" @@ -0,0 +1,8 @@ +import service.Game; + +public class Application { + public static void main(String[] args) { + Game game = new Game(); + game.play(); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Car.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Car.java" new file mode 100644 index 00000000..75f82022 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Car.java" @@ -0,0 +1,43 @@ +package domain; + +public class Car { + private static final String BLANK = " "; + private static final int MAXIMAL_LENGTH = 5; + + private final String name; + private int position = 0; + + public Car(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + if (name.length() > MAXIMAL_LENGTH) { + throw new IllegalArgumentException("[ERROR] 자동차 이름은 5자 이하여야 한다."); + } + if (name.contains(BLANK)) { + throw new IllegalArgumentException("[ERROR] 자동차 이름은 공백은 포함하지 않아야 한다."); + } + } + + public boolean isSamePosition(Car car) { + return car.position == this.position; + } + + public int compareTo(Car car) { + return this.position - car.position; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public void go() { + position++; + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Cars.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Cars.java" new file mode 100644 index 00000000..4f0d3cfc --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Cars.java" @@ -0,0 +1,52 @@ +package domain; + +import java.util.List; +import java.util.stream.Collectors; + +import static view.OutputView.printGameStatus; + +public class Cars { + private final List cars; + + public Cars(List cars) { + validate(cars); + this.cars = cars + .stream() + .map(Car::new) + .collect(Collectors.toList()); + } + + private void validate(List cars) { + boolean checkName = cars.stream() + .distinct() + .count() != cars.size(); + if (checkName) { + throw new IllegalArgumentException("[ERROR] 중복된 이름입니다."); + } + } + + public void moveCars() { + cars.stream() + .filter(car -> Engine.isPower()) + .forEach(Car::go); + } + + public void printCars() { + cars.forEach(car -> + printGameStatus(car.getName(), car.getPosition())); + } + + public Winners findWinner() { + Car maxPositionCar = findMaxPositionCar(); + return new Winners(cars.stream() + .filter(maxPositionCar::isSamePosition) + .map(Winner::new) + .collect(Collectors.toList())); + } + + private Car findMaxPositionCar() { + return cars.stream() + .max(Car::compareTo) + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 입력된 차량이 없습니다.")); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Engine.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Engine.java" new file mode 100644 index 00000000..0eb778ae --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Engine.java" @@ -0,0 +1,17 @@ +package domain; + +import utils.RandomUtils; + +public class Engine { + private static final int START_INCLUSIVE = 0; + private static final int END_INCLUSIVE = 9; + private static final int GO_POINT = 4; + + private Engine() { + + } + + public static boolean isPower() { + return RandomUtils.nextInt(START_INCLUSIVE, END_INCLUSIVE) >= GO_POINT; + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/GameCounter.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/GameCounter.java" new file mode 100644 index 00000000..3cab82d7 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/GameCounter.java" @@ -0,0 +1,28 @@ +package domain; + +public class GameCounter { + private static final int ZERO = 0; + + private int counter; + + public GameCounter(String round) { + validate(round); + this.counter = Integer.parseInt(round); + } + + private void validate(String value) { + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] 시도 횟수는 숫자여야 한다."); + } + if (result <= ZERO) { + throw new IllegalArgumentException("[ERROR] 시도 횟수는 자연수여야 한다."); + } + } + + public int nextRound() { + return counter--; + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winner.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winner.java" new file mode 100644 index 00000000..c4827905 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winner.java" @@ -0,0 +1,13 @@ +package domain; + +public class Winner { + private final Car winner; + + public Winner(Car winner) { + this.winner = winner; + } + + public String getWinnerName() { + return winner.getName(); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winners.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winners.java" new file mode 100644 index 00000000..d65a3127 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/domain/Winners.java" @@ -0,0 +1,21 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Winners { + private static final String DELIMITER = ", "; + + private List winners; + + public Winners(List winners) { + this.winners = new ArrayList<>(winners); + } + + public String winnersToString() { + return winners.stream() + .map(Winner::getWinnerName) + .collect(Collectors.joining(DELIMITER)); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/service/Game.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/service/Game.java" new file mode 100644 index 00000000..cf94ce21 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/service/Game.java" @@ -0,0 +1,26 @@ +package service; + +import domain.Cars; +import domain.GameCounter; +import domain.Winners; +import view.OutputView; + +import static view.InputView.inputCarNames; +import static view.InputView.inputGameCount; + +public class Game { + private static final int FINAL_ROUND = 0; + + public void play() { + Cars cars = new Cars(inputCarNames()); + GameCounter gameCounter = inputGameCount(); + OutputView.printGamePreview(); + while (gameCounter.nextRound() > FINAL_ROUND) { + cars.moveCars(); + cars.printCars(); + OutputView.nextLine(); + } + Winners winners = cars.findWinner(); + OutputView.printGameResult(winners); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/utils/RandomUtils.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/utils/RandomUtils.java" new file mode 100644 index 00000000..2220ecf9 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/utils/RandomUtils.java" @@ -0,0 +1,24 @@ +package utils; + +import java.util.Random; + +public class RandomUtils { + private static final Random RANDOM = new Random(); + private static final int ZERO = 0; + + private RandomUtils() { + + } + + public static int nextInt(final int startInclusive, final int endInclusive) { + if (startInclusive > endInclusive || startInclusive < ZERO) { + throw new IllegalArgumentException("[ERROR] 잘못된 랜덤값 설정"); + } + + if (startInclusive == endInclusive) { + return startInclusive; + } + + return startInclusive + RANDOM.nextInt(endInclusive - startInclusive + 1); + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/view/InputView.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/view/InputView.java" new file mode 100644 index 00000000..77524f80 --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/view/InputView.java" @@ -0,0 +1,52 @@ +package view; + +import domain.GameCounter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; + +public class InputView { + private static final BufferedReader BUFFERED_READER = new BufferedReader(new InputStreamReader(System.in)); + private static final String DELIMITER = ","; + + private InputView() { + + } + + public static GameCounter inputGameCount() { + OutputView.printGameCounterInput(); + String inputNumber = nextLine(); + validateGameCount(inputNumber); + return new GameCounter(inputNumber); + } + + private static void validateGameCount(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 숫자를 입력해주세요."); + } + } + + public static List inputCarNames() { + OutputView.printCarInput(); + String input = nextLine(); + validateCarNames(input); + return Arrays.asList(input.split(DELIMITER)); + } + + private static void validateCarNames(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 이름을 입력해 주세요."); + } + } + + private static String nextLine() { + try { + return BUFFERED_READER.readLine(); + } catch (IOException e) { + throw new IllegalArgumentException("[ERROR] 잘못된 입력입니다."); + } + } +} diff --git "a/8\354\243\274\354\260\250/racingcar/src/main/java/view/OutputView.java" "b/8\354\243\274\354\260\250/racingcar/src/main/java/view/OutputView.java" new file mode 100644 index 00000000..cdfd0d5d --- /dev/null +++ "b/8\354\243\274\354\260\250/racingcar/src/main/java/view/OutputView.java" @@ -0,0 +1,50 @@ +package view; + +import domain.Winners; + +import java.util.stream.IntStream; + +public class OutputView { + private static final String POSITION_DISPLAY = "-"; + + private OutputView() { + + } + + private static void printCarName(final String carName) { + System.out.print(carName + " : "); + } + + private static void printCarPosition(final int carPosition) { + IntStream.range(0, carPosition) + .mapToObj(s -> POSITION_DISPLAY) + .forEach(System.out::print); + System.out.println(); + } + + public static void printGameStatus(final String carName, final int carPosition) { + printCarName(carName); + printCarPosition(carPosition); + } + + public static void printGameResult(final Winners winners) { + System.out.print("최종 우승자: "); + System.out.println(winners.winnersToString()); + } + + public static void printGameCounterInput() { + System.out.println("시도할 횟수는 몇회인가요? "); + } + + public static void printCarInput() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + } + + public static void printGamePreview() { + System.out.println("\n실행결과"); + } + + public static void nextLine() { + System.out.println(); + } +} diff --git "a/8\354\243\274\354\260\250/\353\224\224\353\257\270\355\204\260 \353\262\225\354\271\231.md" "b/8\354\243\274\354\260\250/\353\224\224\353\257\270\355\204\260 \353\262\225\354\271\231.md" new file mode 100644 index 00000000..6b5a0bbf --- /dev/null +++ "b/8\354\243\274\354\260\250/\353\224\224\353\257\270\355\204\260 \353\262\225\354\271\231.md" @@ -0,0 +1,27 @@ +# 2. 디미터 법칙 +## 2-1. 디미터 법칙이란 +디미터 법칙은 협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 법칙이다. + +디미터 법칙은 객체 간 관계를 설정할 때 객체 간의 결합도를 효과적으로 낮출 수 있는 유용한 지침 중 하나로 꼽히며 객체 지향 생활 체조 원칙 중 **"한 줄에 점을 하나만 찍는다."** 로 요약되기도 한다. + +디미터 법칙의 핵심은 객체 구조의 경로를 따라 멀리 떨어져 있는 낯선 객체에 메시지를 보내는 설계를 피하라는 것이다. +다시 말해, 객체는 내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만 가지고 의사 결정을 내려야 하고 **다른 객체를 탐색해 뭔가를 일어나게 해서는 안된다.** + +이 때문에 디미터 법칙은 **Don't Tallk to Strangers**(낯선 이에게 말하지 마라)라고 불리기도 하고, 한 객체가 알아야 하는 다른 객체를 최소한으로 유지하라는 의미로 **Principle of least knowledge**(최소 지식 원칙)라고도 불린다. + +## 2-2. 디미터 법칙의 적용 +```모든 클래스 C```와 ```C에 구현된 모든 메소드 M```에 대해서, ```M```이 메시지를 전송할 수 있는 모든 객체는 + +1. ```M```의 인자로 전달된 클래스(```C``` 자체를 포함) + -> ```M```에 의해 생성된 객체, 호출하는 메소드에 의해 생성된 객체 모두 +2. ```C```의 인스턴스 변수의 클래스 + +쉽게 말하자면, + +1. ```this``` 객체 +2. 메소드의 매개변수 +3. ```this```의 속성 +4. ```this```의 속성인 컬렉션의 요소 +5. 메소드 내에서 생성된 지역 객체 + +이다. \ No newline at end of file diff --git "a/8\354\243\274\354\260\250/\354\235\274\352\270\211 \354\273\254\353\240\211\354\205\230.md" "b/8\354\243\274\354\260\250/\354\235\274\352\270\211 \354\273\254\353\240\211\354\205\230.md" new file mode 100644 index 00000000..4b4499fe --- /dev/null +++ "b/8\354\243\274\354\260\250/\354\235\274\352\270\211 \354\273\254\353\240\211\354\205\230.md" @@ -0,0 +1,128 @@ +# 3. 일급 컬렉션 +## 3-1. 일급 컬렉션이란 +일급 컬렉션이란, 컬렉션(Collection)을 래핑(Wrapping)하면서, 그 외 다른 멤버 변수가 없는 상태이다. + +래핑을 함으로써 다음과 같은 이점을 가지게 된다. + +1. 비지니스에 종속적인 자료구조 +2. 컬렉션의 불변성을 보장 +3. 상태와 행위를 한 곳에서 관리 +4. 이름이 있는 컬렉션 + +## 3-2. 비지니스에 종속적인 자료구조 +```java +public class LottoService { + + private static final int LOTTO_NUMBERS_SIZE = 6; + + public void createLottoNumber() { + List lottoNumbers = createNonDuplicationNumbers(); + validateSize(lottoNumbers); + validateDuplicate(lottoNumbers); + } +} +``` + +위 코드를 통해 로또 서비스에서 로또번호를 생성할 때마다 필요한 모든 장소에서 검증로직이 들어가야한다. 따라서 불필요한 코드가 중복적으로 실행된다. +이를 해결하기 위해 해당 조건으로만 생성할 수 있는 자료구조를 만들게 되면 해결할 수 있다. + +```java +public class LottoTicket { + private static final int LOTTO_NUMBERS_SIZE = 6; + + private final List lottoNumbers; + + public LottoTicket(List lottoNumbers) { + validateSize(lottoNumbers); + validateDuplicate(lottoNumbers); + this.lottoNumbers = lottoNumbers; + } +} + +public class LottoService { + + public void createLottoNumbers() { + LottoTicket lottoTicket = new LottoTicket(createNonDuplicateNumbers()); + } +} +``` +위처럼 일급 컬렉션을 사용해 비즈니스에 종속적인 자료 구조를 만들어주면 좀 더 깔끔한 코드를 만들 수 있게 된다. + +## 3-3. 불변 +일급 컬렉션은 **컬렉션의 불변**을 보장한다. + +이때 ```final```을 사용하면 안되나 의문점이 생긴다. +```final```은 불변을 만들어주는 것이 아니라, **재할당만 금지**한다. + +즉, ```final```은 ```new```를 통한 재할당은 막아주지만 ```set```을 통한 내부값 변경은 막지 못한다. + +따라서 일급 컬렉션을 사용해 이를 막아준다.(set을 포함하지 않는 일급 컬렉션) + +## 3-4. 상태와 행위를 한 곳에서 관리 +일급 컬렉션의 세 번째 장점은 **값과 로직이 함께 존재**한다는 것이다. +-> 이 부분은 Enum 클래스의 장점과 동일하다. + +```java +List pays = Arrays.asList( + new Pay(NAVER_PAY, 10000), + new Pay(NAVER_PAY, 15000); + new Pay(KAKAO_PAY, 20000); + new Pay(TOSS, 30000L); +} + +Long naverPaySum = pays.stream() + .filter(pay -> pay.getPayType().equals(NAVER_PAY)) + .mapToLong(Pay::getAmount) + .sum(); +``` +이 코드의 경우 List에 데이터를 담고, Service 혹은 Util 클래스에서 필요한 로직을 수행한다. + +이때 컬렉션과 계산 로직은 서로 관계가 있지만 위 코드에서는 표현이 안되어있다. + +Pay타입의 상태에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다. +또한, 위 코드는 똑같은 기능을 하는 메소드를 중복 생성할 수 있을 뿐만 아니라 계산 메소드를 누락할 수 있다. + +결국 이를 해결하기 위해 계산식을 컬렉션과 함께 두 번 나누어야 한다. + +```java +public class PayGroups { + private List pays; + + public PayGroups(List pays) { + this.pays = pays; + } + + public Long getNaverPaySum() { + return getFilteredPays(pay -> PayType.isNaverPay(pay.getPayType())); + } + + public Long getKakaoPaySum() { + return getFilteredPays(pay -> PayType.isKakaoPay(pay.getPayType())); + } +} +``` + +이렇게 PayGroups라는 일급 컬렉션이 생김으로써 **상태와 로직이 한 곳에서 관리**된다. + +## 3-5. 이름이 있는 컬렉션 +마지막으로 일급 컬렉션에는 **컬렉션에 이름을 붙일 수 있다**는 장점이 있다. + +같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다르다. +그렇다면 이 둘을 구분하려면 어떻게 해야할까? +가장 흔한 방법은 변수명을 다르게 하는 것이다. + +```java +List naverPays = createNaverPays(); +List kakaoPays = createKakaoPays(); +``` + +위 코드의 단점은 검색이 어렵고, 명확한 표현이 불가능하다는 것이다. + +이러한 문제 역시 일급 컬렉션으로 쉽게 해결할 수 있다. + +네이버페이 그룹과 카카오페이 그룹 각각의 일급 컬렉션을 만들면 이 컬렉션을 기반으로 용어사용과 검색이 가능하다. + +```java +NaverPays naverPays = new NaverPays(createNaverPays()); +KakaoPays kakaoPays = new KakaoPays(createKakaoPays()); +``` \ No newline at end of file