diff --git a/clean-code/complete/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java b/clean-code/complete/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
index cdba6d8..a2bfa72 100644
--- a/clean-code/complete/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
+++ b/clean-code/complete/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
@@ -1,6 +1,7 @@
package cholog.goodcode;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -23,548 +24,560 @@
* 유지보수성과 확장성을 위한 실수를 방지하는 코드를 작성하는 방법을 알아봅니다.
*/
public class AvoidMistakeCodeTest {
- /**
- * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
- * 자동차와 위치를 포장하여 응집도를 높이고 유지보수성 및 확장성을 고려한 코드입니다.
- * 아래 코드는 원시값을 포장했지만 같은 위치 객체를 사용하며 의도와 다르게 동작하는 코드입니다.
- * 어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?")
- void 어떻게_같은_위치_객체를_사용할_때_발생할_수_있는_실수를_방지할_수_있을까() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ @Nested
+ @DisplayName("가변 객체의 공유 문제와 불변 객체")
+ class ImmutableObjectTest {
+ /**
+ * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
+ * 자동차와 위치를 포장하여 응집도를 높이고 유지보수성 및 확장성을 고려한 코드입니다.
+ * 아래 코드는 원시값을 포장했지만 같은 위치 객체를 사용하며 의도와 다르게 동작하는 코드입니다.
+ * 어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?")
+ void 어떻게_같은_위치_객체를_사용할_때_발생할_수_있는_실수를_방지할_수_있을까() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- record Car(
- String name,
- Position position
- ) {
- public Car forward() {
- return new Car(name, position.increase());
+ record Car(
+ String name,
+ Position position
+ ) {
+ public Car forward() {
+ return new Car(name, position.increase());
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- // Note: Car 객체가 불변 객체가 되면서 위치가 이동될 때 마다 새로운 객체가 생성된다.
- neoCar = neoCar.forward();
+ // Note: Car 객체가 불변 객체가 되면서 위치가 이동될 때 마다 새로운 객체가 생성된다.
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(new Position(1));
- assertThat(brownCar.position()).isEqualTo(new Position(0));
- }
+ assertThat(neoCar.position()).isEqualTo(new Position(1));
+ assertThat(brownCar.position()).isEqualTo(new Position(0));
+ }
- /**
- * 원시값을 불변 객체로 만들어 실수를 방지하는 방법입니다.
- * 불변 객체는 객체의 상태를 변경할 수 없기 때문에 객체의 상태를 변경하는 실수를 방지할 수 있습니다.
- * 불변 객체는 변경이 있을 때 마다 새로운 객체를 생성하기 때문에 성능상의 이슈가 발생할 수 있습니다.
- * 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
- *
- * 참고: 불변 객체
- */
- @Test
- @DisplayName("불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
- void 불변_객체를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
- record Position(int value) {
- private static final Map CACHE = new ConcurrentHashMap<>();
-
- public static Position startingPoint() {
- return valueOf(0);
- }
+ /**
+ * 원시값을 불변 객체로 만들어 실수를 방지하는 방법입니다.
+ * 불변 객체는 객체의 상태를 변경할 수 없기 때문에 객체의 상태를 변경하는 실수를 방지할 수 있습니다.
+ * 불변 객체는 변경이 있을 때 마다 새로운 객체를 생성하기 때문에 성능상의 이슈가 발생할 수 있습니다.
+ * 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
+ *
+ * 참고: 불변 객체
+ */
+ @Test
+ @DisplayName("불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
+ void 불변_객체를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
+ record Position(int value) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
+
+ public static Position startingPoint() {
+ return valueOf(0);
+ }
- public static Position valueOf(final int value) {
- return CACHE.computeIfAbsent(value, Position::new);
- }
+ public static Position valueOf(final int value) {
+ return CACHE.computeIfAbsent(value, Position::new);
+ }
- public Position increase() {
- return valueOf(value + 1);
+ public Position increase() {
+ return valueOf(value + 1);
+ }
}
- }
- record Car(
- String name,
- Position position
- ) {
- private static final Map CACHE = new ConcurrentHashMap<>();
+ record Car(
+ String name,
+ Position position
+ ) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
- public static Car of(final String name, final Position position) {
- return CACHE.computeIfAbsent(toKey(name, position), key -> new Car(key, position));
- }
+ public static Car of(final String name, final Position position) {
+ return CACHE.computeIfAbsent(toKey(name, position), key -> new Car(key, position));
+ }
- private static String toKey(final String name, final Position position) {
- return name + position.value();
- }
+ private static String toKey(final String name, final Position position) {
+ return name + position.value();
+ }
- public Car forward() {
- // Note: 움직일 때 마다 캐싱된 객체가 재활용된다. 하지만 캐싱된 객체가 많을수록 메모리 사용량이 증가한다.
- return Car.of(name, position.increase());
+ public Car forward() {
+ // Note: 움직일 때 마다 캐싱된 객체가 재활용된다. 하지만 캐싱된 객체가 많을수록 메모리 사용량이 증가한다.
+ return Car.of(name, position.increase());
+ }
}
- }
- final var position = Position.startingPoint();
+ final var position = Position.startingPoint();
- var neoCar = Car.of("네오", position);
- var brownCar = Car.of("브라운", position);
+ var neoCar = Car.of("네오", position);
+ var brownCar = Car.of("브라운", position);
- neoCar = neoCar.forward();
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(Position.valueOf(1));
- assertThat(brownCar.position()).isEqualTo(Position.valueOf(0));
+ assertThat(neoCar.position()).isEqualTo(Position.valueOf(1));
+ assertThat(brownCar.position()).isEqualTo(Position.valueOf(0));
+ }
}
- /**
- * 정적 팩터리 메서드를 만들고 내부에서 캐싱하여 성능상의 이슈를 해결하는 방법입니다.
- * 객체를 재활용할 경우 매번 새로운 객체가 생성되지 않기 때문에 성능상의 이슈를 해결할 수 있습니다.
- * 하지만 지금의 방법은 캐싱되는 객체가 많을수록 메모리 사용량이 증가할 수 있습니다.
- * 메모리 사용량을 최소화하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
- void 메모리_사용량을_최소화하는_방법은_무엇일까() {
- record PositionForEnhancedCache(int value) {
- private static final int CACHE_MIN = 0;
- private static final int CACHE_MAX = 5;
- private static final Map CACHE = IntStream.range(CACHE_MIN, CACHE_MAX)
- .boxed()
- .collect(toMap(identity(), PositionForEnhancedCache::new));
-
- public static PositionForEnhancedCache startingPoint() {
- return valueOf(0);
- }
+ @Nested
+ @DisplayName("캐싱 전략과 성능")
+ class CachingStrategyTest {
+ /**
+ * 정적 팩터리 메서드를 만들고 내부에서 캐싱하여 성능상의 이슈를 해결하는 방법입니다.
+ * 객체를 재활용할 경우 매번 새로운 객체가 생성되지 않기 때문에 성능상의 이슈를 해결할 수 있습니다.
+ * 하지만 지금의 방법은 캐싱되는 객체가 많을수록 메모리 사용량이 증가할 수 있습니다.
+ * 메모리 사용량을 최소화하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
+ void 메모리_사용량을_최소화하는_방법은_무엇일까() {
+ record PositionForEnhancedCache(int value) {
+ private static final int CACHE_MIN = 0;
+ private static final int CACHE_MAX = 5;
+ private static final Map CACHE = IntStream.range(CACHE_MIN, CACHE_MAX)
+ .boxed()
+ .collect(toMap(identity(), PositionForEnhancedCache::new));
+
+ public static PositionForEnhancedCache startingPoint() {
+ return valueOf(0);
+ }
- public static PositionForEnhancedCache valueOf(final int value) {
- // Note: 자주 사용되는 객체만 캐싱하여 메모리 사용량을 최소화한다.
- if (CACHE_MIN <= value && value <= CACHE_MAX) {
- return CACHE.get(value);
+ public static PositionForEnhancedCache valueOf(final int value) {
+ // Note: 자주 사용되는 객체만 캐싱하여 메모리 사용량을 최소화한다.
+ if (CACHE_MIN <= value && value <= CACHE_MAX) {
+ return CACHE.get(value);
+ }
+ return new PositionForEnhancedCache(value);
}
- return new PositionForEnhancedCache(value);
- }
- public PositionForEnhancedCache increase() {
- return valueOf(value + 1);
+ public PositionForEnhancedCache increase() {
+ return valueOf(value + 1);
+ }
}
- }
- record CarForEnhancedCache(
- String name,
- PositionForEnhancedCache position
- ) {
- private static final Map CACHE = new ConcurrentHashMap<>();
+ record CarForEnhancedCache(
+ String name,
+ PositionForEnhancedCache position
+ ) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
- public static CarForEnhancedCache of(final String name, final PositionForEnhancedCache position) {
- return CACHE.computeIfAbsent(toKey(name, position), key -> new CarForEnhancedCache(key, position));
- }
+ public static CarForEnhancedCache of(final String name, final PositionForEnhancedCache position) {
+ return CACHE.computeIfAbsent(toKey(name, position), key -> new CarForEnhancedCache(key, position));
+ }
- private static String toKey(final String name, final PositionForEnhancedCache position) {
- return name + position.value();
- }
+ private static String toKey(final String name, final PositionForEnhancedCache position) {
+ return name + position.value();
+ }
- public CarForEnhancedCache forward() {
- return CarForEnhancedCache.of(name, position.increase());
+ public CarForEnhancedCache forward() {
+ return CarForEnhancedCache.of(name, position.increase());
+ }
}
- }
- final var position = PositionForEnhancedCache.startingPoint();
+ final var position = PositionForEnhancedCache.startingPoint();
- var neoCar = CarForEnhancedCache.of("네오", position);
- var brownCar = CarForEnhancedCache.of("브라운", position);
+ var neoCar = CarForEnhancedCache.of("네오", position);
+ var brownCar = CarForEnhancedCache.of("브라운", position);
- neoCar = neoCar.forward();
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(1));
- assertThat(brownCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(0));
- }
+ assertThat(neoCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(1));
+ assertThat(brownCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(0));
+ }
- /**
- * 자주 사용될 객체만 캐싱하는 방법입니다.
- * 자주 사용되는 객체만 캐싱할 경우 메모리 사용량을 최소화할 수 있습니다.
- * 어떤 객체를 캐싱할지 계산하는 것도 비용이 들고, 객체 그래프가 복잡할 경우 캐싱하는 것도 복잡해질 수 있습니다.
- * 또한 JVM의 경우 GC의 성능이 우리가 생각하는 것 보다 훨씬 더 좋기 때문에 캐싱을 하지 않는 것이 더 좋을 수도 있습니다.
- * 객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?")
- void 객체_그래프가_깊을_때_문제를_해결하는_방법은_무엇일까() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 자주 사용될 객체만 캐싱하는 방법입니다.
+ * 자주 사용되는 객체만 캐싱할 경우 메모리 사용량을 최소화할 수 있습니다.
+ * 어떤 객체를 캐싱할지 계산하는 것도 비용이 들고, 객체 그래프가 복잡할 경우 캐싱하는 것도 복잡해질 수 있습니다.
+ * 또한 JVM의 경우 GC의 성능이 우리가 생각하는 것 보다 훨씬 더 좋기 때문에 캐싱을 하지 않는 것이 더 좋을 수도 있습니다.
+ * 객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?")
+ void 객체_그래프가_깊을_때_문제를_해결하는_방법은_무엇일까() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public Position getPosition() {
- return position;
+ public Position getPosition() {
+ return position;
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- final var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ final var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- neoCar.forward();
+ neoCar.forward();
- assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
- assertThat(brownCar.getPosition()).isEqualTo(new Position());
- }
+ assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
+ assertThat(brownCar.getPosition()).isEqualTo(new Position());
+ }
- /**
- * Car 내부에서 Position 객체를 변경하는 방법입니다.
- * 적정한 시점까지만 불변 객체를 사용하면 불변 객체의 장점을 살리면서 성능상의 이슈를 해결할 수 있습니다.
- * 성능적인 이점만 존재하는 것은 아닙니다. 불변 객체의 장점을 살리면서 가변 객체의 장점도 살릴 수 있습니다.
- * 동일한 컨텍스트에서만 불변 객체를 사용하고, 다른 컨텍스트에서 사용될 수 있는 지점에선 가변 객체처럼 사용하게 하는 것이 좋습니다.
- * 하지만 이 기준은 상황과 설계에 따라 달라질 수 있습니다.
- * 구현할 때 가변 객체로도 구현해보고, 불변 객체로도 구현해보면서 어떠한 방법이 더 좋은지 판단해보는 것이 좋습니다.
- */
- @Test
- @DisplayName("적정한 시점까지만 불변 객체를 사용한다.")
- void 적정한_시점까지만_불변_객체를_사용한다() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * Car 내부에서 Position 객체를 변경하는 방법입니다.
+ * 적정한 시점까지만 불변 객체를 사용하면 불변 객체의 장점을 살리면서 성능상의 이슈를 해결할 수 있습니다.
+ * 성능적인 이점만 존재하는 것은 아닙니다. 불변 객체의 장점을 살리면서 가변 객체의 장점도 살릴 수 있습니다.
+ * 동일한 컨텍스트에서만 불변 객체를 사용하고, 다른 컨텍스트에서 사용될 수 있는 지점에선 가변 객체처럼 사용하게 하는 것이 좋습니다.
+ * 하지만 이 기준은 상황과 설계에 따라 달라질 수 있습니다.
+ * 구현할 때 가변 객체로도 구현해보고, 불변 객체로도 구현해보면서 어떠한 방법이 더 좋은지 판단해보는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("적정한 시점까지만 불변 객체를 사용한다.")
+ void 적정한_시점까지만_불변_객체를_사용한다() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public Position getPosition() {
- return position;
+ public Position getPosition() {
+ return position;
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- final var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ final var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- neoCar.forward();
+ neoCar.forward();
- assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
- assertThat(brownCar.getPosition()).isEqualTo(new Position());
+ assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
+ assertThat(brownCar.getPosition()).isEqualTo(new Position());
+ }
}
- /**
- * 아래 코드는 우승한 자동차들을 구하는 코드입니다.
- * 자동차 경주 객체 내부에 자동차 객체들이 존재하고 있고, 외부에서 조작할 수 있는 위험이 존재하고 있습니다.
- * 객체의 상태를 변경할 수 있는 위험은 실수로 인한 버그를 발생시킬 수 있습니다.
- * 객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?")
- void 객체의_상태를_변경할_수_있는_위험을_방지하는_방법은_무엇일까() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ @Nested
+ @DisplayName("방어적 복사와 불변 컬렉션")
+ class DefensiveCopyTest {
+ /**
+ * 아래 코드는 우승한 자동차들을 구하는 코드입니다.
+ * 자동차 경주 객체 내부에 자동차 객체들이 존재하고 있고, 외부에서 조작할 수 있는 위험이 존재하고 있습니다.
+ * 객체의 상태를 변경할 수 있는 위험은 실수로 인한 버그를 발생시킬 수 있습니다.
+ * 객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?")
+ void 객체의_상태를_변경할_수_있는_위험을_방지하는_방법은_무엇일까() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = new ArrayList<>(participants);
- }
+ RacingGame(final List participants) {
+ this.participants = new ArrayList<>(participants);
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- // Note: 매번 새로운 리스트를 생성하여 성능상의 이슈가 발생할 수 있다.
- return new ArrayList<>(participants);
+ List getParticipants() {
+ // Note: 매번 새로운 리스트를 생성하여 성능상의 이슈가 발생할 수 있다.
+ return new ArrayList<>(participants);
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- participants.add(new Car("브리", new Position(2)));
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- }
+ participants.add(new Car("브리", new Position(2)));
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ }
- /**
- * 방어적 복사를 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
- * 하지만 방어적 복사를 사용할 경우 객체의 상태를 변경할 수 있는 위험을 방지할 수 있지만 성능상의 이슈가 발생할 수 있습니다.
- * 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
- void 방어적_복사를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 방어적 복사를 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
+ * 하지만 방어적 복사를 사용할 경우 객체의 상태를 변경할 수 있는 위험을 방지할 수 있지만 성능상의 이슈가 발생할 수 있습니다.
+ * 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
+ void 방어적_복사를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = new ArrayList<>(participants);
- }
+ RacingGame(final List participants) {
+ this.participants = new ArrayList<>(participants);
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- return unmodifiableList(participants);
+ List getParticipants() {
+ return unmodifiableList(participants);
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- participants.add(new Car("브리", new Position(2)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ participants.add(new Car("브리", new Position(2)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
- assertThatThrownBy(() -> {
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- }).isInstanceOf(UnsupportedOperationException.class);
- }
+ // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
+ assertThatThrownBy(() -> {
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ }).isInstanceOf(UnsupportedOperationException.class);
+ }
- /**
- * 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
- * 응답하는 컬렉션을 불변 컬렉션으로 만들어 객체의 상태를 변경할 수 있는 위험을 방지할 수 있습니다.
- * 입력을 받는 컬렉션을 불변 컬렉션을 만드는 것은 그대로 외부에서 조작할 수 있는 위험이 존재합니다.
- * 따라서 입력받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋습니다.
- */
- @Test
- @DisplayName("입력을 받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋다.")
- void 입력을_받는_컬렉션은_방어적_복사로_응답하는_컬렉션은_불변_컬렉션으로_만드는_것이_좋다() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
+ * 응답하는 컬렉션을 불변 컬렉션으로 만들어 객체의 상태를 변경할 수 있는 위험을 방지할 수 있습니다.
+ * 입력을 받는 컬렉션을 불변 컬렉션을 만드는 것은 그대로 외부에서 조작할 수 있는 위험이 존재합니다.
+ * 따라서 입력받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("입력을 받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋다.")
+ void 입력을_받는_컬렉션은_방어적_복사로_응답하는_컬렉션은_불변_컬렉션으로_만드는_것이_좋다() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = new ArrayList<>(participants);
- }
+ RacingGame(final List participants) {
+ this.participants = new ArrayList<>(participants);
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- return unmodifiableList(participants);
+ List getParticipants() {
+ return unmodifiableList(participants);
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- participants.add(new Car("브리", new Position(2)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ participants.add(new Car("브리", new Position(2)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
- assertThatThrownBy(() -> {
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- }).isInstanceOf(UnsupportedOperationException.class);
+ // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
+ assertThatThrownBy(() -> {
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ }).isInstanceOf(UnsupportedOperationException.class);
+ }
}
}
diff --git a/clean-code/complete/src/test/java/cholog/goodcode/PredictableCodeTest.java b/clean-code/complete/src/test/java/cholog/goodcode/PredictableCodeTest.java
index 2e36351..b8ea0c1 100644
--- a/clean-code/complete/src/test/java/cholog/goodcode/PredictableCodeTest.java
+++ b/clean-code/complete/src/test/java/cholog/goodcode/PredictableCodeTest.java
@@ -1,6 +1,7 @@
package cholog.goodcode;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -23,462 +24,478 @@ public class PredictableCodeTest {
record Car(int position) {
}
- /**
- * 아래 코드는 자동차 경주에서 평균 위치를 계산하는 기능입니다.
- * 게임 참여자가 없을 경우 -1을 반환하여 처리하면 해당 기능을 사용하는 개발자들은 매번 -1을 체크해야 하고, 이는 실수하기 좋은 코드가 됩니다.
- * 어떻게 매번 -1를 체크하지 않도록 할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 매번 -1를 체크하지 않도록 할 수 있을까?")
- void 어떻게_매번_값을_체크하지_않도록_할_수_있을까() {
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ @Nested
+ @DisplayName("반환값으로 상태 전달하기")
+ class ReturnValueTest {
+ /**
+ * 아래 코드는 자동차 경주에서 평균 위치를 계산하는 기능입니다.
+ * 게임 참여자가 없을 경우 -1을 반환하여 처리하면 해당 기능을 사용하는 개발자들은 매번 -1을 체크해야 하고, 이는 실수하기 좋은 코드가 됩니다.
+ * 어떻게 매번 -1를 체크하지 않도록 할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 매번 -1를 체크하지 않도록 할 수 있을까?")
+ void 어떻게_매번_값을_체크하지_않도록_할_수_있을까() {
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- Integer averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ Integer averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- // Note: null은 버그를 유발할 수 있다.
- return null;
+ if (average.isEmpty()) {
+ // Note: null은 버그를 유발할 수 있다.
+ return null;
+ }
+ return (int) average.getAsDouble();
}
- return (int) average.getAsDouble();
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- final var averagePosition = racingGame.averagePosition();
+ final var averagePosition = racingGame.averagePosition();
- assertThat(averagePosition).isNull();
- }
+ assertThat(averagePosition).isNull();
+ }
- /**
- * null을 통해 의도를 전달하는 방법입니다.
- * null을 사용하면 코드를 읽는 사람이 해당 변수가 null일 수 있다는 것을 알 수 있습니다.
- * 하지만 null을 사용하면 NullPointerException이 발생할 수 있고, null로 인해 생길 수 있는 부작용이 발생할 수 있습니다.
- * null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?")
- void null을_사용하지_않고_의도를_전달하는_방법은_무엇일까() {
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * null을 통해 의도를 전달하는 방법입니다.
+ * null을 사용하면 코드를 읽는 사람이 해당 변수가 null일 수 있다는 것을 알 수 있습니다.
+ * 하지만 null을 사용하면 NullPointerException이 발생할 수 있고, null로 인해 생길 수 있는 부작용이 발생할 수 있습니다.
+ * null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?")
+ void null을_사용하지_않고_의도를_전달하는_방법은_무엇일까() {
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- // Note: Optional를 사용하면 외부에 처리를 위임하게 되고, 응집도가 떨어질 수 있다.
- Optional averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ // Note: Optional를 사용하면 외부에 처리를 위임하게 되고, 응집도가 떨어질 수 있다.
+ Optional averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- return Optional.empty();
+ if (average.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of((int) average.getAsDouble());
}
- return Optional.of((int) average.getAsDouble());
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- final var averagePosition = racingGame.averagePosition();
+ final var averagePosition = racingGame.averagePosition();
- assertThat(averagePosition).isEmpty();
- }
+ assertThat(averagePosition).isEmpty();
+ }
- /**
- * Optional을 통해 의도를 전달하는 방법입니다.
- * Optional을 사용하면 코드를 읽는 사람이 해당 변수가 비어있을 수 있다는 것을 알 수 있습니다.
- * 지금의 구조는 참여자가 없다는 사실을 전달할 수 있지만, 그 처리를 외부에 위임하고 있습니다.
- * 만약 참여자가 없다는 사실을 처리하는 코드가 여러군데에 중복되어 있다면, 이는 유지보수성을 떨어뜨리는 코드가 됩니다.
- * 어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?")
- void 어떻게_참여자가_없는_상황을_처리하는_코드를_중복하지_않고_처리할_수_있을까() {
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * Optional을 통해 의도를 전달하는 방법입니다.
+ * Optional을 사용하면 코드를 읽는 사람이 해당 변수가 비어있을 수 있다는 것을 알 수 있습니다.
+ * 지금의 구조는 참여자가 없다는 사실을 전달할 수 있지만, 그 처리를 외부에 위임하고 있습니다.
+ * 만약 참여자가 없다는 사실을 처리하는 코드가 여러군데에 중복되어 있다면, 이는 유지보수성을 떨어뜨리는 코드가 됩니다.
+ * 어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?")
+ void 어떻게_참여자가_없는_상황을_처리하는_코드를_중복하지_않고_처리할_수_있을까() {
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
- int averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
+ int averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- throw new IllegalStateException("게임 참여자가 없습니다.");
+ if (average.isEmpty()) {
+ throw new IllegalStateException("게임 참여자가 없습니다.");
+ }
+ return (int) average.getAsDouble();
}
- return (int) average.getAsDouble();
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- assertThatThrownBy(() -> {
- racingGame.averagePosition();
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("게임 참여자가 없습니다.");
- }
+ assertThatThrownBy(() -> {
+ racingGame.averagePosition();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("게임 참여자가 없습니다.");
+ }
- /**
- * 예외를 발생하여 명시적으로 처리하는 방법입니다.
- * 논리적으로 참여자가 없다는 사실을 처리하는 코드를 중복하지 않고 처리할 수 있습니다.
- * 설계에 따라 외부에서 처리하는 것이 적합할 경우 Optional을 사용할 수 있지만, 설계에 따라 예외를 발생하는 것이 적합할 수 있습니다.
- * 정답은 없습니다. 상황에 따라 적절한 방법을 선택해야 합니다.
- */
- @Test
- @DisplayName("예외를 발생하여 명시적으로 처리하는 방법입니다.")
- void 예외를_발생하여_명시적으로_처리하는_방법입니다() {
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * 예외를 발생하여 명시적으로 처리하는 방법입니다.
+ * 논리적으로 참여자가 없다는 사실을 처리하는 코드를 중복하지 않고 처리할 수 있습니다.
+ * 설계에 따라 외부에서 처리하는 것이 적합할 경우 Optional을 사용할 수 있지만, 설계에 따라 예외를 발생하는 것이 적합할 수 있습니다.
+ * 정답은 없습니다. 상황에 따라 적절한 방법을 선택해야 합니다.
+ */
+ @Test
+ @DisplayName("예외를 발생하여 명시적으로 처리하는 방법입니다.")
+ void 예외를_발생하여_명시적으로_처리하는_방법입니다() {
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
- int averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
+ int averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- throw new IllegalStateException("게임 참여자가 없습니다.");
+ if (average.isEmpty()) {
+ throw new IllegalStateException("게임 참여자가 없습니다.");
+ }
+ return (int) average.getAsDouble();
}
- return (int) average.getAsDouble();
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- assertThatThrownBy(() -> {
- racingGame.averagePosition();
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("게임 참여자가 없습니다.");
+ assertThatThrownBy(() -> {
+ racingGame.averagePosition();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("게임 참여자가 없습니다.");
+ }
}
- /**
- * 아래 코드는 4 이상의 파워가 넘어왔을 때 자동차를 움직이는 기능입니다.
- * 자동차의 위치를 조회하는 것 또한 해당 메서드를 통해 조회하고 있습니다.
- * 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선한다.")
- void 자동차_이동과_조회를_같이_할_경우_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- // Note: 이동과 조회를 같이 할 경우 부수효과가 발생할 수 있다. Command-Query Separation 원칙을 준수한다.
- void move(final int power) {
- if (power <= 4) {
- return;
+ @Nested
+ @DisplayName("Command-Query Separation")
+ class CommandQuerySeparationTest {
+ /**
+ * 아래 코드는 4 이상의 파워가 넘어왔을 때 자동차를 움직이는 기능입니다.
+ * 자동차의 위치를 조회하는 것 또한 해당 메서드를 통해 조회하고 있습니다.
+ * 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 자동차_이동과_조회를_같이_할_경우_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ // Note: 이동과 조회를 같이 할 경우 부수효과가 발생할 수 있다. Command-Query Separation 원칙을 준수한다.
+ void move(final int power) {
+ if (power <= 4) {
+ return;
+ }
+
+ ++position;
}
- ++position;
- }
-
- int getPosition() {
- return position;
+ int getPosition() {
+ return position;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.move(5);
- final var position = car.getPosition();
+ car.move(5);
+ final var position = car.getPosition();
- assertThat(position).isEqualTo(1);
+ assertThat(position).isEqualTo(1);
+ }
}
- /**
- * 아래 코드는 자동차가 최대 5칸을 움직일 수 있는 코드입니다.
- * 5칸에 위치하였을 때 더 이동하려고 하면 더 이상 움직이지 않고 위치를 유지하고 있습니다.
- * 아래 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
- void 자동차가_최대_위치에서_움직이지_않고_유지하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- Car(final int position) {
- this.position = position;
- }
-
- void move(final int power) {
- if (power <= 4) {
- return;
- }
- if (position >= 5) {
- // Note: 중요한 동작을 무시하는 것은 버그를 유발할 수 있다.
- throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ @Nested
+ @DisplayName("동작을 무시하지 않기")
+ class DoNotIgnoreActionTest {
+ /**
+ * 아래 코드는 자동차가 최대 5칸을 움직일 수 있는 코드입니다.
+ * 5칸에 위치하였을 때 더 이동하려고 하면 더 이상 움직이지 않고 위치를 유지하고 있습니다.
+ * 아래 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 자동차가_최대_위치에서_움직이지_않고_유지하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ Car(final int position) {
+ this.position = position;
}
- position++;
+ void move(final int power) {
+ if (power <= 4) {
+ return;
+ }
+ if (position >= 5) {
+ // Note: 중요한 동작을 무시하는 것은 버그를 유발할 수 있다.
+ throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ }
+
+ position++;
+ }
}
- }
- final var car = new Car(5);
+ final var car = new Car(5);
- assertThatThrownBy(() -> {
- car.move(5);
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("더 이상 움직일 수 없습니다.");
- }
-
- /**
- * 더 이상 움직일 수 없을 때 예외를 발생하여 명시적으로 처리하는 방법입니다.
- * 중요한 동작을 무시하는 것은 버그를 유발할 수 있습니다.
- * 하지만 아직 파워가 4보다 작을 때는 무시하고 있습니다.
- * 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
- void 파워가_4보다_작을_때_무시하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- Car(final int position) {
- this.position = position;
- }
+ assertThatThrownBy(() -> {
+ car.move(5);
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("더 이상 움직일 수 없습니다.");
+ }
- void move(final int power) {
- if (power <= 4) {
- // Note: 중요한 것은 의도다. 파워가 4보다 적을 때는 움직이지 않는 것이 의도된 설계다. 코드의 형태가 아닌 의도에 집중한다.
- return;
- }
- if (position >= 5) {
- throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ /**
+ * 더 이상 움직일 수 없을 때 예외를 발생하여 명시적으로 처리하는 방법입니다.
+ * 중요한 동작을 무시하는 것은 버그를 유발할 수 있습니다.
+ * 하지만 아직 파워가 4보다 작을 때는 무시하고 있습니다.
+ * 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 파워가_4보다_작을_때_무시하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ Car(final int position) {
+ this.position = position;
}
- position++;
+ void move(final int power) {
+ if (power <= 4) {
+ // Note: 중요한 것은 의도다. 파워가 4보다 적을 때는 움직이지 않는 것이 의도된 설계다. 코드의 형태가 아닌 의도에 집중한다.
+ return;
+ }
+ if (position >= 5) {
+ throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ }
+
+ position++;
+ }
}
- }
- final var car = new Car(5);
+ final var car = new Car(5);
- assertThatThrownBy(() -> {
- car.move(5);
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("더 이상 움직일 수 없습니다.");
+ assertThatThrownBy(() -> {
+ car.move(5);
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("더 이상 움직일 수 없습니다.");
+ }
}
- /**
- * 아래 코드는 입력된 문자열 명령에 따라 동작하는 코드입니다.
- * 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선한다.")
- void 문자열로_명령을_받는_것은_어떠한_문제가_있을지_고민_후_개선한다() {
- enum Command {
- PLUS,
- MINUS
- }
+ @Nested
+ @DisplayName("타입 안전성")
+ class TypeSafetyTest {
+ /**
+ * 아래 코드는 입력된 문자열 명령에 따라 동작하는 코드입니다.
+ * 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 문자열로_명령을_받는_것은_어떠한_문제가_있을지_고민_후_개선한다() {
+ enum Command {
+ PLUS,
+ MINUS
+ }
- class Calculator {
- // Note: 문자열로 명령을 받는 것은 타입 안정성이 보장되지 않아 버그를 유발할 수 있다.
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- if (command == Command.PLUS) {
- return left + right;
- }
- if (command == Command.MINUS) {
- return left - right;
+ class Calculator {
+ // Note: 문자열로 명령을 받는 것은 타입 안정성이 보장되지 않아 버그를 유발할 수 있다.
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ if (command == Command.PLUS) {
+ return left + right;
+ }
+ if (command == Command.MINUS) {
+ return left - right;
+ }
+
+ throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
}
-
- throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
}
- }
-
- assertAll(
- () -> assertThat(Calculator.calculate(Command.PLUS, 1, 2)).isEqualTo(3),
- () -> assertThat(Calculator.calculate(Command.MINUS, 1, 2)).isEqualTo(-1)
- );
- }
- /**
- * 명령을 문자열에서 열거형으로 변경하여 명시적으로 처리하는 방법입니다.
- * 문자열 상수의 경우 타입 안정성이 보장되지 않아 버그를 유발할 수 있습니다.
- * 열거형으로 변경하면 타입 안정성이 보장되어 버그를 줄일 수 있습니다.
- * 하지만 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓친다면 버그를 유발할 수 있습니다.
- * 어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
- */
- @Test
- @DisplayName("어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
- void 어떻게_새로운_열거형이_추가되었을_때_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
+ assertAll(
+ () -> assertThat(Calculator.calculate(Command.PLUS, 1, 2)).isEqualTo(3),
+ () -> assertThat(Calculator.calculate(Command.MINUS, 1, 2)).isEqualTo(-1)
+ );
}
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- if (command == Command.PLUS) {
- return left + right;
- }
- if (command == Command.MINUS) {
- return left - right;
+ /**
+ * 명령을 문자열에서 열거형으로 변경하여 명시적으로 처리하는 방법입니다.
+ * 문자열 상수의 경우 타입 안정성이 보장되지 않아 버그를 유발할 수 있습니다.
+ * 열거형으로 변경하면 타입 안정성이 보장되어 버그를 줄일 수 있습니다.
+ * 하지만 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓친다면 버그를 유발할 수 있습니다.
+ * 어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
+ void 어떻게_새로운_열거형이_추가되었을_때_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
+ }
+
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ if (command == Command.PLUS) {
+ return left + right;
+ }
+ if (command == Command.MINUS) {
+ return left - right;
+ }
+
+ throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
}
+ }
- throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
+ for (final Command command : Command.values()) {
+ // Note: 런타임 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있다.
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
- for (final Command command : Command.values()) {
- // Note: 런타임 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있다.
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
- }
+ /**
+ * 테스트 코드를 추가하여 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않는 방법입니다.
+ * 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않으려면 테스트 코드를 작성하여 해당 명령을 처리하는 코드를 놓치지 않도록 해야 합니다.
+ * 하지만 테스트를 직접 실행시키기 전까지 해당 명령을 처리하는 코드를 놓칠 수 있습니다.
+ * 어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
+ void 어떻게_더_빠른_시점에_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
+ }
- /**
- * 테스트 코드를 추가하여 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않는 방법입니다.
- * 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않으려면 테스트 코드를 작성하여 해당 명령을 처리하는 코드를 놓치지 않도록 해야 합니다.
- * 하지만 테스트를 직접 실행시키기 전까지 해당 명령을 처리하는 코드를 놓칠 수 있습니다.
- * 어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
- */
- @Test
- @DisplayName("어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
- void 어떻게_더_빠른_시점에_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
- }
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ return switch (command) {
+ case PLUS -> left + right;
+ case MINUS -> left - right;
+ case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
+ };
+ }
+ }
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- return switch (command) {
- case PLUS -> left + right;
- case MINUS -> left - right;
- case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
- };
+ for (final Command command : Command.values()) {
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
- for (final Command command : Command.values()) {
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
- }
+ /**
+ * switch 문을 사용하여 명령을 처리하는 방법입니다.
+ * 놓친 코드를 찾는 시점을 런타임이 아닌 컴파일 타임으로 변경하여 해당 명령을 처리하는 코드를 놓치지 않을 수 있습니다.
+ * 코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋다.")
+ void 코드를_작성할_때_최대한_런타임_시점에_발생할_수_있는_오류를_컴파일_타임으로_발생하도록_작성하는_것이_좋다() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
+ }
- /**
- * switch 문을 사용하여 명령을 처리하는 방법입니다.
- * 놓친 코드를 찾는 시점을 런타임이 아닌 컴파일 타임으로 변경하여 해당 명령을 처리하는 코드를 놓치지 않을 수 있습니다.
- * 코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋습니다.
- */
- @Test
- @DisplayName("코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋다.")
- void 코드를_작성할_때_최대한_런타임_시점에_발생할_수_있는_오류를_컴파일_타임으로_발생하도록_작성하는_것이_좋다() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
- }
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ return switch (command) {
+ case PLUS -> left + right;
+ case MINUS -> left - right;
+ case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
+ };
+ }
+ }
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- return switch (command) {
- case PLUS -> left + right;
- case MINUS -> left - right;
- case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
- };
+ for (final Command command : Command.values()) {
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
- for (final Command command : Command.values()) {
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
- }
+ /**
+ * 추가로 열거형도 객체로 바라보는 방법도 있습니다.
+ * 이러한 방법은 열거형에 역할을 부여하여 열거형이 해당 역할을 수행하도록 하는 방법입니다.
+ * 지금과 같이 간단한 코드에선 더욱 응집도가 높은 코드가 될 수 있지만, 열거형을 상수와 객체 역할을 모두 수행하도록 하는 것은 적절하지 않을 수 있습니다.
+ * 열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.
+ */
+ @Test
+ @DisplayName("열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.")
+ void 열거형이_해당_역할을_수행하도록_하는_것이_적합한지_충분한_고민을_하고_사용해야_합니다() {
+ // Note: 열거형을 상수로 바라볼 것인가, 객체로 바라볼 것인가에 대한 고민이 필요하다. 고민이 부족하면 부작용이 커진다.
+ enum Command {
+ PLUS((left, right) -> left + right),
+ MINUS((left, right) -> left - right),
+ MULTIPLY((left, right) -> left * right);
+
+ private final BiFunction function;
+
+ Command(final BiFunction function) {
+ this.function = function;
+ }
- /**
- * 추가로 열거형도 객체로 바라보는 방법도 있습니다.
- * 이러한 방법은 열거형에 역할을 부여하여 열거형이 해당 역할을 수행하도록 하는 방법입니다.
- * 지금과 같이 간단한 코드에선 더욱 응집도가 높은 코드가 될 수 있지만, 열거형을 상수와 객체 역할을 모두 수행하도록 하는 것은 적절하지 않을 수 있습니다.
- * 열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.
- */
- @Test
- @DisplayName("열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.")
- void 열거형이_해당_역할을_수행하도록_하는_것이_적합한지_충분한_고민을_하고_사용해야_합니다() {
- // Note: 열거형을 상수로 바라볼 것인가, 객체로 바라볼 것인가에 대한 고민이 필요하다. 고민이 부족하면 부작용이 커진다.
- enum Command {
- PLUS((left, right) -> left + right),
- MINUS((left, right) -> left - right),
- MULTIPLY((left, right) -> left * right);
-
- private final BiFunction function;
-
- Command(final BiFunction function) {
- this.function = function;
+ int execute(int left, int right) {
+ return function.apply(left, right);
+ }
}
- int execute(int left, int right) {
- return function.apply(left, right);
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ return command.execute(left, right);
+ }
}
- }
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- return command.execute(left, right);
+ for (final Command command : Command.values()) {
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
-
- for (final Command command : Command.values()) {
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
}
}
diff --git a/clean-code/complete/src/test/java/cholog/goodcode/ReadableCodeTest.java b/clean-code/complete/src/test/java/cholog/goodcode/ReadableCodeTest.java
index 1455ba8..6c3d837 100644
--- a/clean-code/complete/src/test/java/cholog/goodcode/ReadableCodeTest.java
+++ b/clean-code/complete/src/test/java/cholog/goodcode/ReadableCodeTest.java
@@ -23,648 +23,616 @@
* 가독성 높은 코드는 유지보수성을 높이고 버그를 줄이는데 도움을 줍니다.
* 유지보수성과 확장성을 위한 읽기 좋은 코드를 작성하는 방법을 알아봅니다.
*/
-@Nested
-@DisplayName("가독성 높은 코드")
public class ReadableCodeTest {
- /**
- * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
- * 한 눈에 봤을 때 코드의 의도를 파악하기 어렵습니다.
- * 어떻게 의도를 전달할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 의도를 전달할 수 있을까?")
- void 어떻게_의도를_전달할_수_있을까() {
- class Car {
- // 자동차 위치
- private int p = 0;
-
- void forward() {
- if (p > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
- }
-
- p += 1;
+ @Nested
+ @DisplayName("이름으로 의도 전달하기")
+ class NamingIntentTest {
+ /**
+ * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
+ * 한 눈에 봤을 때 코드의 의도를 파악하기 어렵습니다.
+ * 어떻게 의도를 전달할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 의도를 전달할 수 있을까?")
+ void 어떻게_의도를_전달할_수_있을까() {
+ class Car {
+ // 자동차 위치
+ private int p = 0;
+
+ void forward() {
+ if (p > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
+
+ p += 1;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.forward();
- assertThat(car.p).isEqualTo(1);
- }
+ car.forward();
+ assertThat(car.p).isEqualTo(1);
+ }
- /**
- * 주석을 사용하여 의도를 전달하는 방법입니다.
- * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
- * 주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?")
- void 주석을_사용하지_않고도_의도를_전달할_수_있는_방법은_없을까() {
- class Car {
- private int position = 0;
+ /**
+ * 주석을 사용하여 의도를 전달하는 방법입니다.
+ * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
+ * 주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?")
+ void 주석을_사용하지_않고도_의도를_전달할_수_있는_방법은_없을까() {
+ class Car {
+ private int position = 0;
+
+ void forward() {
+ if (position > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
- void forward() {
- if (position > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ position += 1;
}
-
- position += 1;
}
+
+ final var car = new Car();
+
+ car.forward();
+ assertThat(car.position).isEqualTo(1);
}
- final var car = new Car();
+ /**
+ * 주석을 사용하지 않고 의미있는 이름을 통해 의도를 전달하는 방법입니다.
+ * 의미있는 이름을 사용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 또한 코드로 관리되기 때문에 코드 변경 시 주석을 신경쓰지 않아도 됩니다.
+ * 지금의 position은 이름을 통해 의도를 전달하고 있지만, 최대 5까지만 움직인다는 사실은 동작을 통해 알 수 있습니다.
+ * 코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까")
+ void 코드를_통해_객체의_역할을_명확하게_드러내는_방법은_없을까() {
+ class Position {
+ private final int value;
+
+ public Position() {
+ this(0);
+ }
- car.forward();
- assertThat(car.position).isEqualTo(1);
- }
+ public Position(final int value) {
+ if (value > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
- /**
- * 주석을 사용하지 않고 의미있는 이름을 통해 의도를 전달하는 방법입니다.
- * 의미있는 이름을 사용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 또한 코드로 관리되기 때문에 코드 변경 시 주석을 신경쓰지 않아도 됩니다.
- * 지금의 position은 이름을 통해 의도를 전달하고 있지만, 최대 5까지만 움직인다는 사실은 동작을 통해 알 수 있습니다.
- * 코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까?
- */
- @Test
- @DisplayName("코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까")
- void 코드를_통해_객체의_역할을_명확하게_드러내는_방법은_없을까() {
- class Position {
- private final int value;
-
- public Position() {
- this(0);
- }
+ this.value = value;
+ }
- public Position(final int value) {
- if (value > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ Position forward() {
+ return new Position(value + 1);
}
- this.value = value;
- }
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Position position = (Position) o;
+ return value == position.value;
+ }
- Position forward() {
- return new Position(value + 1);
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- @Override
- public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final Position position = (Position) o;
- return value == position.value;
- }
+ class Car {
+ private Position position = new Position();
- @Override
- public int hashCode() {
- return Objects.hash(value);
+ void forward() {
+ position = position.forward();
+ }
}
- }
- class Car {
- private Position position = new Position();
+ final var car = new Car();
- void forward() {
- position = position.forward();
- }
+ car.forward();
+ assertThat(car.position).isEqualTo(new Position(1));
}
- final var car = new Car();
+ /**
+ * 움직임을 담당하는 객체를 만들어서 코드를 통해 객체의 역할을 명확하게 드러내는 방법입니다.
+ * 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 하지만 의미없는 객체가 많이 생기게 된다면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다.
+ * 코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어듭니다.
+ */
+ @Test
+ @DisplayName("코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어든다")
+ void 코드_자체로_설명이_되도록_코드를_작성하면_유지_및_관리의_비용이_줄어든다() {
+ class Position {
+ private final int value;
+
+ public Position() {
+ this(0);
+ }
- car.forward();
- assertThat(car.position).isEqualTo(new Position(1));
- }
+ public Position(final int value) {
+ if (value > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
- /**
- * 움직임을 담당하는 객체를 만들어서 코드를 통해 객체의 역할을 명확하게 드러내는 방법입니다.
- * 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 하지만 의미없는 객체가 많이 생기게 된다면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다.
- * 코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어듭니다.
- */
- @Test
- @DisplayName("코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어든다")
- void 코드_자체로_설명이_되도록_코드를_작성하면_유지_및_관리의_비용이_줄어든다() {
- class Position {
- private final int value;
-
- public Position() {
- this(0);
- }
+ this.value = value;
+ }
- public Position(final int value) {
- if (value > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ Position forward() {
+ return new Position(value + 1);
}
- this.value = value;
- }
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Position position = (Position) o;
+ return value == position.value;
+ }
- Position forward() {
- return new Position(value + 1);
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- @Override
- public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final Position position = (Position) o;
- return value == position.value;
- }
+ class Car {
+ private Position position = new Position();
- @Override
- public int hashCode() {
- return Objects.hash(value);
+ void forward() {
+ position = position.forward();
+ }
}
- }
- class Car {
- private Position position = new Position();
+ final var car = new Car();
- void forward() {
- position = position.forward();
- }
+ car.forward();
+ assertThat(car.position).isEqualTo(new Position(1));
}
+ }
- final var car = new Car();
+ @Nested
+ @DisplayName("일관된 코드 스타일")
+ class ConsistentStyleTest {
+ /**
+ * 일관적이지 않은 코드 스타일은 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄 수 있습니다.
+ * 오해할 위험을 줄이면, 버그가 줄어들고 혼란스러운 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
+ */
+ @Test
+ @DisplayName("일관된 코드 스타일을 가져간다")
+ void 일관된_코드_스타일을_가져간다() {
+ // @formatter:off
+ class Car {
+ private String name;
+ private int position;
- car.forward();
- assertThat(car.position).isEqualTo(new Position(1));
- }
+ public void forward() {
+ position += 1;
+ }
- /**
- * 일관적이지 않은 코드 스타일은 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄 수 있습니다.
- * 오해할 위험을 줄이면, 버그가 줄어들고 혼란스러운 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
- */
- @Test
- @DisplayName("일관된 코드 스타일을 가져간다")
- void 일관된_코드_스타일을_가져간다() {
- // @formatter:off
- class Car {
- private String name;
- private int position;
-
- public void forward() {
- position += 1;
- }
+ public void backward() {
+ position -= 1;
+ }
- public void backward() {
- position -= 1;
- }
+ public String getName() {
+ return name;
+ }
- public String getName() {
- return name;
+ public int getPosition() {
+ return position;
+ }
}
+ // @formatter:on
- public int getPosition() {
- return position;
- }
- }
- // @formatter:on
+ final var car = new Car();
- final var car = new Car();
+ car.forward();
+ assertThat(car.getPosition()).isEqualTo(1);
+ }
- car.forward();
- assertThat(car.getPosition()).isEqualTo(1);
- }
+ /**
+ * 일관된 코드 스타일로 리팩토링한 코드입니다.
+ * 코드를 이해하기 쉽게 만들기 위해 일관된 코드 스타일을 사용하는 것이 중요합니다.
+ * 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄어들어 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
+ */
+ @Test
+ @DisplayName("일관된 코드 스타일로 리팩토링한 코드")
+ void 일관된_코드_스타일로_리팩토링한_코드() {
+ class Car {
+ private String name;
+ private int position;
- /**
- * 일관된 코드 스타일로 리팩토링한 코드입니다.
- * 코드를 이해하기 쉽게 만들기 위해 일관된 코드 스타일을 사용하는 것이 중요합니다.
- * 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄어들어 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
- */
- @Test
- @DisplayName("일관된 코드 스타일로 리팩토링한 코드")
- void 일관된_코드_스타일로_리팩토링한_코드() {
- class Car {
- private String name;
- private int position;
-
- public void forward() {
- position += 1;
- }
+ public void forward() {
+ position += 1;
+ }
- public void backward() {
- position -= 1;
- }
+ public void backward() {
+ position -= 1;
+ }
- public String getName() {
- return name;
- }
+ public String getName() {
+ return name;
+ }
- public int getPosition() {
- return position;
+ public int getPosition() {
+ return position;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.forward();
- assertThat(car.getPosition()).isEqualTo(1);
+ car.forward();
+ assertThat(car.getPosition()).isEqualTo(1);
+ }
}
- /**
- * 하나의 메서드가 많은 일을 하면 추상화 계층이 깊어지고, 코드를 이해하기 어려워집니다.
- * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
- * 아래 우승 로또 번호와 나의 로또 번호를 비교하여 상금을 계산하는 코드는 한 가지 이상의 일을 하고 있습니다.
- * 어떻게 추상화하여 메서드를 작게 만들 수 있을까?
- */
- @Test
- @DisplayName("어떻게 추상화하여 메서드를 작게 만들 수 있을까?")
- void 어떻게_추상화하여_메서드를_작게_만들_수_있을까() {
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- validateNumbers(numbers);
- validateNumbers(winningNumbers);
-
- final int count = countMatchNumbers(numbers, winningNumbers);
- return calculatePrizeByCount(count);
- }
-
- private void validateNumbers(final List lottoNumbers) {
- for (int number : lottoNumbers) {
- validateNumber(number);
+ @Nested
+ @DisplayName("메서드와 객체의 추상화")
+ class AbstractionTest {
+ /**
+ * 하나의 메서드가 많은 일을 하면 추상화 계층이 깊어지고, 코드를 이해하기 어려워집니다.
+ * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
+ * 아래 우승 로또 번호와 나의 로또 번호를 비교하여 상금을 계산하는 코드는 한 가지 이상의 일을 하고 있습니다.
+ * 어떻게 추상화하여 메서드를 작게 만들 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 추상화하여 메서드를 작게 만들 수 있을까?")
+ void 어떻게_추상화하여_메서드를_작게_만들_수_있을까() {
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ validateNumbers(numbers);
+ validateNumbers(winningNumbers);
+
+ final int count = countMatchNumbers(numbers, winningNumbers);
+ return calculatePrizeByCount(count);
}
- if (new HashSet<>(lottoNumbers).size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+
+ private void validateNumbers(final List lottoNumbers) {
+ for (int number : lottoNumbers) {
+ validateNumber(number);
+ }
+ if (new HashSet<>(lottoNumbers).size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
}
- }
- private void validateNumber(final Integer lottoNumber) {
- if (lottoNumber < 1 || lottoNumber > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ private void validateNumber(final Integer lottoNumber) {
+ if (lottoNumber < 1 || lottoNumber > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
}
- }
- private int countMatchNumbers(
- final List numbers,
- final List winningNumbers
- ) {
- int count = 0;
- for (int number : numbers) {
- for (int winningNumber : winningNumbers) {
- if (number == winningNumber) {
- count++;
+ private int countMatchNumbers(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ int count = 0;
+ for (int number : numbers) {
+ for (int winningNumber : winningNumbers) {
+ if (number == winningNumber) {
+ count++;
+ }
}
}
+
+ return count;
}
- return count;
+ private int calculatePrizeByCount(final int count) {
+ return switch (count) {
+ case 6 -> 1_000_000_000;
+ case 5 -> 50_000_000;
+ case 4 -> 500_000;
+ case 3 -> 5_000;
+ default -> 0;
+ };
+ }
}
- private int calculatePrizeByCount(final int count) {
- return switch (count) {
- case 6 -> 1_000_000_000;
- case 5 -> 50_000_000;
- case 4 -> 500_000;
- case 3 -> 5_000;
- default -> 0;
- };
- }
- }
+ final var lottoGame = new LottoGame();
- final var lottoGame = new LottoGame();
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
+ /**
+ * 메서드를 작게 만들어 추상화한 코드입니다.
+ * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
+ * 추상화 수준을 적절하게 유지하면 유지보수성을 높일 수 있습니다.
+ * 메서드를 작게 만들어 추상화하다 보면 메서드의 역할이 명확해지지만, 객체의 역할은 아직 명확하지 않습니다.
+ * 어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?")
+ void 어떻게_추상화하여_객체의_역할을_명확하게_드러낼_수_있을까() {
+ class LottoNumber {
+ private final int value;
+
+ public LottoNumber(final int value) {
+ if (value < 1 || value > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
+ this.value = value;
+ }
- /**
- * 메서드를 작게 만들어 추상화한 코드입니다.
- * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
- * 추상화 수준을 적절하게 유지하면 유지보수성을 높일 수 있습니다.
- * 메서드를 작게 만들어 추상화하다 보면 메서드의 역할이 명확해지지만, 객체의 역할은 아직 명확하지 않습니다.
- * 어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?
- */
- @Test
- @DisplayName("어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?")
- void 어떻게_추상화하여_객체의_역할을_명확하게_드러낼_수_있을까() {
- class LottoNumber {
- private final int value;
-
- public LottoNumber(final int value) {
- if (value < 1 || value > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
- }
- this.value = value;
- }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LottoNumber that = (LottoNumber) o;
+ return value == that.value;
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- LottoNumber that = (LottoNumber) o;
- return value == that.value;
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- @Override
- public int hashCode() {
- return Objects.hash(value);
- }
- }
+ class Lotto {
+ private final Set numbers;
- class Lotto {
- private final Set numbers;
+ public Lotto(final List numbers) {
+ this(numbers.stream()
+ .map(LottoNumber::new)
+ .collect(toSet()));
+ }
- public Lotto(final List numbers) {
- this(numbers.stream()
- .map(LottoNumber::new)
- .collect(toSet()));
- }
+ public Lotto(final Set numbers) {
+ if (numbers.size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
- public Lotto(final Set numbers) {
- if (numbers.size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ this.numbers = numbers;
}
- this.numbers = numbers;
- }
-
- int countMatchNumbers(final Lotto winningLotto) {
- int count = 0;
- for (LottoNumber number : numbers) {
- for (LottoNumber winningNumber : winningLotto.numbers) {
- if (number.equals(winningNumber)) {
- count++;
+ int countMatchNumbers(final Lotto winningLotto) {
+ int count = 0;
+ for (LottoNumber number : numbers) {
+ for (LottoNumber winningNumber : winningLotto.numbers) {
+ if (number.equals(winningNumber)) {
+ count++;
+ }
}
}
- }
- return count;
+ return count;
+ }
}
- }
- enum LottoRank {
- FIRST(6, 1_000_000_000),
- SECOND(5, 50_000_000),
- THIRD(4, 500_000),
- FOURTH(3, 5_000),
- NONE(0, 0);
+ enum LottoRank {
+ FIRST(6, 1_000_000_000),
+ SECOND(5, 50_000_000),
+ THIRD(4, 500_000),
+ FOURTH(3, 5_000),
+ NONE(0, 0);
- private final int count;
- private final int prize;
+ private final int count;
+ private final int prize;
- LottoRank(final int count, final int prize) {
- this.count = count;
- this.prize = prize;
- }
+ LottoRank(final int count, final int prize) {
+ this.count = count;
+ this.prize = prize;
+ }
- public static LottoRank of(final int count) {
- return Arrays.stream(values())
- .filter(prize -> prize.count == count)
- .findFirst()
- .orElse(NONE);
- }
+ public static LottoRank of(final int count) {
+ return Arrays.stream(values())
+ .filter(prize -> prize.count == count)
+ .findFirst()
+ .orElse(NONE);
+ }
- public int getPrize() {
- return prize;
+ public int getPrize() {
+ return prize;
+ }
}
- }
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
- }
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
+ }
- int calculatePrize(
- final Lotto lotto,
- final Lotto winningLotto
- ) {
- final var count = lotto.countMatchNumbers(winningLotto);
- final var lottoRank = LottoRank.of(count);
- return lottoRank.getPrize();
+ int calculatePrize(
+ final Lotto lotto,
+ final Lotto winningLotto
+ ) {
+ final var count = lotto.countMatchNumbers(winningLotto);
+ final var lottoRank = LottoRank.of(count);
+ return lottoRank.getPrize();
+ }
}
- }
- final var lottoGame = new LottoGame();
+ final var lottoGame = new LottoGame();
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
- /**
- * 객체의 역할을 명확하게 드러낸 코드입니다.
- * 객체가 자기 자신의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 코드를 읽는 사람이 코드의 의도를 파악하기 쉽게 만들기 위해 객체의 역할을 명확하게 드러내는 것이 중요합니다.
- */
- @Test
- @DisplayName("객체의 역할을 명확하게 드러낸 코드")
- void 객체의_역할을_명확하게_드러낸_코드() {
- class LottoNumber {
- private final int value;
-
- public LottoNumber(final int value) {
- if (value < 1 || value > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
- }
- this.value = value;
- }
+ /**
+ * 객체의 역할을 명확하게 드러낸 코드입니다.
+ * 객체가 자기 자신의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 코드를 읽는 사람이 코드의 의도를 파악하기 쉽게 만들기 위해 객체의 역할을 명확하게 드러내는 것이 중요합니다.
+ */
+ @Test
+ @DisplayName("객체의 역할을 명확하게 드러낸 코드")
+ void 객체의_역할을_명확하게_드러낸_코드() {
+ class LottoNumber {
+ private final int value;
+
+ public LottoNumber(final int value) {
+ if (value < 1 || value > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
+ this.value = value;
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- LottoNumber that = (LottoNumber) o;
- return value == that.value;
- }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LottoNumber that = (LottoNumber) o;
+ return value == that.value;
+ }
- @Override
- public int hashCode() {
- return Objects.hash(value);
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- }
-
- class Lotto {
- private final Set numbers;
- public Lotto(final List numbers) {
- this(numbers.stream()
- .map(LottoNumber::new)
- .collect(toSet()));
- }
+ class Lotto {
+ private final Set numbers;
- public Lotto(final Set numbers) {
- if (numbers.size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ public Lotto(final List numbers) {
+ this(numbers.stream()
+ .map(LottoNumber::new)
+ .collect(toSet()));
}
- this.numbers = numbers;
- }
+ public Lotto(final Set numbers) {
+ if (numbers.size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
+
+ this.numbers = numbers;
+ }
- int countMatchNumbers(final Lotto winningLotto) {
- int count = 0;
- for (LottoNumber number : numbers) {
- for (LottoNumber winningNumber : winningLotto.numbers) {
- if (number.equals(winningNumber)) {
- count++;
+ int countMatchNumbers(final Lotto winningLotto) {
+ int count = 0;
+ for (LottoNumber number : numbers) {
+ for (LottoNumber winningNumber : winningLotto.numbers) {
+ if (number.equals(winningNumber)) {
+ count++;
+ }
}
}
- }
- return count;
+ return count;
+ }
}
- }
- enum LottoRank {
- FIRST(6, 1_000_000_000),
- SECOND(5, 50_000_000),
- THIRD(4, 500_000),
- FOURTH(3, 5_000),
- NONE(0, 0);
+ enum LottoRank {
+ FIRST(6, 1_000_000_000),
+ SECOND(5, 50_000_000),
+ THIRD(4, 500_000),
+ FOURTH(3, 5_000),
+ NONE(0, 0);
- private final int count;
- private final int prize;
+ private final int count;
+ private final int prize;
- LottoRank(final int count, final int prize) {
- this.count = count;
- this.prize = prize;
- }
+ LottoRank(final int count, final int prize) {
+ this.count = count;
+ this.prize = prize;
+ }
- public static LottoRank of(final int count) {
- return Arrays.stream(values())
- .filter(prize -> prize.count == count)
- .findFirst()
- .orElse(NONE);
- }
+ public static LottoRank of(final int count) {
+ return Arrays.stream(values())
+ .filter(prize -> prize.count == count)
+ .findFirst()
+ .orElse(NONE);
+ }
- public int getPrize() {
- return prize;
+ public int getPrize() {
+ return prize;
+ }
}
- }
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
- }
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
+ }
- int calculatePrize(
- final Lotto lotto,
- final Lotto winningLotto
- ) {
- final var count = lotto.countMatchNumbers(winningLotto);
- final var lottoRank = LottoRank.of(count);
- return lottoRank.getPrize();
+ int calculatePrize(
+ final Lotto lotto,
+ final Lotto winningLotto
+ ) {
+ final var count = lotto.countMatchNumbers(winningLotto);
+ final var lottoRank = LottoRank.of(count);
+ return lottoRank.getPrize();
+ }
}
- }
-
- final var lottoGame = new LottoGame();
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
+ final var lottoGame = new LottoGame();
- /**
- * 메서드의 이름을 잘 지어도 매개변수가 무엇이고 어떤 역할을 하는지 알기 어렵다면 의미를 전달하기 어렵습니다.
- * 매개변수 또한 의미를 전달할 수 있도록 작성하는 것이 중요합니다.
- * 아래 코드는 좋아하는 음식과 싫어하는 음식을 가지고 있는 Crew 객체를 생성하는 코드입니다.
- * 클래스 내에 이름을 잘 지어두었다면 의미를 전달할 수 있지만, 매번 해당 클래스로 가서 이름을 확인하는 방법은 번거롭습니다.
- * 어떻게 매개변수의 의미를 전달할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 매개변수의 의미를 전달할 수 있을까?")
- void 어떻게_매개변수의_의미를_전달할_수_있을까() {
- final var crew = new Crew(
- /*name*/ "Neo",
- /*likeMenuItems*/ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- /*dislikeMenuItems*/ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
-
- assertThat(crew.name()).isEqualTo("Neo");
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
}
- /**
- * 주석으로 매개변수의 의미를 전달할 수 있는 코드입니다.
- * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
- * 주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?")
- void 주석을_사용하지_않고_매개변수의_의미를_전달할_수_있는_방법은_없을까() {
- /* Note: 자바는 네임드 파라미터를 지원하지 않는다. IntelliJ에서 도움을 주지만 아쉬운 부분이 있다.
- final var neo = new Crew(
- name: "neo",
- likeMenuItems: Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- dislikeMenuItems: Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
+ @Nested
+ @DisplayName("매개변수의 의미 전달")
+ class ParameterMeaningTest {
+ /**
+ * 메서드의 이름을 잘 지어도 매개변수가 무엇이고 어떤 역할을 하는지 알기 어렵다면 의미를 전달하기 어렵습니다.
+ * 매개변수 또한 의미를 전달할 수 있도록 작성하는 것이 중요합니다.
+ * 아래 코드는 좋아하는 음식과 싫어하는 음식을 가지고 있는 Crew 객체를 생성하는 코드입니다.
+ * 클래스 내에 이름을 잘 지어두었다면 의미를 전달할 수 있지만, 매번 해당 클래스로 가서 이름을 확인하는 방법은 번거롭습니다.
+ * 어떻게 매개변수의 의미를 전달할 수 있을까?
*/
-
- class Builder {
- private String name;
- private Set likeMenuItems;
- private Set dislikeMenuItems;
-
- public Builder name(final String name) {
- this.name = name;
- return this;
- }
-
- public Builder likeMenuItems(final Set likeMenuItems) {
- this.likeMenuItems = likeMenuItems;
- return this;
- }
-
- public Builder dislikeMenuItems(final Set dislikeMenuItems) {
- this.dislikeMenuItems = dislikeMenuItems;
- return this;
- }
-
- public Crew build() {
- return new Crew(name, likeMenuItems, dislikeMenuItems);
- }
+ @Test
+ @DisplayName("어떻게 매개변수의 의미를 전달할 수 있을까?")
+ void 어떻게_매개변수의_의미를_전달할_수_있을까() {
+ final var crew = new Crew(
+ /*name*/ "Neo",
+ /*likeMenuItems*/ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ /*dislikeMenuItems*/ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+
+ assertThat(crew.name()).isEqualTo("Neo");
}
- final var crew = new Builder()
- .name("Neo")
- .dislikeMenuItems(Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스"))
- .likeMenuItems(Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"))
- .build();
-
- assertThat(crew.name()).isEqualTo("Neo");
- }
-
- /**
- * 빌더를 통해 매개변수의 의미를 전달할 수 있는 코드입니다.
- * 빌더를 사용하면 매개변수의 의미를 전달할 수 있고, 빌더를 통해 객체를 생성할 때 매개변수의 순서를 신경쓰지 않아도 됩니다.
- * 매개변수가 많아지면 어떤 값을 설정했는지 확인이 어렵거나 어떤 매개변수끼리 의미가 있는지 확인이 어려워질 수 있습니다.
- * 매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?")
- void 매개변수를_묶어_의미를_전달할_수_있는_방법은_없을까() {
- record Taste(
- Set likeMenuItems,
- Set dislikeMenuItems
- ) {
- static class Builder {
+ /**
+ * 주석으로 매개변수의 의미를 전달할 수 있는 코드입니다.
+ * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
+ * 주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?")
+ void 주석을_사용하지_않고_매개변수의_의미를_전달할_수_있는_방법은_없을까() {
+ /* Note: 자바는 네임드 파라미터를 지원하지 않는다. IntelliJ에서 도움을 주지만 아쉬운 부분이 있다.
+ final var neo = new Crew(
+ name: "neo",
+ likeMenuItems: Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ dislikeMenuItems: Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+ */
+
+ class Builder {
+ private String name;
private Set likeMenuItems;
private Set dislikeMenuItems;
- public Builder likeMenuItems(final String... likeMenuItems) {
- return likeMenuItems(Set.of(likeMenuItems));
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
}
public Builder likeMenuItems(final Set likeMenuItems) {
@@ -672,243 +640,293 @@ public Builder likeMenuItems(final Set likeMenuItems) {
return this;
}
- public Builder dislikeMenuItems(final String... dislikeMenuItems) {
- return dislikeMenuItems(Set.of(dislikeMenuItems));
- }
-
public Builder dislikeMenuItems(final Set dislikeMenuItems) {
this.dislikeMenuItems = dislikeMenuItems;
return this;
}
- public Taste build() {
- return new Taste(likeMenuItems, dislikeMenuItems);
+ public Crew build() {
+ return new Crew(name, likeMenuItems, dislikeMenuItems);
}
}
- }
-
- record Crew(
- String name,
- Taste taste
- ) {
- static class Builder {
- private String name;
- private Taste taste;
-
- public Builder name(final String name) {
- this.name = name;
- return this;
- }
- public Builder taste(final Taste taste) {
- this.taste = taste;
- return this;
- }
+ final var crew = new Builder()
+ .name("Neo")
+ .dislikeMenuItems(Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스"))
+ .likeMenuItems(Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"))
+ .build();
- public Crew build() {
- return new Crew(name, taste);
- }
- }
+ assertThat(crew.name()).isEqualTo("Neo");
}
- /* Note: 만약 타입이 같은 매개변수의 초기화 순서가 바뀐다면 문제가 발생할 수 있다.
- final var crew = new Crew(
- "Neo",
- Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
+ /**
+ * 빌더를 통해 매개변수의 의미를 전달할 수 있는 코드입니다.
+ * 빌더를 사용하면 매개변수의 의미를 전달할 수 있고, 빌더를 통해 객체를 생성할 때 매개변수의 순서를 신경쓰지 않아도 됩니다.
+ * 매개변수가 많아지면 어떤 값을 설정했는지 확인이 어렵거나 어떤 매개변수끼리 의미가 있는지 확인이 어려워질 수 있습니다.
+ * 매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?
*/
+ @Test
+ @DisplayName("매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?")
+ void 매개변수를_묶어_의미를_전달할_수_있는_방법은_없을까() {
+ record Taste(
+ Set likeMenuItems,
+ Set dislikeMenuItems
+ ) {
+ static class Builder {
+ private Set likeMenuItems;
+ private Set dislikeMenuItems;
- final var taste = new Taste.Builder()
- .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
- .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- .build();
- final var crew = new Crew.Builder()
- .name("Neo")
- .taste(taste)
- .build();
+ public Builder likeMenuItems(final String... likeMenuItems) {
+ return likeMenuItems(Set.of(likeMenuItems));
+ }
- assertThat(crew.name()).isEqualTo("Neo");
- }
+ public Builder likeMenuItems(final Set likeMenuItems) {
+ this.likeMenuItems = likeMenuItems;
+ return this;
+ }
- /**
- * 매개변수를 묶어 의미를 전달할 수 있는 코드입니다.
- * 의미가 동일한 매개변수를 묶어 별도 클래스로 분리하여 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 원리는 위에서 학습한 메서드 분리, 추상화, 객체의 역할을 명확하게 드러내는 방법과 동일합니다.
- * 의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있습니다.
- * 정답은 없습니다. 적절한 추상화 수준을 유지하는 것이 중요합니다.
- */
- @Test
- @DisplayName("의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다")
- void 의미가_명확해진_장점은_있지만_객체가_많아지면_유지보수성이_떨어질_수_있으니_적절한_추상화_수준을_유지하는_것이_중요합니다() {
- record Taste(
- Set likeMenuItems,
- Set dislikeMenuItems
- ) {
- static class Builder {
- private Set likeMenuItems;
- private Set dislikeMenuItems;
+ public Builder dislikeMenuItems(final String... dislikeMenuItems) {
+ return dislikeMenuItems(Set.of(dislikeMenuItems));
+ }
- public Builder likeMenuItems(final String... likeMenuItems) {
- return likeMenuItems(Set.of(likeMenuItems));
- }
+ public Builder dislikeMenuItems(final Set dislikeMenuItems) {
+ this.dislikeMenuItems = dislikeMenuItems;
+ return this;
+ }
- public Builder likeMenuItems(final Set likeMenuItems) {
- this.likeMenuItems = likeMenuItems;
- return this;
+ public Taste build() {
+ return new Taste(likeMenuItems, dislikeMenuItems);
+ }
}
+ }
- public Builder dislikeMenuItems(final String... dislikeMenuItems) {
- return dislikeMenuItems(Set.of(dislikeMenuItems));
- }
+ record Crew(
+ String name,
+ Taste taste
+ ) {
+ static class Builder {
+ private String name;
+ private Taste taste;
- public Builder dislikeMenuItems(final Set dislikeMenuItems) {
- this.dislikeMenuItems = dislikeMenuItems;
- return this;
- }
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder taste(final Taste taste) {
+ this.taste = taste;
+ return this;
+ }
- public Taste build() {
- return new Taste(likeMenuItems, dislikeMenuItems);
+ public Crew build() {
+ return new Crew(name, taste);
+ }
}
}
+
+ /* Note: 만약 타입이 같은 매개변수의 초기화 순서가 바뀐다면 문제가 발생할 수 있다.
+ final var crew = new Crew(
+ "Neo",
+ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+ */
+
+ final var taste = new Taste.Builder()
+ .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
+ .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ .build();
+ final var crew = new Crew.Builder()
+ .name("Neo")
+ .taste(taste)
+ .build();
+
+ assertThat(crew.name()).isEqualTo("Neo");
}
- record Crew(
- String name,
- Taste taste
- ) {
- static class Builder {
- private String name;
- private Taste taste;
+ /**
+ * 매개변수를 묶어 의미를 전달할 수 있는 코드입니다.
+ * 의미가 동일한 매개변수를 묶어 별도 클래스로 분리하여 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 원리는 위에서 학습한 메서드 분리, 추상화, 객체의 역할을 명확하게 드러내는 방법과 동일합니다.
+ * 의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있습니다.
+ * 정답은 없습니다. 적절한 추상화 수준을 유지하는 것이 중요합니다.
+ */
+ @Test
+ @DisplayName("의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다")
+ void 의미가_명확해진_장점은_있지만_객체가_많아지면_유지보수성이_떨어질_수_있으니_적절한_추상화_수준을_유지하는_것이_중요합니다() {
+ record Taste(
+ Set likeMenuItems,
+ Set dislikeMenuItems
+ ) {
+ static class Builder {
+ private Set likeMenuItems;
+ private Set dislikeMenuItems;
- public Builder name(final String name) {
- this.name = name;
- return this;
- }
+ public Builder likeMenuItems(final String... likeMenuItems) {
+ return likeMenuItems(Set.of(likeMenuItems));
+ }
- public Builder taste(final Taste taste) {
- this.taste = taste;
- return this;
- }
+ public Builder likeMenuItems(final Set likeMenuItems) {
+ this.likeMenuItems = likeMenuItems;
+ return this;
+ }
- public Crew build() {
- return new Crew(name, taste);
+ public Builder dislikeMenuItems(final String... dislikeMenuItems) {
+ return dislikeMenuItems(Set.of(dislikeMenuItems));
+ }
+
+ public Builder dislikeMenuItems(final Set dislikeMenuItems) {
+ this.dislikeMenuItems = dislikeMenuItems;
+ return this;
+ }
+
+ public Taste build() {
+ return new Taste(likeMenuItems, dislikeMenuItems);
+ }
}
}
- }
- final var taste = new Taste.Builder()
- .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
- .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- .build();
- final var crew = new Crew.Builder()
- .name("Neo")
- .taste(taste)
- .build();
+ record Crew(
+ String name,
+ Taste taste
+ ) {
+ static class Builder {
+ private String name;
+ private Taste taste;
- assertThat(crew.name()).isEqualTo("Neo");
- }
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
+ }
- /**
- * 기능을 구현하다 보면 이미 누군가가 구현한 기능을 재사용하고 싶을 때가 있습니다.
- * 대표적으로 자바에서 제공하는 Collection API를 사용하는 것입니다.
- * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?
- */
- @Test
- @DisplayName("Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?")
- void Collection_API를_사용하여_코드를_재사용하고_의도를_파악하기_쉽게_만들_수_없을까() {
- class Menu {
- private final List menuItems;
+ public Builder taste(final Taste taste) {
+ this.taste = taste;
+ return this;
+ }
- public Menu(final List menuItems) {
- if (menuItems.size() != menuItems.stream().distinct().count()) {
- throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
+ public Crew build() {
+ return new Crew(name, taste);
+ }
}
-
- this.menuItems = menuItems;
}
- }
- assertThatThrownBy(() -> {
- new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
- }).hasMessage("중복된 메뉴가 있습니다.");
- }
+ final var taste = new Taste.Builder()
+ .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
+ .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ .build();
+ final var crew = new Crew.Builder()
+ .name("Neo")
+ .taste(taste)
+ .build();
- /**
- * Collection의 distinct를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만든 코드입니다.
- * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 위 객체 분리에서 학습한 것처럼 메서드로 나타내는 것 보다 객체 자체로 나타내는 것이 더 좋은 방법일 수 있습니다.
- * 객체 자체로 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("객체 자체로 의미를 전달할 수 있는 방법은 없을까?")
- void 객체_자체로_의미를_전달할_수_있는_방법은_없을까() {
- class Menu {
- private final Set menuItems;
-
- public Menu(final Set menuItems) {
- this.menuItems = menuItems;
- }
+ assertThat(crew.name()).isEqualTo("Neo");
}
-
- assertThatCode(() -> {
- new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
- }).doesNotThrowAnyException();
}
- /**
- * 객체 자체로 의미를 전달할 수 있는 코드입니다.
- * 자료구조를 활용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * Set 자료구조는 중복을 허용하지 않기 때문에 해당 객체를 생성하는 시점에 중복이 없는 것을 보장할 수 있습니다.
- * 적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다.
- */
- @Test
- @DisplayName("적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다")
- void 적절한_API를_사용하면_견고한_코드를_작성할_수_있습니다() {
- class Menu {
- private final Set menuItems;
-
- public Menu(final Set menuItems) {
- this.menuItems = menuItems;
+ @Nested
+ @DisplayName("적절한 API 활용")
+ class ProperApiUsageTest {
+ /**
+ * 기능을 구현하다 보면 이미 누군가가 구현한 기능을 재사용하고 싶을 때가 있습니다.
+ * 대표적으로 자바에서 제공하는 Collection API를 사용하는 것입니다.
+ * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?
+ */
+ @Test
+ @DisplayName("Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?")
+ void Collection_API를_사용하여_코드를_재사용하고_의도를_파악하기_쉽게_만들_수_없을까() {
+ class Menu {
+ private final List menuItems;
+
+ public Menu(final List menuItems) {
+ if (menuItems.size() != menuItems.stream().distinct().count()) {
+ throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
+ }
+
+ this.menuItems = menuItems;
+ }
}
+
+ assertThatThrownBy(() -> {
+ new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+ }).hasMessage("중복된 메뉴가 있습니다.");
}
- assertThatCode(() -> {
- // Note: List 자료구조를 허용하지 않고 Set 자료구조만 허용하여 중복을 허용하지 않는다는 의도를 전달할 수 있다.
- // new Menu(Set.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+ /**
+ * Collection의 distinct를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만든 코드입니다.
+ * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 위 객체 분리에서 학습한 것처럼 메서드로 나타내는 것 보다 객체 자체로 나타내는 것이 더 좋은 방법일 수 있습니다.
+ * 객체 자체로 의미를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("객체 자체로 의미를 전달할 수 있는 방법은 없을까?")
+ void 객체_자체로_의미를_전달할_수_있는_방법은_없을까() {
+ class Menu {
+ private final Set menuItems;
+
+ public Menu(final Set menuItems) {
+ this.menuItems = menuItems;
+ }
+ }
- new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
- }).doesNotThrowAnyException();
- }
+ assertThatCode(() -> {
+ new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
+ }).doesNotThrowAnyException();
+ }
- /**
- * 적절한 API를 사용하는 것이 견고한 코드를 작성할 수 있다는 사실을 알게 되었습니다.
- * 그렇다고 너무 API를 사용하는 것에 매몰되면 부작용이 발생할 수 있습니다.
- */
- @Test
- @DisplayName("API에 매몰되어 과하게 사용하면 생길 수 있는 문제")
- void API에_매몰되어_과하게_사용하면_생길_수_있는_문제() {
- class Menu {
- private final Map menu;
-
- public Menu(final Map menu) {
- this.menu = menu;
+ /**
+ * 객체 자체로 의미를 전달할 수 있는 코드입니다.
+ * 자료구조를 활용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * Set 자료구조는 중복을 허용하지 않기 때문에 해당 객체를 생성하는 시점에 중복이 없는 것을 보장할 수 있습니다.
+ * 적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다.
+ */
+ @Test
+ @DisplayName("적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다")
+ void 적절한_API를_사용하면_견고한_코드를_작성할_수_있습니다() {
+ class Menu {
+ private final Set menuItems;
+
+ public Menu(final Set menuItems) {
+ this.menuItems = menuItems;
+ }
}
- public int getPrice(final String menuName) {
- return menu.getOrDefault(menuName, 0);
- }
+ assertThatCode(() -> {
+ // Note: List 자료구조를 허용하지 않고 Set 자료구조만 허용하여 중복을 허용하지 않는다는 의도를 전달할 수 있다.
+ // new Menu(Set.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+
+ new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
+ }).doesNotThrowAnyException();
}
- final var menu = new Menu(Map.of(
- "쌈밥", 11_000,
- "김치찌개", 9_000,
- "비빔밥", 10_000,
- "평양냉면", 15_000
- ));
- final int 평양냉면_가격 = menu.getPrice("평양냉면");
+ /**
+ * 적절한 API를 사용하는 것이 견고한 코드를 작성할 수 있다는 사실을 알게 되었습니다.
+ * 그렇다고 너무 API를 사용하는 것에 매몰되면 부작용이 발생할 수 있습니다.
+ */
+ @Test
+ @DisplayName("API에 매몰되어 과하게 사용하면 생길 수 있는 문제")
+ void API에_매몰되어_과하게_사용하면_생길_수_있는_문제() {
+ class Menu {
+ private final Map menu;
+
+ public Menu(final Map menu) {
+ this.menu = menu;
+ }
- assertThat(평양냉면_가격).isEqualTo(15_000);
+ public int getPrice(final String menuName) {
+ return menu.getOrDefault(menuName, 0);
+ }
+ }
+ final var menu = new Menu(Map.of(
+ "쌈밥", 11_000,
+ "김치찌개", 9_000,
+ "비빔밥", 10_000,
+ "평양냉면", 15_000
+ ));
+
+ final int 평양냉면_가격 = menu.getPrice("평양냉면");
+
+ assertThat(평양냉면_가격).isEqualTo(15_000);
+ }
}
}
diff --git a/clean-code/initial/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java b/clean-code/initial/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
index bbcefee..b9c44b4 100644
--- a/clean-code/initial/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
+++ b/clean-code/initial/src/test/java/cholog/goodcode/AvoidMistakeCodeTest.java
@@ -1,6 +1,7 @@
package cholog.goodcode;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -24,562 +25,574 @@
* 유지보수성과 확장성을 위한 실수를 방지하는 코드를 작성하는 방법을 알아봅니다.
*/
public class AvoidMistakeCodeTest {
- /**
- * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
- * 자동차와 위치를 포장하여 응집도를 높이고 유지보수성 및 확장성을 고려한 코드입니다.
- * 아래 코드는 원시값을 포장했지만 같은 위치 객체를 사용하며 의도와 다르게 동작하는 코드입니다.
- * 어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?")
- void 어떻게_같은_위치_객체를_사용할_때_발생할_수_있는_실수를_방지할_수_있을까() {
- // TODO: 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있는 방법을 고민 후 개선해보세요.
- class Position {
- private int value;
-
- Position() {
- this(0);
- }
+ @Nested
+ @DisplayName("가변 객체의 공유 문제와 불변 객체")
+ class ImmutableObjectTest {
+ /**
+ * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
+ * 자동차와 위치를 포장하여 응집도를 높이고 유지보수성 및 확장성을 고려한 코드입니다.
+ * 아래 코드는 원시값을 포장했지만 같은 위치 객체를 사용하며 의도와 다르게 동작하는 코드입니다.
+ * 어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있을까?")
+ void 어떻게_같은_위치_객체를_사용할_때_발생할_수_있는_실수를_방지할_수_있을까() {
+ // TODO: 같은 위치 객체를 사용할 때 발생할 수 있는 실수를 방지할 수 있는 방법을 고민 후 개선해보세요.
+ class Position {
+ private int value;
+
+ Position() {
+ this(0);
+ }
- Position(final int value) {
- this.value = value;
- }
+ Position(final int value) {
+ this.value = value;
+ }
- public void increase() {
- value++;
- }
+ public void increase() {
+ value++;
+ }
- @Override
- public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final Position position = (Position) o;
- return value == position.value;
- }
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Position position = (Position) o;
+ return value == position.value;
+ }
- @Override
- public int hashCode() {
- return Objects.hash(value);
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- }
- record Car(
- String name,
- Position position
- ) {
- public void forward() {
- position.increase();
+ record Car(
+ String name,
+ Position position
+ ) {
+ public void forward() {
+ position.increase();
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- final var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ final var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- neoCar.forward();
+ neoCar.forward();
- // Note: 네오의 자동차만 움직였기 때문에 브라운의 자동차는 움직이지 않아야 한다.
- assertThat(neoCar.position()).isEqualTo(new Position(1));
- assertThat(brownCar.position()).isEqualTo(new Position(0));
- }
+ // Note: 네오의 자동차만 움직였기 때문에 브라운의 자동차는 움직이지 않아야 한다.
+ assertThat(neoCar.position()).isEqualTo(new Position(1));
+ assertThat(brownCar.position()).isEqualTo(new Position(0));
+ }
- /**
- * 원시값을 불변 객체로 만들어 실수를 방지하는 방법입니다.
- * 불변 객체는 객체의 상태를 변경할 수 없기 때문에 객체의 상태를 변경하는 실수를 방지할 수 있습니다.
- * 불변 객체는 변경이 있을 때 마다 새로운 객체를 생성하기 때문에 성능상의 이슈가 발생할 수 있습니다.
- * 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
- *
- * 참고: 불변 객체
- */
- @Test
- @DisplayName("불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
- void 불변_객체를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
- // TODO: 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법을 고민 후 개선해보세요.
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 원시값을 불변 객체로 만들어 실수를 방지하는 방법입니다.
+ * 불변 객체는 객체의 상태를 변경할 수 없기 때문에 객체의 상태를 변경하는 실수를 방지할 수 있습니다.
+ * 불변 객체는 변경이 있을 때 마다 새로운 객체를 생성하기 때문에 성능상의 이슈가 발생할 수 있습니다.
+ * 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
+ *
+ * 참고: 불변 객체
+ */
+ @Test
+ @DisplayName("불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
+ void 불변_객체를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
+ // TODO: 불변 객체를 사용할 때 성능상의 이슈를 해결하는 방법을 고민 후 개선해보세요.
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- record Car(
- String name,
- Position position
- ) {
- public Car forward() {
- return new Car(name, position.increase());
+ record Car(
+ String name,
+ Position position
+ ) {
+ public Car forward() {
+ return new Car(name, position.increase());
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- // Note: Car 객체가 불변 객체가 되면서 위치가 이동될 때 마다 새로운 객체가 생성된다.
- neoCar = neoCar.forward();
+ // Note: Car 객체가 불변 객체가 되면서 위치가 이동될 때 마다 새로운 객체가 생성된다.
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(new Position(1));
- assertThat(brownCar.position()).isEqualTo(new Position(0));
+ assertThat(neoCar.position()).isEqualTo(new Position(1));
+ assertThat(brownCar.position()).isEqualTo(new Position(0));
+ }
}
- /**
- * 정적 팩터리 메서드를 만들고 내부에서 캐싱하여 성능상의 이슈를 해결하는 방법입니다.
- * 객체를 재활용할 경우 매번 새로운 객체가 생성되지 않기 때문에 성능상의 이슈를 해결할 수 있습니다.
- * 하지만 지금의 방법은 캐싱되는 객체가 많을수록 메모리 사용량이 증가할 수 있습니다.
- * 메모리 사용량을 최소화하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
- void 메모리_사용량을_최소화하는_방법은_무엇일까() {
- // TODO: 메모리 사용량을 최소화하는 방법을 고민 후 개선해보세요.
- record Position(int value) {
- private static final Map CACHE = new ConcurrentHashMap<>();
-
- public static Position startingPoint() {
- return valueOf(0);
- }
+ @Nested
+ @DisplayName("캐싱 전략과 성능")
+ class CachingStrategyTest {
+ /**
+ * 정적 팩터리 메서드를 만들고 내부에서 캐싱하여 성능상의 이슈를 해결하는 방법입니다.
+ * 객체를 재활용할 경우 매번 새로운 객체가 생성되지 않기 때문에 성능상의 이슈를 해결할 수 있습니다.
+ * 하지만 지금의 방법은 캐싱되는 객체가 많을수록 메모리 사용량이 증가할 수 있습니다.
+ * 메모리 사용량을 최소화하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
+ void 메모리_사용량을_최소화하는_방법은_무엇일까() {
+ // TODO: 메모리 사용량을 최소화하는 방법을 고민 후 개선해보세요.
+ record Position(int value) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
+
+ public static Position startingPoint() {
+ return valueOf(0);
+ }
- public static Position valueOf(final int value) {
- return CACHE.computeIfAbsent(value, Position::new);
- }
+ public static Position valueOf(final int value) {
+ return CACHE.computeIfAbsent(value, Position::new);
+ }
- public Position increase() {
- return valueOf(value + 1);
+ public Position increase() {
+ return valueOf(value + 1);
+ }
}
- }
- record Car(
- String name,
- Position position
- ) {
- private static final Map CACHE = new ConcurrentHashMap<>();
+ record Car(
+ String name,
+ Position position
+ ) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
- public static Car of(final String name, final Position position) {
- return CACHE.computeIfAbsent(toKey(name, position), key -> new Car(key, position));
- }
+ public static Car of(final String name, final Position position) {
+ return CACHE.computeIfAbsent(toKey(name, position), key -> new Car(key, position));
+ }
- private static String toKey(final String name, final Position position) {
- return name + position.value();
- }
+ private static String toKey(final String name, final Position position) {
+ return name + position.value();
+ }
- public Car forward() {
- // Note: 움직일 때 마다 캐싱된 객체가 재활용된다. 하지만 캐싱된 객체가 많을수록 메모리 사용량이 증가한다.
- return Car.of(name, position.increase());
+ public Car forward() {
+ // Note: 움직일 때 마다 캐싱된 객체가 재활용된다. 하지만 캐싱된 객체가 많을수록 메모리 사용량이 증가한다.
+ return Car.of(name, position.increase());
+ }
}
- }
- final var position = Position.startingPoint();
+ final var position = Position.startingPoint();
- var neoCar = Car.of("네오", position);
- var brownCar = Car.of("브라운", position);
+ var neoCar = Car.of("네오", position);
+ var brownCar = Car.of("브라운", position);
- neoCar = neoCar.forward();
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(Position.valueOf(1));
- assertThat(brownCar.position()).isEqualTo(Position.valueOf(0));
- }
+ assertThat(neoCar.position()).isEqualTo(Position.valueOf(1));
+ assertThat(brownCar.position()).isEqualTo(Position.valueOf(0));
+ }
- /**
- * 자주 사용될 객체만 캐싱하는 방법입니다.
- * 자주 사용되는 객체만 캐싱할 경우 메모리 사용량을 최소화할 수 있습니다.
- * 어떤 객체를 캐싱할지 계산하는 것도 비용이 들고, 객체 그래프가 복잡할 경우 캐싱하는 것도 복잡해질 수 있습니다.
- * 또한 JVM의 경우 GC의 성능이 우리가 생각하는 것 보다 훨씬 더 좋기 때문에 캐싱을 하지 않는 것이 더 좋을 수도 있습니다.
- * 객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?")
- void 객체_그래프가_깊을_때_문제를_해결하는_방법은_무엇일까() {
- // TODO: 객체 그래프가 깊을 때 문제를 해결하는 방법을 고민 후 개선해보세요.
- record PositionForEnhancedCache(int value) {
- private static final int CACHE_MIN = 0;
- private static final int CACHE_MAX = 5;
- private static final Map CACHE = IntStream.range(CACHE_MIN, CACHE_MAX)
- .boxed()
- .collect(toMap(identity(), PositionForEnhancedCache::new));
-
- public static PositionForEnhancedCache startingPoint() {
- return valueOf(0);
- }
+ /**
+ * 자주 사용될 객체만 캐싱하는 방법입니다.
+ * 자주 사용되는 객체만 캐싱할 경우 메모리 사용량을 최소화할 수 있습니다.
+ * 어떤 객체를 캐싱할지 계산하는 것도 비용이 들고, 객체 그래프가 복잡할 경우 캐싱하는 것도 복잡해질 수 있습니다.
+ * 또한 JVM의 경우 GC의 성능이 우리가 생각하는 것 보다 훨씬 더 좋기 때문에 캐싱을 하지 않는 것이 더 좋을 수도 있습니다.
+ * 객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("객체 그래프가 깊을 때 문제를 해결하는 방법은 무엇일까?")
+ void 객체_그래프가_깊을_때_문제를_해결하는_방법은_무엇일까() {
+ // TODO: 객체 그래프가 깊을 때 문제를 해결하는 방법을 고민 후 개선해보세요.
+ record PositionForEnhancedCache(int value) {
+ private static final int CACHE_MIN = 0;
+ private static final int CACHE_MAX = 5;
+ private static final Map CACHE = IntStream.range(CACHE_MIN, CACHE_MAX)
+ .boxed()
+ .collect(toMap(identity(), PositionForEnhancedCache::new));
+
+ public static PositionForEnhancedCache startingPoint() {
+ return valueOf(0);
+ }
- public static PositionForEnhancedCache valueOf(final int value) {
- // Note: 자주 사용되는 객체만 캐싱하여 메모리 사용량을 최소화한다.
- if (CACHE_MIN <= value && value <= CACHE_MAX) {
- return CACHE.get(value);
+ public static PositionForEnhancedCache valueOf(final int value) {
+ // Note: 자주 사용되는 객체만 캐싱하여 메모리 사용량을 최소화한다.
+ if (CACHE_MIN <= value && value <= CACHE_MAX) {
+ return CACHE.get(value);
+ }
+ return new PositionForEnhancedCache(value);
}
- return new PositionForEnhancedCache(value);
- }
- public PositionForEnhancedCache increase() {
- return valueOf(value + 1);
+ public PositionForEnhancedCache increase() {
+ return valueOf(value + 1);
+ }
}
- }
- record CarForEnhancedCache(
- String name,
- PositionForEnhancedCache position
- ) {
- private static final Map CACHE = new ConcurrentHashMap<>();
+ record CarForEnhancedCache(
+ String name,
+ PositionForEnhancedCache position
+ ) {
+ private static final Map CACHE = new ConcurrentHashMap<>();
- public static CarForEnhancedCache of(final String name, final PositionForEnhancedCache position) {
- return CACHE.computeIfAbsent(toKey(name, position), key -> new CarForEnhancedCache(key, position));
- }
+ public static CarForEnhancedCache of(final String name, final PositionForEnhancedCache position) {
+ return CACHE.computeIfAbsent(toKey(name, position), key -> new CarForEnhancedCache(key, position));
+ }
- private static String toKey(final String name, final PositionForEnhancedCache position) {
- return name + position.value();
- }
+ private static String toKey(final String name, final PositionForEnhancedCache position) {
+ return name + position.value();
+ }
- public CarForEnhancedCache forward() {
- return CarForEnhancedCache.of(name, position.increase());
+ public CarForEnhancedCache forward() {
+ return CarForEnhancedCache.of(name, position.increase());
+ }
}
- }
- final var position = PositionForEnhancedCache.startingPoint();
+ final var position = PositionForEnhancedCache.startingPoint();
- var neoCar = CarForEnhancedCache.of("네오", position);
- var brownCar = CarForEnhancedCache.of("브라운", position);
+ var neoCar = CarForEnhancedCache.of("네오", position);
+ var brownCar = CarForEnhancedCache.of("브라운", position);
- neoCar = neoCar.forward();
+ neoCar = neoCar.forward();
- assertThat(neoCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(1));
- assertThat(brownCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(0));
- }
+ assertThat(neoCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(1));
+ assertThat(brownCar.position()).isEqualTo(PositionForEnhancedCache.valueOf(0));
+ }
- /**
- * Car 내부에서 Position 객체를 변경하는 방법입니다.
- * 적정한 시점까지만 불변 객체를 사용하면 불변 객체의 장점을 살리면서 성능상의 이슈를 해결할 수 있습니다.
- * 성능적인 이점만 존재하는 것은 아닙니다. 불변 객체의 장점을 살리면서 가변 객체의 장점도 살릴 수 있습니다.
- * 동일한 컨텍스트에서만 불변 객체를 사용하고, 다른 컨텍스트에서 사용될 수 있는 지점에선 가변 객체처럼 사용하게 하는 것이 좋습니다.
- * 하지만 이 기준은 상황과 설계에 따라 달라질 수 있습니다.
- * 구현할 때 가변 객체로도 구현해보고, 불변 객체로도 구현해보면서 어떠한 방법이 더 좋은지 판단해보는 것이 좋습니다.
- */
- @Test
- @DisplayName("적정한 시점까지만 불변 객체를 사용한다.")
- void 적정한_시점까지만_불변_객체를_사용한다() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * Car 내부에서 Position 객체를 변경하는 방법입니다.
+ * 적정한 시점까지만 불변 객체를 사용하면 불변 객체의 장점을 살리면서 성능상의 이슈를 해결할 수 있습니다.
+ * 성능적인 이점만 존재하는 것은 아닙니다. 불변 객체의 장점을 살리면서 가변 객체의 장점도 살릴 수 있습니다.
+ * 동일한 컨텍스트에서만 불변 객체를 사용하고, 다른 컨텍스트에서 사용될 수 있는 지점에선 가변 객체처럼 사용하게 하는 것이 좋습니다.
+ * 하지만 이 기준은 상황과 설계에 따라 달라질 수 있습니다.
+ * 구현할 때 가변 객체로도 구현해보고, 불변 객체로도 구현해보면서 어떠한 방법이 더 좋은지 판단해보는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("적정한 시점까지만 불변 객체를 사용한다.")
+ void 적정한_시점까지만_불변_객체를_사용한다() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public Position getPosition() {
- return position;
+ public Position getPosition() {
+ return position;
+ }
}
- }
- final var position = new Position();
+ final var position = new Position();
- final var neoCar = new Car("네오", position);
- final var brownCar = new Car("브라운", position);
+ final var neoCar = new Car("네오", position);
+ final var brownCar = new Car("브라운", position);
- neoCar.forward();
+ neoCar.forward();
- assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
- assertThat(brownCar.getPosition()).isEqualTo(new Position());
+ assertThat(neoCar.getPosition()).isEqualTo(new Position(1));
+ assertThat(brownCar.getPosition()).isEqualTo(new Position());
+ }
}
- /**
- * 아래 코드는 우승한 자동차들을 구하는 코드입니다.
- * 자동차 경주 객체 내부에 자동차 객체들이 존재하고 있고, 외부에서 조작할 수 있는 위험이 존재하고 있습니다.
- * 객체의 상태를 변경할 수 있는 위험은 실수로 인한 버그를 발생시킬 수 있습니다.
- * 객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?")
- void 객체의_상태를_변경할_수_있는_위험을_방지하는_방법은_무엇일까() {
- // TODO: 객체의 상태를 변경할 수 있는 위험을 방지하는 방법을 고민 후 개선해보세요.
- record Position(int value) {
- Position() {
- this(0);
- }
+ @Nested
+ @DisplayName("방어적 복사와 불변 컬렉션")
+ class DefensiveCopyTest {
+ /**
+ * 아래 코드는 우승한 자동차들을 구하는 코드입니다.
+ * 자동차 경주 객체 내부에 자동차 객체들이 존재하고 있고, 외부에서 조작할 수 있는 위험이 존재하고 있습니다.
+ * 객체의 상태를 변경할 수 있는 위험은 실수로 인한 버그를 발생시킬 수 있습니다.
+ * 객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("객체의 상태를 변경할 수 있는 위험을 방지하는 방법은 무엇일까?")
+ void 객체의_상태를_변경할_수_있는_위험을_방지하는_방법은_무엇일까() {
+ // TODO: 객체의 상태를 변경할 수 있는 위험을 방지하는 방법을 고민 후 개선해보세요.
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- return participants;
+ List getParticipants() {
+ return participants;
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- // Note: 외부에서 조작할 수 있는 위험이 존재하고 있다.
- participants.add(new Car("브리", new Position(2)));
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- }
+ // Note: 외부에서 조작할 수 있는 위험이 존재하고 있다.
+ participants.add(new Car("브리", new Position(2)));
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ }
- /**
- * 방어적 복사를 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
- * 하지만 방어적 복사를 사용할 경우 객체의 상태를 변경할 수 있는 위험을 방지할 수 있지만 성능상의 이슈가 발생할 수 있습니다.
- * 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
- void 방어적_복사를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
- // TODO: 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법을 고민 후 개선해보세요.
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 방어적 복사를 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
+ * 하지만 방어적 복사를 사용할 경우 객체의 상태를 변경할 수 있는 위험을 방지할 수 있지만 성능상의 이슈가 발생할 수 있습니다.
+ * 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법은 무엇일까?")
+ void 방어적_복사를_사용할_때_성능상의_이슈를_해결하는_방법은_무엇일까() {
+ // TODO: 방어적 복사를 사용할 때 성능상의 이슈를 해결하는 방법을 고민 후 개선해보세요.
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = new ArrayList<>(participants);
- }
+ RacingGame(final List participants) {
+ this.participants = new ArrayList<>(participants);
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- // Note: 매번 새로운 리스트를 생성하여 성능상의 이슈가 발생할 수 있다.
- return new ArrayList<>(participants);
+ List getParticipants() {
+ // Note: 매번 새로운 리스트를 생성하여 성능상의 이슈가 발생할 수 있다.
+ return new ArrayList<>(participants);
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- participants.add(new Car("브리", new Position(2)));
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- }
+ participants.add(new Car("브리", new Position(2)));
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ }
- /**
- * 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
- * 응답하는 컬렉션을 불변 컬렉션으로 만들어 객체의 상태를 변경할 수 있는 위험을 방지할 수 있습니다.
- * 입력을 받는 컬렉션을 불변 컬렉션을 만드는 것은 그대로 외부에서 조작할 수 있는 위험이 존재합니다.
- * 따라서 입력받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋습니다.
- */
- @Test
- @DisplayName("입력을 받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋다.")
- void 입력을_받는_컬렉션은_방어적_복사로_응답하는_컬렉션은_불변_컬렉션으로_만드는_것이_좋다() {
- record Position(int value) {
- Position() {
- this(0);
- }
+ /**
+ * 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지하는 방법입니다.
+ * 응답하는 컬렉션을 불변 컬렉션으로 만들어 객체의 상태를 변경할 수 있는 위험을 방지할 수 있습니다.
+ * 입력을 받는 컬렉션을 불변 컬렉션을 만드는 것은 그대로 외부에서 조작할 수 있는 위험이 존재합니다.
+ * 따라서 입력받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("입력을 받는 컬렉션은 방어적 복사로, 응답하는 컬렉션은 불변 컬렉션으로 만드는 것이 좋다.")
+ void 입력을_받는_컬렉션은_방어적_복사로_응답하는_컬렉션은_불변_컬렉션으로_만드는_것이_좋다() {
+ record Position(int value) {
+ Position() {
+ this(0);
+ }
- public Position increase() {
- return new Position(value + 1);
+ public Position increase() {
+ return new Position(value + 1);
+ }
}
- }
- class Car {
- private final String name;
- private Position position;
+ class Car {
+ private final String name;
+ private Position position;
- Car(final String name, final Position position) {
- this.name = name;
- this.position = position;
- }
+ Car(final String name, final Position position) {
+ this.name = name;
+ this.position = position;
+ }
- public void forward() {
- position = position.increase();
- }
+ public void forward() {
+ position = position.increase();
+ }
- public boolean matchPosition(final Position position) {
- return this.position.equals(position);
- }
+ public boolean matchPosition(final Position position) {
+ return this.position.equals(position);
+ }
- public Position getPosition() {
- return position;
- }
+ public Position getPosition() {
+ return position;
+ }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- ", position=" + position +
- '}';
+ @Override
+ public String toString() {
+ return "Car{" +
+ "name='" + name + '\'' +
+ ", position=" + position +
+ '}';
+ }
}
- }
- class RacingGame {
- private final List participants;
+ class RacingGame {
+ private final List participants;
- RacingGame(final List participants) {
- this.participants = new ArrayList<>(participants);
- }
+ RacingGame(final List participants) {
+ this.participants = new ArrayList<>(participants);
+ }
- public List selectWinners() {
- return matchCarsByPosition(calculateWinnerPosition());
- }
+ public List selectWinners() {
+ return matchCarsByPosition(calculateWinnerPosition());
+ }
- private Position calculateWinnerPosition() {
- return participants.stream()
- .map(Car::getPosition)
- .max(Comparator.comparingInt(Position::value))
- .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
- }
+ private Position calculateWinnerPosition() {
+ return participants.stream()
+ .map(Car::getPosition)
+ .max(Comparator.comparingInt(Position::value))
+ .orElseThrow(() -> new IllegalStateException("참가자가 없습니다."));
+ }
- private List matchCarsByPosition(final Position position) {
- return participants.stream()
- .filter(car -> car.matchPosition(position))
- .toList();
- }
+ private List matchCarsByPosition(final Position position) {
+ return participants.stream()
+ .filter(car -> car.matchPosition(position))
+ .toList();
+ }
- List getParticipants() {
- return unmodifiableList(participants);
+ List getParticipants() {
+ return unmodifiableList(participants);
+ }
}
- }
- final var neoCar = new Car("네오", new Position());
- final var brownCar = new Car("브라운", new Position(1));
- final var participants = new ArrayList<>(List.of(neoCar, brownCar));
- final var racingGame = new RacingGame(participants);
+ final var neoCar = new Car("네오", new Position());
+ final var brownCar = new Car("브라운", new Position(1));
+ final var participants = new ArrayList<>(List.of(neoCar, brownCar));
+ final var racingGame = new RacingGame(participants);
- final var winners = racingGame.selectWinners();
- assertThat(winners).containsExactly(brownCar);
+ final var winners = racingGame.selectWinners();
+ assertThat(winners).containsExactly(brownCar);
- participants.add(new Car("브리", new Position(2)));
- assertThat(winners).containsExactly(brownCar);
- assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
+ participants.add(new Car("브리", new Position(2)));
+ assertThat(winners).containsExactly(brownCar);
+ assertThat(racingGame.getParticipants()).containsExactlyElementsOf(List.of(neoCar, brownCar));
- // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
- assertThatThrownBy(() -> {
- racingGame.getParticipants().add(new Car("솔라", new Position(3)));
- }).isInstanceOf(UnsupportedOperationException.class);
+ // Note: 불변 컬렉션을 사용하여 객체의 상태를 변경할 수 있는 위험을 방지할 수 있다.
+ assertThatThrownBy(() -> {
+ racingGame.getParticipants().add(new Car("솔라", new Position(3)));
+ }).isInstanceOf(UnsupportedOperationException.class);
+ }
}
}
diff --git a/clean-code/initial/src/test/java/cholog/goodcode/PredictableCodeTest.java b/clean-code/initial/src/test/java/cholog/goodcode/PredictableCodeTest.java
index fd0eb0f..74d9e6a 100644
--- a/clean-code/initial/src/test/java/cholog/goodcode/PredictableCodeTest.java
+++ b/clean-code/initial/src/test/java/cholog/goodcode/PredictableCodeTest.java
@@ -1,6 +1,7 @@
package cholog.goodcode;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -23,466 +24,482 @@ public class PredictableCodeTest {
record Car(int position) {
}
- /**
- * 아래 코드는 자동차 경주에서 평균 위치를 계산하는 기능입니다.
- * 게임 참여자가 없을 경우 -1을 반환하여 처리하면 해당 기능을 사용하는 개발자들은 매번 -1을 체크해야 하고, 이는 실수하기 좋은 코드가 됩니다.
- * 어떻게 매번 -1를 체크하지 않도록 할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 매번 -1를 체크하지 않도록 할 수 있을까?")
- void 어떻게_매번_값을_체크하지_않도록_할_수_있을까() {
- // TODO: 매번 -1을 체크하지 않고 참여자가 없다는 것을 명시적으로 표현할 수 있는 코드를 작성해보세요.
- class RacingGame {
- private static final int NO_PARTICIPANT = -1;
-
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ @Nested
+ @DisplayName("반환값으로 상태 전달하기")
+ class ReturnValueTest {
+ /**
+ * 아래 코드는 자동차 경주에서 평균 위치를 계산하는 기능입니다.
+ * 게임 참여자가 없을 경우 -1을 반환하여 처리하면 해당 기능을 사용하는 개발자들은 매번 -1을 체크해야 하고, 이는 실수하기 좋은 코드가 됩니다.
+ * 어떻게 매번 -1를 체크하지 않도록 할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 매번 -1를 체크하지 않도록 할 수 있을까?")
+ void 어떻게_매번_값을_체크하지_않도록_할_수_있을까() {
+ // TODO: 매번 -1을 체크하지 않고 참여자가 없다는 것을 명시적으로 표현할 수 있는 코드를 작성해보세요.
+ class RacingGame {
+ private static final int NO_PARTICIPANT = -1;
+
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- int averagePosition() {
- // Note: 매직값은 버그를 유발할 수 있다.
- return (int) participants.stream()
- .mapToInt(Car::position)
- .average()
- .orElse(NO_PARTICIPANT);
+ int averagePosition() {
+ // Note: 매직값은 버그를 유발할 수 있다.
+ return (int) participants.stream()
+ .mapToInt(Car::position)
+ .average()
+ .orElse(NO_PARTICIPANT);
+ }
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- final var averagePosition = racingGame.averagePosition();
+ final var averagePosition = racingGame.averagePosition();
- assertThat(averagePosition).isEqualTo(RacingGame.NO_PARTICIPANT);
- }
+ assertThat(averagePosition).isEqualTo(RacingGame.NO_PARTICIPANT);
+ }
- /**
- * null을 통해 의도를 전달하는 방법입니다.
- * null을 사용하면 코드를 읽는 사람이 해당 변수가 null일 수 있다는 것을 알 수 있습니다.
- * 하지만 null을 사용하면 NullPointerException이 발생할 수 있고, null로 인해 생길 수 있는 부작용이 발생할 수 있습니다.
- * null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?
- */
- @Test
- @DisplayName("null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?")
- void null을_사용하지_않고_의도를_전달하는_방법은_무엇일까() {
- // TODO: null을 사용하지 않고 참여자가 없다는 것을 명시적으로 표현할 수 있는 코드를 작성해보세요.
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * null을 통해 의도를 전달하는 방법입니다.
+ * null을 사용하면 코드를 읽는 사람이 해당 변수가 null일 수 있다는 것을 알 수 있습니다.
+ * 하지만 null을 사용하면 NullPointerException이 발생할 수 있고, null로 인해 생길 수 있는 부작용이 발생할 수 있습니다.
+ * null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?
+ */
+ @Test
+ @DisplayName("null을 사용하지 않고 의도를 전달하는 방법은 무엇일까?")
+ void null을_사용하지_않고_의도를_전달하는_방법은_무엇일까() {
+ // TODO: null을 사용하지 않고 참여자가 없다는 것을 명시적으로 표현할 수 있는 코드를 작성해보세요.
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- Integer averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ Integer averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- // Note: null은 버그를 유발할 수 있다.
- return null;
+ if (average.isEmpty()) {
+ // Note: null은 버그를 유발할 수 있다.
+ return null;
+ }
+ return (int) average.getAsDouble();
}
- return (int) average.getAsDouble();
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- final var averagePosition = racingGame.averagePosition();
+ final var averagePosition = racingGame.averagePosition();
- assertThat(averagePosition).isNull();
- }
+ assertThat(averagePosition).isNull();
+ }
- /**
- * Optional을 통해 의도를 전달하는 방법입니다.
- * Optional을 사용하면 코드를 읽는 사람이 해당 변수가 비어있을 수 있다는 것을 알 수 있습니다.
- * 지금의 구조는 참여자가 없다는 사실을 전달할 수 있지만, 그 처리를 외부에 위임하고 있습니다.
- * 만약 참여자가 없다는 사실을 처리하는 코드가 여러군데에 중복되어 있다면, 이는 유지보수성을 떨어뜨리는 코드가 됩니다.
- * 어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?")
- void 어떻게_참여자가_없는_상황을_처리하는_코드를_중복하지_않고_처리할_수_있을까() {
- // TODO: 참여자가 없는 상황을 중복하지 않고 처리할 수 있는 코드를 작성해보세요.
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * Optional을 통해 의도를 전달하는 방법입니다.
+ * Optional을 사용하면 코드를 읽는 사람이 해당 변수가 비어있을 수 있다는 것을 알 수 있습니다.
+ * 지금의 구조는 참여자가 없다는 사실을 전달할 수 있지만, 그 처리를 외부에 위임하고 있습니다.
+ * 만약 참여자가 없다는 사실을 처리하는 코드가 여러군데에 중복되어 있다면, 이는 유지보수성을 떨어뜨리는 코드가 됩니다.
+ * 어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 참여자가 없는 상황을 처리하는 코드를 중복하지 않고 처리할 수 있을까?")
+ void 어떻게_참여자가_없는_상황을_처리하는_코드를_중복하지_않고_처리할_수_있을까() {
+ // TODO: 참여자가 없는 상황을 중복하지 않고 처리할 수 있는 코드를 작성해보세요.
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- // Note: Optional를 사용하면 외부에 처리를 위임하게 되고, 응집도가 떨어질 수 있다.
- Optional averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ // Note: Optional를 사용하면 외부에 처리를 위임하게 되고, 응집도가 떨어질 수 있다.
+ Optional averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- return Optional.empty();
+ if (average.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of((int) average.getAsDouble());
}
- return Optional.of((int) average.getAsDouble());
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- final var averagePosition = racingGame.averagePosition();
+ final var averagePosition = racingGame.averagePosition();
- assertThat(averagePosition).isEmpty();
- }
+ assertThat(averagePosition).isEmpty();
+ }
- /**
- * 예외를 발생하여 명시적으로 처리하는 방법입니다.
- * 논리적으로 참여자가 없다는 사실을 처리하는 코드를 중복하지 않고 처리할 수 있습니다.
- * 설계에 따라 외부에서 처리하는 것이 적합할 경우 Optional을 사용할 수 있지만, 설계에 따라 예외를 발생하는 것이 적합할 수 있습니다.
- * 정답은 없습니다. 상황에 따라 적절한 방법을 선택해야 합니다.
- */
- @Test
- @DisplayName("예외를 발생하여 명시적으로 처리하는 방법입니다.")
- void 예외를_발생하여_명시적으로_처리하는_방법입니다() {
- class RacingGame {
- private final List participants;
-
- RacingGame() {
- this(List.of());
- }
+ /**
+ * 예외를 발생하여 명시적으로 처리하는 방법입니다.
+ * 논리적으로 참여자가 없다는 사실을 처리하는 코드를 중복하지 않고 처리할 수 있습니다.
+ * 설계에 따라 외부에서 처리하는 것이 적합할 경우 Optional을 사용할 수 있지만, 설계에 따라 예외를 발생하는 것이 적합할 수 있습니다.
+ * 정답은 없습니다. 상황에 따라 적절한 방법을 선택해야 합니다.
+ */
+ @Test
+ @DisplayName("예외를 발생하여 명시적으로 처리하는 방법입니다.")
+ void 예외를_발생하여_명시적으로_처리하는_방법입니다() {
+ class RacingGame {
+ private final List participants;
+
+ RacingGame() {
+ this(List.of());
+ }
- RacingGame(final List participants) {
- this.participants = participants;
- }
+ RacingGame(final List participants) {
+ this.participants = participants;
+ }
- // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
- int averagePosition() {
- final OptionalDouble average = participants.stream()
- .mapToInt(Car::position)
- .average();
+ // Note: 내부에서 예외를 처리하면 확장성이 떨어질 수 있다.
+ int averagePosition() {
+ final OptionalDouble average = participants.stream()
+ .mapToInt(Car::position)
+ .average();
- if (average.isEmpty()) {
- throw new IllegalStateException("게임 참여자가 없습니다.");
+ if (average.isEmpty()) {
+ throw new IllegalStateException("게임 참여자가 없습니다.");
+ }
+ return (int) average.getAsDouble();
}
- return (int) average.getAsDouble();
}
- }
- final var racingGame = new RacingGame();
+ final var racingGame = new RacingGame();
- assertThatThrownBy(() -> {
- racingGame.averagePosition();
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("게임 참여자가 없습니다.");
+ assertThatThrownBy(() -> {
+ racingGame.averagePosition();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("게임 참여자가 없습니다.");
+ }
}
- /**
- * 아래 코드는 4 이상의 파워가 넘어왔을 때 자동차를 움직이는 기능입니다.
- * 자동차의 위치를 조회하는 것 또한 해당 메서드를 통해 조회하고 있습니다.
- * 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선한다.")
- void 자동차_이동과_조회를_같이_할_경우_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- // TODO: 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
- int move(final int power) {
- if (power <= 4) {
- return position;
+ @Nested
+ @DisplayName("Command-Query Separation")
+ class CommandQuerySeparationTest {
+ /**
+ * 아래 코드는 4 이상의 파워가 넘어왔을 때 자동차를 움직이는 기능입니다.
+ * 자동차의 위치를 조회하는 것 또한 해당 메서드를 통해 조회하고 있습니다.
+ * 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 자동차_이동과_조회를_같이_할_경우_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ // TODO: 자동차 이동과 조회를 같이 할 경우 어떠한 문제가 있을지 고민 후 개선해보세요.
+ int move(final int power) {
+ if (power <= 4) {
+ return position;
+ }
+
+ return ++position;
}
-
- return ++position;
}
- }
- final var car = new Car();
+ final var car = new Car();
- final var position = car.move(5);
+ final var position = car.move(5);
- assertThat(position).isEqualTo(1);
+ assertThat(position).isEqualTo(1);
+ }
}
- /**
- * 아래 코드는 자동차가 최대 5칸을 움직일 수 있는 코드입니다.
- * 5칸에 위치하였을 때 더 이동하려고 하면 더 이상 움직이지 않고 위치를 유지하고 있습니다.
- * 아래 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
- void 자동차가_최대_위치에서_움직이지_않고_유지하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- Car(final int position) {
- this.position = position;
- }
-
- // TODO: 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- void move(final int power) {
- if (power <= 4) {
- return;
- }
- if (position >= 5) {
- return;
+ @Nested
+ @DisplayName("동작을 무시하지 않기")
+ class DoNotIgnoreActionTest {
+ /**
+ * 아래 코드는 자동차가 최대 5칸을 움직일 수 있는 코드입니다.
+ * 5칸에 위치하였을 때 더 이동하려고 하면 더 이상 움직이지 않고 위치를 유지하고 있습니다.
+ * 아래 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 자동차가_최대_위치에서_움직이지_않고_유지하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ Car(final int position) {
+ this.position = position;
}
- position++;
- }
+ // TODO: 자동차가 최대 위치에서 움직이지 않고 유지하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ void move(final int power) {
+ if (power <= 4) {
+ return;
+ }
+ if (position >= 5) {
+ return;
+ }
- int getPosition() {
- return position;
- }
- }
+ position++;
+ }
- final var car = new Car(5);
+ int getPosition() {
+ return position;
+ }
+ }
- car.move(5);
+ final var car = new Car(5);
- assertThat(car.getPosition()).isEqualTo(5);
- }
+ car.move(5);
- /**
- * 더 이상 움직일 수 없을 때 예외를 발생하여 명시적으로 처리하는 방법입니다.
- * 중요한 동작을 무시하는 것은 버그를 유발할 수 있습니다.
- * 하지만 아직 파워가 4보다 작을 때는 무시하고 있습니다.
- * 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
- void 파워가_4보다_작을_때_무시하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
- class Car {
- private int position;
-
- Car(final int position) {
- this.position = position;
- }
+ assertThat(car.getPosition()).isEqualTo(5);
+ }
- void move(final int power) {
- // TODO: 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
- if (power <= 4) {
- return;
- }
- if (position >= 5) {
- // Note: 중요한 동작을 무시하는 것은 버그를 유발할 수 있다.
- throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ /**
+ * 더 이상 움직일 수 없을 때 예외를 발생하여 명시적으로 처리하는 방법입니다.
+ * 중요한 동작을 무시하는 것은 버그를 유발할 수 있습니다.
+ * 하지만 아직 파워가 4보다 작을 때는 무시하고 있습니다.
+ * 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 파워가_4보다_작을_때_무시하는_코드는_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Car {
+ private int position;
+
+ Car(final int position) {
+ this.position = position;
}
- position++;
+ void move(final int power) {
+ // TODO: 파워가 4보다 작을 때 무시하는 코드는 어떠한 문제가 있을지 고민 후 개선해보세요.
+ if (power <= 4) {
+ return;
+ }
+ if (position >= 5) {
+ // Note: 중요한 동작을 무시하는 것은 버그를 유발할 수 있다.
+ throw new IllegalStateException("더 이상 움직일 수 없습니다.");
+ }
+
+ position++;
+ }
}
- }
- final var car = new Car(5);
+ final var car = new Car(5);
- assertThatThrownBy(() -> {
- car.move(5);
- }).isInstanceOf(IllegalStateException.class)
- .hasMessage("더 이상 움직일 수 없습니다.");
+ assertThatThrownBy(() -> {
+ car.move(5);
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessage("더 이상 움직일 수 없습니다.");
+ }
}
- /**
- * 아래 코드는 입력된 문자열 명령에 따라 동작하는 코드입니다.
- * 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
- */
- @Test
- @DisplayName("문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선한다.")
- void 문자열로_명령을_받는_것은_어떠한_문제가_있을지_고민_후_개선한다() {
- class Calculator {
- private static final String PLUS = "PLUS";
- private static final String MINUS = "MINUS";
-
- // TODO: 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
- public static int calculate(
- final String command,
- final int left,
- final int right
- ) {
- if (PLUS.equals(command)) {
- return left + right;
+ @Nested
+ @DisplayName("타입 안전성")
+ class TypeSafetyTest {
+ /**
+ * 아래 코드는 입력된 문자열 명령에 따라 동작하는 코드입니다.
+ * 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
+ */
+ @Test
+ @DisplayName("문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선한다.")
+ void 문자열로_명령을_받는_것은_어떠한_문제가_있을지_고민_후_개선한다() {
+ class Calculator {
+ private static final String PLUS = "PLUS";
+ private static final String MINUS = "MINUS";
+
+ // TODO: 문자열로 명령을 받는 것은 어떠한 문제가 있을지 고민 후 개선해보세요.
+ public static int calculate(
+ final String command,
+ final int left,
+ final int right
+ ) {
+ if (PLUS.equals(command)) {
+ return left + right;
+ }
+ if (MINUS.equals(command)) {
+ return left - right;
+ }
+
+ throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
}
- if (MINUS.equals(command)) {
- return left - right;
- }
-
- throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
}
- }
-
- assertAll(
- () -> assertThat(Calculator.calculate("PLUS", 1, 2)).isEqualTo(3),
- () -> assertThat(Calculator.calculate("MINUS", 1, 2)).isEqualTo(-1)
- );
- }
- /**
- * 명령을 문자열에서 열거형으로 변경하여 명시적으로 처리하는 방법입니다.
- * 문자열 상수의 경우 타입 안정성이 보장되지 않아 버그를 유발할 수 있습니다.
- * 열거형으로 변경하면 타입 안정성이 보장되어 버그를 줄일 수 있습니다.
- * 하지만 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓친다면 버그를 유발할 수 있습니다.
- * 어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
- */
- @Test
- @DisplayName("어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
- void 어떻게_새로운_열거형이_추가되었을_때_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
+ assertAll(
+ () -> assertThat(Calculator.calculate("PLUS", 1, 2)).isEqualTo(3),
+ () -> assertThat(Calculator.calculate("MINUS", 1, 2)).isEqualTo(-1)
+ );
}
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- if (command == Command.PLUS) {
- return left + right;
- }
- if (command == Command.MINUS) {
- return left - right;
- }
- // Note: MULTIPLY 명령이 추가되었지만 해당 명령을 처리하는 코드를 놓쳤습니다.
-
- throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
+ /**
+ * 명령을 문자열에서 열거형으로 변경하여 명시적으로 처리하는 방법입니다.
+ * 문자열 상수의 경우 타입 안정성이 보장되지 않아 버그를 유발할 수 있습니다.
+ * 열거형으로 변경하면 타입 안정성이 보장되어 버그를 줄일 수 있습니다.
+ * 하지만 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓친다면 버그를 유발할 수 있습니다.
+ * 어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
+ void 어떻게_새로운_열거형이_추가되었을_때_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
}
- }
- // TODO: 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있는 방법을 고민 후 개선해보세요.
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ if (command == Command.PLUS) {
+ return left + right;
+ }
+ if (command == Command.MINUS) {
+ return left - right;
+ }
+ // Note: MULTIPLY 명령이 추가되었지만 해당 명령을 처리하는 코드를 놓쳤습니다.
+
+ throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
+ }
+ }
- assertAll(
- () -> assertThat(Calculator.calculate(Command.PLUS, 1, 2)).isEqualTo(3),
- () -> assertThat(Calculator.calculate(Command.MINUS, 1, 2)).isEqualTo(-1)
- );
- }
+ // TODO: 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않을 수 있는 방법을 고민 후 개선해보세요.
- /**
- * 테스트 코드를 추가하여 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않는 방법입니다.
- * 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않으려면 테스트 코드를 작성하여 해당 명령을 처리하는 코드를 놓치지 않도록 해야 합니다.
- * 하지만 테스트를 직접 실행시키기 전까지 해당 명령을 처리하는 코드를 놓칠 수 있습니다.
- * 어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
- */
- @Test
- @DisplayName("어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
- void 어떻게_더_빠른_시점에_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
+ assertAll(
+ () -> assertThat(Calculator.calculate(Command.PLUS, 1, 2)).isEqualTo(3),
+ () -> assertThat(Calculator.calculate(Command.MINUS, 1, 2)).isEqualTo(-1)
+ );
}
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- if (command == Command.PLUS) {
- return left + right;
- }
- if (command == Command.MINUS) {
- return left - right;
- }
+ /**
+ * 테스트 코드를 추가하여 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않는 방법입니다.
+ * 새로운 열거형이 추가되었을 때 해당 명령을 처리하는 코드를 놓치지 않으려면 테스트 코드를 작성하여 해당 명령을 처리하는 코드를 놓치지 않도록 해야 합니다.
+ * 하지만 테스트를 직접 실행시키기 전까지 해당 명령을 처리하는 코드를 놓칠 수 있습니다.
+ * 어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있을까?")
+ void 어떻게_더_빠른_시점에_해당_명령을_처리하는_코드를_놓치지_않을_수_있을까() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
+ }
- throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ if (command == Command.PLUS) {
+ return left + right;
+ }
+ if (command == Command.MINUS) {
+ return left - right;
+ }
+
+ throw new UnsupportedOperationException("지원하지 않는 명령입니다.");
+ }
}
- }
- // TODO: 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있는 방법을 고민 후 개선해보세요.
+ // TODO: 더 빠른 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있는 방법을 고민 후 개선해보세요.
- for (final Command command : Command.values()) {
- // Note: 런타임 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있다.
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
+ for (final Command command : Command.values()) {
+ // Note: 런타임 시점에 해당 명령을 처리하는 코드를 놓치지 않을 수 있다.
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
+ }
}
- }
- /**
- * switch 문을 사용하여 명령을 처리하는 방법입니다.
- * 놓친 코드를 찾는 시점을 런타임이 아닌 컴파일 타임으로 변경하여 해당 명령을 처리하는 코드를 놓치지 않을 수 있습니다.
- * 코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋습니다.
- */
- @Test
- @DisplayName("코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋다.")
- void 코드를_작성할_때_최대한_런타임_시점에_발생할_수_있는_오류를_컴파일_타임으로_발생하도록_작성하는_것이_좋다() {
- enum Command {
- PLUS,
- MINUS,
- MULTIPLY
- }
+ /**
+ * switch 문을 사용하여 명령을 처리하는 방법입니다.
+ * 놓친 코드를 찾는 시점을 런타임이 아닌 컴파일 타임으로 변경하여 해당 명령을 처리하는 코드를 놓치지 않을 수 있습니다.
+ * 코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋습니다.
+ */
+ @Test
+ @DisplayName("코드를 작성할 때 최대한 런타임 시점에 발생할 수 있는 오류를 컴파일 타임으로 발생하도록 작성하는 것이 좋다.")
+ void 코드를_작성할_때_최대한_런타임_시점에_발생할_수_있는_오류를_컴파일_타임으로_발생하도록_작성하는_것이_좋다() {
+ enum Command {
+ PLUS,
+ MINUS,
+ MULTIPLY
+ }
+
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ return switch (command) {
+ case PLUS -> left + right;
+ case MINUS -> left - right;
+ case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
+ };
+ }
+ }
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- return switch (command) {
- case PLUS -> left + right;
- case MINUS -> left - right;
- case MULTIPLY -> left * right; // Note: 모든 열것값을 처리하지 않으면 컴파일 오류가 발생한다.
- };
+ for (final Command command : Command.values()) {
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
- for (final Command command : Command.values()) {
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
- }
+ /**
+ * 추가로 열거형도 객체로 바라보는 방법도 있습니다.
+ * 이러한 방법은 열거형에 역할을 부여하여 열거형이 해당 역할을 수행하도록 하는 방법입니다.
+ * 지금과 같이 간단한 코드에선 더욱 응집도가 높은 코드가 될 수 있지만, 열거형을 상수와 객체 역할을 모두 수행하도록 하는 것은 적절하지 않을 수 있습니다.
+ * 열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.
+ */
+ @Test
+ @DisplayName("열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.")
+ void 열거형이_해당_역할을_수행하도록_하는_것이_적합한지_충분한_고민을_하고_사용해야_합니다() {
+ // Note: 열거형을 상수로 바라볼 것인가, 객체로 바라볼 것인가에 대한 고민이 필요하다. 고민이 부족하면 부작용이 커진다.
+ enum Command {
+ PLUS((left, right) -> left + right),
+ MINUS((left, right) -> left - right),
+ MULTIPLY((left, right) -> left * right);
+
+ private final BiFunction function;
+
+ Command(final BiFunction function) {
+ this.function = function;
+ }
- /**
- * 추가로 열거형도 객체로 바라보는 방법도 있습니다.
- * 이러한 방법은 열거형에 역할을 부여하여 열거형이 해당 역할을 수행하도록 하는 방법입니다.
- * 지금과 같이 간단한 코드에선 더욱 응집도가 높은 코드가 될 수 있지만, 열거형을 상수와 객체 역할을 모두 수행하도록 하는 것은 적절하지 않을 수 있습니다.
- * 열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.
- */
- @Test
- @DisplayName("열거형이 해당 역할을 수행하도록 하는 것이 적합한지 충분한 고민을 하고 사용해야 합니다.")
- void 열거형이_해당_역할을_수행하도록_하는_것이_적합한지_충분한_고민을_하고_사용해야_합니다() {
- // Note: 열거형을 상수로 바라볼 것인가, 객체로 바라볼 것인가에 대한 고민이 필요하다. 고민이 부족하면 부작용이 커진다.
- enum Command {
- PLUS((left, right) -> left + right),
- MINUS((left, right) -> left - right),
- MULTIPLY((left, right) -> left * right);
-
- private final BiFunction function;
-
- Command(final BiFunction function) {
- this.function = function;
+ int execute(int left, int right) {
+ return function.apply(left, right);
+ }
}
- int execute(int left, int right) {
- return function.apply(left, right);
+ class Calculator {
+ public static int calculate(
+ final Command command,
+ final int left,
+ final int right
+ ) {
+ return command.execute(left, right);
+ }
}
- }
- class Calculator {
- public static int calculate(
- final Command command,
- final int left,
- final int right
- ) {
- return command.execute(left, right);
+ for (final Command command : Command.values()) {
+ assertThatCode(() -> {
+ Calculator.calculate(command, 1, 2);
+ }).doesNotThrowAnyException();
}
}
-
- for (final Command command : Command.values()) {
- assertThatCode(() -> {
- Calculator.calculate(command, 1, 2);
- }).doesNotThrowAnyException();
- }
}
}
diff --git a/clean-code/initial/src/test/java/cholog/goodcode/ReadableCodeTest.java b/clean-code/initial/src/test/java/cholog/goodcode/ReadableCodeTest.java
index 05c69b3..cc16af2 100644
--- a/clean-code/initial/src/test/java/cholog/goodcode/ReadableCodeTest.java
+++ b/clean-code/initial/src/test/java/cholog/goodcode/ReadableCodeTest.java
@@ -23,597 +23,557 @@
* 가독성 높은 코드는 유지보수성을 높이고 버그를 줄이는데 도움을 줍니다.
* 유지보수성과 확장성을 위한 읽기 좋은 코드를 작성하는 방법을 알아봅니다.
*/
-@Nested
-@DisplayName("가독성 높은 코드")
public class ReadableCodeTest {
- /**
- * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
- * 한 눈에 봤을 때 코드의 의도를 파악하기 어렵습니다.
- * 어떻게 의도를 전달할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 의도를 전달할 수 있을까?")
- void 어떻게_의도를_전달할_수_있을까() {
- // TODO: 자동차를 움직이고 위치가 변경된다는 의도를 드러낼 수 있는 코드를 작성해보세요.
- class Car {
- private int p = 0;
-
- void forward() {
- if (p > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
- }
-
- p += 1;
+ @Nested
+ @DisplayName("이름으로 의도 전달하기")
+ class NamingIntentTest {
+ /**
+ * 아래 코드는 최대 5까지만 움직이는 자동차를 구현한 코드입니다.
+ * 한 눈에 봤을 때 코드의 의도를 파악하기 어렵습니다.
+ * 어떻게 의도를 전달할 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 의도를 전달할 수 있을까?")
+ void 어떻게_의도를_전달할_수_있을까() {
+ // TODO: 자동차를 움직이고 위치가 변경된다는 의도를 드러낼 수 있는 코드를 작성해보세요.
+ class Car {
+ private int p = 0;
+
+ void forward() {
+ if (p > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
+
+ p += 1;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.forward();
- assertThat(car.p).isEqualTo(1);
- }
+ car.forward();
+ assertThat(car.p).isEqualTo(1);
+ }
- /**
- * 주석을 사용하여 의도를 전달하는 방법입니다.
- * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
- * 주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?")
- void 주석을_사용하지_않고도_의도를_전달할_수_있는_방법은_없을까() {
- // TODO: 자동차를 움직이고 위치가 변경된다는 의도를 드러낼 수 있는 코드를 작성해보세요.
- class Car {
- // 자동차 위치
- private int p = 0;
-
- void forward() {
- if (p > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
- }
-
- p += 1;
+ /**
+ * 주석을 사용하여 의도를 전달하는 방법입니다.
+ * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
+ * 주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("주석을 사용하지 않고도 의도를 전달할 수 있는 방법은 없을까?")
+ void 주석을_사용하지_않고도_의도를_전달할_수_있는_방법은_없을까() {
+ // TODO: 자동차를 움직이고 위치가 변경된다는 의도를 드러낼 수 있는 코드를 작성해보세요.
+ class Car {
+ // 자동차 위치
+ private int p = 0;
+
+ void forward() {
+ if (p > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
+
+ p += 1;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.forward();
- assertThat(car.p).isEqualTo(1);
- }
+ car.forward();
+ assertThat(car.p).isEqualTo(1);
+ }
+
+ /**
+ * 주석을 사용하지 않고 의미있는 이름을 통해 의도를 전달하는 방법입니다.
+ * 의미있는 이름을 사용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 또한 코드로 관리되기 때문에 코드 변경 시 주석을 신경쓰지 않아도 됩니다.
+ * 지금의 position은 이름을 통해 의도를 전달하고 있지만, 최대 5까지만 움직인다는 사실은 동작을 통해 알 수 있습니다.
+ * 코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까")
+ void 코드를_통해_객체의_역할을_명확하게_드러내는_방법은_없을까() {
+ // TODO: 객체의 역할을 명확하게 드러내는 코드를 작성해보세요.
+ class Car {
+ private int position;
+
+ void forward() {
+ if (position > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
- /**
- * 주석을 사용하지 않고 의미있는 이름을 통해 의도를 전달하는 방법입니다.
- * 의미있는 이름을 사용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 또한 코드로 관리되기 때문에 코드 변경 시 주석을 신경쓰지 않아도 됩니다.
- * 지금의 position은 이름을 통해 의도를 전달하고 있지만, 최대 5까지만 움직인다는 사실은 동작을 통해 알 수 있습니다.
- * 코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까?
- */
- @Test
- @DisplayName("코드를 통해 객체의 역할을 명확하게 드러내는 방법은 없을까")
- void 코드를_통해_객체의_역할을_명확하게_드러내는_방법은_없을까() {
- // TODO: 객체의 역할을 명확하게 드러내는 코드를 작성해보세요.
- class Car {
- private int position;
-
- void forward() {
- if (position > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
- }
-
- position += 1;
+ position += 1;
+ }
}
+
+ final var car = new Car();
+
+ car.forward();
+ assertThat(car.position).isEqualTo(1);
}
- final var car = new Car();
+ /**
+ * 움직임을 담당하는 객체를 만들어서 코드를 통해 객체의 역할을 명확하게 드러내는 방법입니다.
+ * 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 하지만 의미없는 객체가 많이 생기게 된다면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다.
+ * 코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어듭니다.
+ */
+ @Test
+ @DisplayName("코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어든다")
+ void 코드_자체로_설명이_되도록_코드를_작성하면_유지_및_관리의_비용이_줄어든다() {
+ class Position {
+ private final int value;
+
+ public Position() {
+ this(0);
+ }
- car.forward();
- assertThat(car.position).isEqualTo(1);
- }
+ public Position(final int value) {
+ if (value > 5) {
+ throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ }
- /**
- * 움직임을 담당하는 객체를 만들어서 코드를 통해 객체의 역할을 명확하게 드러내는 방법입니다.
- * 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 하지만 의미없는 객체가 많이 생기게 된다면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다.
- * 코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어듭니다.
- */
- @Test
- @DisplayName("코드 자체로 설명이 되도록 코드를 작성하면 유지 및 관리의 비용이 줄어든다")
- void 코드_자체로_설명이_되도록_코드를_작성하면_유지_및_관리의_비용이_줄어든다() {
- class Position {
- private final int value;
-
- public Position() {
- this(0);
- }
+ this.value = value;
+ }
- public Position(final int value) {
- if (value > 5) {
- throw new IllegalStateException("최대 5까지만 움직일 수 있습니다.");
+ Position forward() {
+ return new Position(value + 1);
}
- this.value = value;
- }
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Position position = (Position) o;
+ return value == position.value;
+ }
- Position forward() {
- return new Position(value + 1);
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- @Override
- public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final Position position = (Position) o;
- return value == position.value;
- }
+ class Car {
+ private Position position = new Position();
- @Override
- public int hashCode() {
- return Objects.hash(value);
+ void forward() {
+ position = position.forward();
+ }
}
- }
- class Car {
- private Position position = new Position();
+ final var car = new Car();
- void forward() {
- position = position.forward();
- }
+ car.forward();
+ assertThat(car.position).isEqualTo(new Position(1));
}
+ }
- final var car = new Car();
+ @Nested
+ @DisplayName("일관된 코드 스타일")
+ class ConsistentStyleTest {
+ /**
+ * 일관적이지 않은 코드 스타일은 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄 수 있습니다.
+ * 오해할 위험을 줄이면, 버그가 줄어들고 혼란스러운 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
+ */
+ @Test
+ @DisplayName("일관된 코드 스타일을 가져간다")
+ void 일관된_코드_스타일을_가져간다() {
+ // TODO: 일관된 코드 스타일로 리팩토링 해보세요.
+ // @formatter:off
+ class Car {
+ private String name;
+ private int position;
- car.forward();
- assertThat(car.position).isEqualTo(new Position(1));
- }
+ public String getName()
+ {
+ return name;
+ }
- /**
- * 일관적이지 않은 코드 스타일은 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄 수 있습니다.
- * 오해할 위험을 줄이면, 버그가 줄어들고 혼란스러운 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
- */
- @Test
- @DisplayName("일관된 코드 스타일을 가져간다")
- void 일관된_코드_스타일을_가져간다() {
- // TODO: 일관된 코드 스타일로 리팩토링 해보세요.
- // @formatter:off
- class Car {
- private String name;
- private int position;
-
- public String getName()
- {
- return name;
- }
+ void Forward() {
+ position += 1;
+ }
- void Forward() {
- position += 1;
- }
+ public int position() {
+ return position;
+ }
- public int position() {
- return position;
+ void minusPosition()
+ {
+ position--;
+ }
}
+ // @formatter:on
- void minusPosition()
- {
- position--;
- }
- }
- // @formatter:on
+ final var car = new Car();
- final var car = new Car();
+ car.Forward();
+ assertThat(car.position()).isEqualTo(1);
+ }
- car.Forward();
- assertThat(car.position()).isEqualTo(1);
- }
+ /**
+ * 일관된 코드 스타일로 리팩토링한 코드입니다.
+ * 코드를 이해하기 쉽게 만들기 위해 일관된 코드 스타일을 사용하는 것이 중요합니다.
+ * 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄어들어 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
+ */
+ @Test
+ @DisplayName("일관된 코드 스타일로 리팩토링한 코드")
+ void 일관된_코드_스타일로_리팩토링한_코드() {
+ class Car {
+ private String name;
+ private int position;
- /**
- * 일관된 코드 스타일로 리팩토링한 코드입니다.
- * 코드를 이해하기 쉽게 만들기 위해 일관된 코드 스타일을 사용하는 것이 중요합니다.
- * 코드를 읽는 사람이 코드를 이해하는데 혼동을 줄어들어 코드를 이해하는데 낭비되는 시간을 줄일 수 있습니다.
- */
- @Test
- @DisplayName("일관된 코드 스타일로 리팩토링한 코드")
- void 일관된_코드_스타일로_리팩토링한_코드() {
- class Car {
- private String name;
- private int position;
-
- public void forward() {
- position += 1;
- }
+ public void forward() {
+ position += 1;
+ }
- public void backward() {
- position -= 1;
- }
+ public void backward() {
+ position -= 1;
+ }
- public String getName() {
- return name;
- }
+ public String getName() {
+ return name;
+ }
- public int getPosition() {
- return position;
+ public int getPosition() {
+ return position;
+ }
}
- }
- final var car = new Car();
+ final var car = new Car();
- car.forward();
- assertThat(car.getPosition()).isEqualTo(1);
+ car.forward();
+ assertThat(car.getPosition()).isEqualTo(1);
+ }
}
- /**
- * 하나의 메서드가 많은 일을 하면 추상화 계층이 깊어지고, 코드를 이해하기 어려워집니다.
- * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
- * 아래 우승 로또 번호와 나의 로또 번호를 비교하여 상금을 계산하는 코드는 한 가지 이상의 일을 하고 있습니다.
- * 어떻게 추상화하여 메서드를 작게 만들 수 있을까?
- */
- @Test
- @DisplayName("어떻게 추상화하여 메서드를 작게 만들 수 있을까?")
- void 어떻게_추상화하여_메서드를_작게_만들_수_있을까() {
- // TODO: 역할을 적절히 추상화하여 메서드를 작게 만들어보세요. 메서드의 시그니처는 변경하지 않습니다.
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- for (int number : numbers) {
- if (number < 1 || number > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ @Nested
+ @DisplayName("메서드와 객체의 추상화")
+ class AbstractionTest {
+ /**
+ * 하나의 메서드가 많은 일을 하면 추상화 계층이 깊어지고, 코드를 이해하기 어려워집니다.
+ * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
+ * 아래 우승 로또 번호와 나의 로또 번호를 비교하여 상금을 계산하는 코드는 한 가지 이상의 일을 하고 있습니다.
+ * 어떻게 추상화하여 메서드를 작게 만들 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 추상화하여 메서드를 작게 만들 수 있을까?")
+ void 어떻게_추상화하여_메서드를_작게_만들_수_있을까() {
+ // TODO: 역할을 적절히 추상화하여 메서드를 작게 만들어보세요. 메서드의 시그니처는 변경하지 않습니다.
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ for (int number : numbers) {
+ if (number < 1 || number > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
}
- }
- for (int winningNumber : winningNumbers) {
- if (winningNumber < 1 || winningNumber > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ for (int winningNumber : winningNumbers) {
+ if (winningNumber < 1 || winningNumber > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
+ }
+ if (new HashSet<>(numbers).size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
+ if (new HashSet<>(winningNumbers).size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
}
- }
- if (new HashSet<>(numbers).size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
- }
- if (new HashSet<>(winningNumbers).size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
- }
- int count = 0;
- for (int number : numbers) {
- for (int winningNumber : winningNumbers) {
- if (number == winningNumber) {
- count++;
+ int count = 0;
+ for (int number : numbers) {
+ for (int winningNumber : winningNumbers) {
+ if (number == winningNumber) {
+ count++;
+ }
}
}
- }
- return switch (count) {
- case 6 -> 1_000_000_000;
- case 5 -> 50_000_000;
- case 4 -> 500_000;
- case 3 -> 5_000;
- default -> 0;
- };
+ return switch (count) {
+ case 6 -> 1_000_000_000;
+ case 5 -> 50_000_000;
+ case 4 -> 500_000;
+ case 3 -> 5_000;
+ default -> 0;
+ };
+ }
}
- }
-
- final var lottoGame = new LottoGame();
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
-
- /**
- * 메서드를 작게 만들어 추상화한 코드입니다.
- * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
- * 추상화 수준을 적절하게 유지하면 유지보수성을 높일 수 있습니다.
- * 메서드를 작게 만들어 추상화하다 보면 메서드의 역할이 명확해지지만, 객체의 역할은 아직 명확하지 않습니다.
- * 어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?
- */
- @Test
- @DisplayName("어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?")
- void 어떻게_추상화하여_객체의_역할을_명확하게_드러낼_수_있을까() {
- // TODO: 역할을 적절히 추상화하여 클래스를 작게 만들어보세요. 시작점 메서드의 시그니처는 변경하지 않습니다.
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- validateNumbers(numbers);
- validateNumbers(winningNumbers);
+ final var lottoGame = new LottoGame();
- final int count = countMatchNumbers(numbers, winningNumbers);
- return calculatePrizeByCount(count);
- }
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
- private void validateNumbers(final List lottoNumbers) {
- for (int number : lottoNumbers) {
- validateNumber(number);
+ /**
+ * 메서드를 작게 만들어 추상화한 코드입니다.
+ * 메서드가 한 가지 일만 하도록 작성하면 코드를 이해하기 쉬워집니다.
+ * 추상화 수준을 적절하게 유지하면 유지보수성을 높일 수 있습니다.
+ * 메서드를 작게 만들어 추상화하다 보면 메서드의 역할이 명확해지지만, 객체의 역할은 아직 명확하지 않습니다.
+ * 어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?
+ */
+ @Test
+ @DisplayName("어떻게 추상화하여 객체의 역할을 명확하게 드러낼 수 있을까?")
+ void 어떻게_추상화하여_객체의_역할을_명확하게_드러낼_수_있을까() {
+ // TODO: 역할을 적절히 추상화하여 클래스를 작게 만들어보세요. 시작점 메서드의 시그니처는 변경하지 않습니다.
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ validateNumbers(numbers);
+ validateNumbers(winningNumbers);
+
+ final int count = countMatchNumbers(numbers, winningNumbers);
+ return calculatePrizeByCount(count);
}
- if (new HashSet<>(lottoNumbers).size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+
+ private void validateNumbers(final List lottoNumbers) {
+ for (int number : lottoNumbers) {
+ validateNumber(number);
+ }
+ if (new HashSet<>(lottoNumbers).size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
}
- }
- private void validateNumber(final Integer lottoNumber) {
- if (lottoNumber < 1 || lottoNumber > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ private void validateNumber(final Integer lottoNumber) {
+ if (lottoNumber < 1 || lottoNumber > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
}
- }
- private int countMatchNumbers(
- final List numbers,
- final List winningNumbers
- ) {
- int count = 0;
- for (int number : numbers) {
- for (int winningNumber : winningNumbers) {
- if (number == winningNumber) {
- count++;
+ private int countMatchNumbers(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ int count = 0;
+ for (int number : numbers) {
+ for (int winningNumber : winningNumbers) {
+ if (number == winningNumber) {
+ count++;
+ }
}
}
+
+ return count;
}
- return count;
+ private int calculatePrizeByCount(final int count) {
+ return switch (count) {
+ case 6 -> 1_000_000_000;
+ case 5 -> 50_000_000;
+ case 4 -> 500_000;
+ case 3 -> 5_000;
+ default -> 0;
+ };
+ }
}
- private int calculatePrizeByCount(final int count) {
- return switch (count) {
- case 6 -> 1_000_000_000;
- case 5 -> 50_000_000;
- case 4 -> 500_000;
- case 3 -> 5_000;
- default -> 0;
- };
- }
- }
+ final var lottoGame = new LottoGame();
- final var lottoGame = new LottoGame();
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
+ /**
+ * 객체의 역할을 명확하게 드러낸 코드입니다.
+ * 객체가 자기 자신의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 코드를 읽는 사람이 코드의 의도를 파악하기 쉽게 만들기 위해 객체의 역할을 명확하게 드러내는 것이 중요합니다.
+ */
+ @Test
+ @DisplayName("객체의 역할을 명확하게 드러낸 코드")
+ void 객체의_역할을_명확하게_드러낸_코드() {
+ class LottoNumber {
+ private final int value;
+
+ public LottoNumber(final int value) {
+ if (value < 1 || value > 45) {
+ throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
+ }
+ this.value = value;
+ }
- /**
- * 객체의 역할을 명확하게 드러낸 코드입니다.
- * 객체가 자기 자신의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 코드를 읽는 사람이 코드의 의도를 파악하기 쉽게 만들기 위해 객체의 역할을 명확하게 드러내는 것이 중요합니다.
- */
- @Test
- @DisplayName("객체의 역할을 명확하게 드러낸 코드")
- void 객체의_역할을_명확하게_드러낸_코드() {
- class LottoNumber {
- private final int value;
-
- public LottoNumber(final int value) {
- if (value < 1 || value > 45) {
- throw new IllegalArgumentException("로또 번호는 1부터 45까지의 숫자여야 합니다.");
- }
- this.value = value;
- }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LottoNumber that = (LottoNumber) o;
+ return value == that.value;
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- LottoNumber that = (LottoNumber) o;
- return value == that.value;
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
}
- @Override
- public int hashCode() {
- return Objects.hash(value);
- }
- }
+ class Lotto {
+ private final Set numbers;
- class Lotto {
- private final Set numbers;
+ public Lotto(final List numbers) {
+ this(numbers.stream()
+ .map(LottoNumber::new)
+ .collect(toSet()));
+ }
- public Lotto(final List numbers) {
- this(numbers.stream()
- .map(LottoNumber::new)
- .collect(toSet()));
- }
+ public Lotto(final Set numbers) {
+ if (numbers.size() != 6) {
+ throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ }
- public Lotto(final Set numbers) {
- if (numbers.size() != 6) {
- throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
+ this.numbers = numbers;
}
- this.numbers = numbers;
- }
-
- int countMatchNumbers(final Lotto winningLotto) {
- int count = 0;
- for (LottoNumber number : numbers) {
- for (LottoNumber winningNumber : winningLotto.numbers) {
- if (number.equals(winningNumber)) {
- count++;
+ int countMatchNumbers(final Lotto winningLotto) {
+ int count = 0;
+ for (LottoNumber number : numbers) {
+ for (LottoNumber winningNumber : winningLotto.numbers) {
+ if (number.equals(winningNumber)) {
+ count++;
+ }
}
}
- }
- return count;
+ return count;
+ }
}
- }
- enum LottoRank {
- FIRST(6, 1_000_000_000),
- SECOND(5, 50_000_000),
- THIRD(4, 500_000),
- FOURTH(3, 5_000),
- NONE(0, 0);
+ enum LottoRank {
+ FIRST(6, 1_000_000_000),
+ SECOND(5, 50_000_000),
+ THIRD(4, 500_000),
+ FOURTH(3, 5_000),
+ NONE(0, 0);
- private final int count;
- private final int prize;
+ private final int count;
+ private final int prize;
- LottoRank(final int count, final int prize) {
- this.count = count;
- this.prize = prize;
- }
+ LottoRank(final int count, final int prize) {
+ this.count = count;
+ this.prize = prize;
+ }
- public static LottoRank of(final int count) {
- return Arrays.stream(values())
- .filter(prize -> prize.count == count)
- .findFirst()
- .orElse(NONE);
- }
+ public static LottoRank of(final int count) {
+ return Arrays.stream(values())
+ .filter(prize -> prize.count == count)
+ .findFirst()
+ .orElse(NONE);
+ }
- public int getPrize() {
- return prize;
+ public int getPrize() {
+ return prize;
+ }
}
- }
- class LottoGame {
- int calculatePrize(
- final List numbers,
- final List winningNumbers
- ) {
- return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
- }
+ class LottoGame {
+ int calculatePrize(
+ final List numbers,
+ final List winningNumbers
+ ) {
+ return calculatePrize(new Lotto(numbers), new Lotto(winningNumbers));
+ }
- int calculatePrize(
- final Lotto lotto,
- final Lotto winningLotto
- ) {
- final var count = lotto.countMatchNumbers(winningLotto);
- final var lottoRank = LottoRank.of(count);
- return lottoRank.getPrize();
+ int calculatePrize(
+ final Lotto lotto,
+ final Lotto winningLotto
+ ) {
+ final var count = lotto.countMatchNumbers(winningLotto);
+ final var lottoRank = LottoRank.of(count);
+ return lottoRank.getPrize();
+ }
}
- }
- final var lottoGame = new LottoGame();
+ final var lottoGame = new LottoGame();
- assertAll(
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
- () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
- );
- }
-
- /**
- * 메서드의 이름을 잘 지어도 매개변수가 무엇이고 어떤 역할을 하는지 알기 어렵다면 의미를 전달하기 어렵습니다.
- * 매개변수 또한 의미를 전달할 수 있도록 작성하는 것이 중요합니다.
- * 아래 코드는 좋아하는 음식과 싫어하는 음식을 가지고 있는 Crew 객체를 생성하는 코드입니다.
- * 클래스 내에 이름을 잘 지어두었다면 의미를 전달할 수 있지만, 매번 해당 클래스로 가서 이름을 확인하는 방법은 번거롭습니다.
- * 어떻게 매개변수의 의미를 전달할 수 있을까?
- */
- @Test
- @DisplayName("어떻게 매개변수의 의미를 전달할 수 있을까?")
- void 어떻게_매개변수의_의미를_전달할_수_있을까() {
- // TODO: Crew 클래스를 확인하지 않고 매개변수의 의미를 전달할 수 있는 코드를 작성해보세요.
- final var crew = new Crew(
- "Neo",
- Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
-
- assertThat(crew.name()).isEqualTo("Neo");
+ assertAll(
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 6))).isEqualTo(1_000_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 5, 7))).isEqualTo(50_000_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 4, 7, 8))).isEqualTo(500_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 3, 7, 8, 9))).isEqualTo(5_000),
+ () -> assertThat(lottoGame.calculatePrize(List.of(1, 2, 3, 4, 5, 6), List.of(1, 2, 7, 8, 9, 10))).isEqualTo(0)
+ );
+ }
}
- /**
- * 주석으로 매개변수의 의미를 전달할 수 있는 코드입니다.
- * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
- * 주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?")
- void 주석을_사용하지_않고_매개변수의_의미를_전달할_수_있는_방법은_없을까() {
- /* Note: 자바는 네임드 파라미터를 지원하지 않는다. IntelliJ에서 도움을 주지만 아쉬운 부분이 있다.
- final var neo = new Crew(
- name: "neo",
- likeMenuItems: Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- dislikeMenuItems: Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
+ @Nested
+ @DisplayName("매개변수의 의미 전달")
+ class ParameterMeaningTest {
+ /**
+ * 메서드의 이름을 잘 지어도 매개변수가 무엇이고 어떤 역할을 하는지 알기 어렵다면 의미를 전달하기 어렵습니다.
+ * 매개변수 또한 의미를 전달할 수 있도록 작성하는 것이 중요합니다.
+ * 아래 코드는 좋아하는 음식과 싫어하는 음식을 가지고 있는 Crew 객체를 생성하는 코드입니다.
+ * 클래스 내에 이름을 잘 지어두었다면 의미를 전달할 수 있지만, 매번 해당 클래스로 가서 이름을 확인하는 방법은 번거롭습니다.
+ * 어떻게 매개변수의 의미를 전달할 수 있을까?
*/
-
- // TODO: Crew 클래스를 확인하지 않고 매개변수의 의미를 전달할 수 있는 코드를 작성해보세요.
- final var crew = new Crew(
- /*name*/ "Neo",
- /*likeMenuItems*/ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- /*dislikeMenuItems*/ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
-
- assertThat(crew.name()).isEqualTo("Neo");
- }
-
- /**
- * 빌더를 통해 매개변수의 의미를 전달할 수 있는 코드입니다.
- * 빌더를 사용하면 매개변수의 의미를 전달할 수 있고, 빌더를 통해 객체를 생성할 때 매개변수의 순서를 신경쓰지 않아도 됩니다.
- * 매개변수가 많아지면 어떤 값을 설정했는지 확인이 어렵거나 어떤 매개변수끼리 의미가 있는지 확인이 어려워질 수 있습니다.
- * 매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?")
- void 매개변수를_묶어_의미를_전달할_수_있는_방법은_없을까() {
- // TODO: 매개변수의 의미를 묶어서 전달할 수 있는 코드를 작성해보세요.
- class Builder {
- private String name;
- private Set likeMenuItems;
- private Set dislikeMenuItems;
-
- public Builder name(final String name) {
- this.name = name;
- return this;
- }
-
- public Builder likeMenuItems(final Set likeMenuItems) {
- this.likeMenuItems = likeMenuItems;
- return this;
- }
-
- public Builder dislikeMenuItems(final Set dislikeMenuItems) {
- this.dislikeMenuItems = dislikeMenuItems;
- return this;
- }
-
- public Crew build() {
- return new Crew(name, likeMenuItems, dislikeMenuItems);
- }
+ @Test
+ @DisplayName("어떻게 매개변수의 의미를 전달할 수 있을까?")
+ void 어떻게_매개변수의_의미를_전달할_수_있을까() {
+ // TODO: Crew 클래스를 확인하지 않고 매개변수의 의미를 전달할 수 있는 코드를 작성해보세요.
+ final var crew = new Crew(
+ "Neo",
+ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+
+ assertThat(crew.name()).isEqualTo("Neo");
}
- /* Note: 만약 타입이 같은 매개변수의 초기화 순서가 바뀐다면 문제가 발생할 수 있다.
- final var crew = new Crew(
- "Neo",
- Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
- Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- );
+ /**
+ * 주석으로 매개변수의 의미를 전달할 수 있는 코드입니다.
+ * 하지만 주석을 신경쓰지 않고 코드를 변경하거나, 처음부터 주석과 다른 코드를 작성했다면 오히려 오해할 수 있는 코드가 될 수 있습니다.
+ * 주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?
*/
- final var crew = new Builder()
- .name("Neo")
- .dislikeMenuItems(Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스"))
- .likeMenuItems(Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"))
- .build();
-
- assertThat(crew.name()).isEqualTo("Neo");
- }
+ @Test
+ @DisplayName("주석을 사용하지 않고 매개변수의 의미를 전달할 수 있는 방법은 없을까?")
+ void 주석을_사용하지_않고_매개변수의_의미를_전달할_수_있는_방법은_없을까() {
+ /* Note: 자바는 네임드 파라미터를 지원하지 않는다. IntelliJ에서 도움을 주지만 아쉬운 부분이 있다.
+ final var neo = new Crew(
+ name: "neo",
+ likeMenuItems: Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ dislikeMenuItems: Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+ */
+
+ // TODO: Crew 클래스를 확인하지 않고 매개변수의 의미를 전달할 수 있는 코드를 작성해보세요.
+ final var crew = new Crew(
+ /*name*/ "Neo",
+ /*likeMenuItems*/ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ /*dislikeMenuItems*/ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+
+ assertThat(crew.name()).isEqualTo("Neo");
+ }
- /**
- * 매개변수를 묶어 의미를 전달할 수 있는 코드입니다.
- * 의미가 동일한 매개변수를 묶어 별도 클래스로 분리하여 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 원리는 위에서 학습한 메서드 분리, 추상화, 객체의 역할을 명확하게 드러내는 방법과 동일합니다.
- * 의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있습니다.
- * 정답은 없습니다. 적절한 추상화 수준을 유지하는 것이 중요합니다.
- */
- @Test
- @DisplayName("의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다")
- void 의미가_명확해진_장점은_있지만_객체가_많아지면_유지보수성이_떨어질_수_있으니_적절한_추상화_수준을_유지하는_것이_중요합니다() {
- record Taste(
- Set likeMenuItems,
- Set dislikeMenuItems
- ) {
- static class Builder {
+ /**
+ * 빌더를 통해 매개변수의 의미를 전달할 수 있는 코드입니다.
+ * 빌더를 사용하면 매개변수의 의미를 전달할 수 있고, 빌더를 통해 객체를 생성할 때 매개변수의 순서를 신경쓰지 않아도 됩니다.
+ * 매개변수가 많아지면 어떤 값을 설정했는지 확인이 어렵거나 어떤 매개변수끼리 의미가 있는지 확인이 어려워질 수 있습니다.
+ * 매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("매개변수를 묶어 의미를 전달할 수 있는 방법은 없을까?")
+ void 매개변수를_묶어_의미를_전달할_수_있는_방법은_없을까() {
+ // TODO: 매개변수의 의미를 묶어서 전달할 수 있는 코드를 작성해보세요.
+ class Builder {
+ private String name;
private Set likeMenuItems;
private Set dislikeMenuItems;
- public Builder likeMenuItems(final String... likeMenuItems) {
- return likeMenuItems(Set.of(likeMenuItems));
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
}
public Builder likeMenuItems(final Set likeMenuItems) {
@@ -621,173 +581,231 @@ public Builder likeMenuItems(final Set likeMenuItems) {
return this;
}
- public Builder dislikeMenuItems(final String... dislikeMenuItems) {
- return dislikeMenuItems(Set.of(dislikeMenuItems));
- }
-
public Builder dislikeMenuItems(final Set dislikeMenuItems) {
this.dislikeMenuItems = dislikeMenuItems;
return this;
}
- public Taste build() {
- return new Taste(likeMenuItems, dislikeMenuItems);
+ public Crew build() {
+ return new Crew(name, likeMenuItems, dislikeMenuItems);
}
}
+
+ /* Note: 만약 타입이 같은 매개변수의 초기화 순서가 바뀐다면 문제가 발생할 수 있다.
+ final var crew = new Crew(
+ "Neo",
+ Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"),
+ Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ );
+ */
+ final var crew = new Builder()
+ .name("Neo")
+ .dislikeMenuItems(Set.of("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스"))
+ .likeMenuItems(Set.of("쌈밥", "김치찌개", "탕수육", "비빔밥"))
+ .build();
+
+ assertThat(crew.name()).isEqualTo("Neo");
}
- record Crew(
- String name,
- Taste taste
- ) {
- static class Builder {
- private String name;
- private Taste taste;
+ /**
+ * 매개변수를 묶어 의미를 전달할 수 있는 코드입니다.
+ * 의미가 동일한 매개변수를 묶어 별도 클래스로 분리하여 객체의 역할을 명확하게 드러내면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 원리는 위에서 학습한 메서드 분리, 추상화, 객체의 역할을 명확하게 드러내는 방법과 동일합니다.
+ * 의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있습니다.
+ * 정답은 없습니다. 적절한 추상화 수준을 유지하는 것이 중요합니다.
+ */
+ @Test
+ @DisplayName("의미가 명확해진 장점은 있지만, 객체가 많아지면 유지보수성이 떨어질 수 있으니 적절한 추상화 수준을 유지하는 것이 중요합니다")
+ void 의미가_명확해진_장점은_있지만_객체가_많아지면_유지보수성이_떨어질_수_있으니_적절한_추상화_수준을_유지하는_것이_중요합니다() {
+ record Taste(
+ Set likeMenuItems,
+ Set dislikeMenuItems
+ ) {
+ static class Builder {
+ private Set likeMenuItems;
+ private Set dislikeMenuItems;
- public Builder name(final String name) {
- this.name = name;
- return this;
- }
+ public Builder likeMenuItems(final String... likeMenuItems) {
+ return likeMenuItems(Set.of(likeMenuItems));
+ }
- public Builder taste(final Taste taste) {
- this.taste = taste;
- return this;
+ public Builder likeMenuItems(final Set likeMenuItems) {
+ this.likeMenuItems = likeMenuItems;
+ return this;
+ }
+
+ public Builder dislikeMenuItems(final String... dislikeMenuItems) {
+ return dislikeMenuItems(Set.of(dislikeMenuItems));
+ }
+
+ public Builder dislikeMenuItems(final Set dislikeMenuItems) {
+ this.dislikeMenuItems = dislikeMenuItems;
+ return this;
+ }
+
+ public Taste build() {
+ return new Taste(likeMenuItems, dislikeMenuItems);
+ }
}
+ }
- public Crew build() {
- return new Crew(name, taste);
+ record Crew(
+ String name,
+ Taste taste
+ ) {
+ static class Builder {
+ private String name;
+ private Taste taste;
+
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder taste(final Taste taste) {
+ this.taste = taste;
+ return this;
+ }
+
+ public Crew build() {
+ return new Crew(name, taste);
+ }
}
}
- }
- final var taste = new Taste.Builder()
- .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
- .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
- .build();
- final var crew = new Crew.Builder()
- .name("Neo")
- .taste(taste)
- .build();
+ final var taste = new Taste.Builder()
+ .likeMenuItems("쌈밥", "김치찌개", "탕수육", "비빔밥")
+ .dislikeMenuItems("샐러드", "파인애플 볶음밥", "미소시루", "하이라이스")
+ .build();
+ final var crew = new Crew.Builder()
+ .name("Neo")
+ .taste(taste)
+ .build();
- assertThat(crew.name()).isEqualTo("Neo");
+ assertThat(crew.name()).isEqualTo("Neo");
+ }
}
- /**
- * 기능을 구현하다 보면 이미 누군가가 구현한 기능을 재사용하고 싶을 때가 있습니다.
- * 대표적으로 자바에서 제공하는 Collection API를 사용하는 것입니다.
- * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?
- */
- @Test
- @DisplayName("Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?")
- void Collection_API를_사용하여_코드를_재사용하고_의도를_파악하기_쉽게_만들_수_없을까() {
- class Menu {
- private final List menuItems;
-
- public Menu(final List menuItems) {
- // TODO: Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들어보세요.
- for (int i = 0; i < menuItems.size(); i++) {
- for (int j = 0; j < i; j++) {
- if (menuItems.get(i).equals(menuItems.get(j))) {
- throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
+ @Nested
+ @DisplayName("적절한 API 활용")
+ class ProperApiUsageTest {
+ /**
+ * 기능을 구현하다 보면 이미 누군가가 구현한 기능을 재사용하고 싶을 때가 있습니다.
+ * 대표적으로 자바에서 제공하는 Collection API를 사용하는 것입니다.
+ * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?
+ */
+ @Test
+ @DisplayName("Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들 수 없을까?")
+ void Collection_API를_사용하여_코드를_재사용하고_의도를_파악하기_쉽게_만들_수_없을까() {
+ class Menu {
+ private final List menuItems;
+
+ public Menu(final List menuItems) {
+ // TODO: Collection API를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만들어보세요.
+ for (int i = 0; i < menuItems.size(); i++) {
+ for (int j = 0; j < i; j++) {
+ if (menuItems.get(i).equals(menuItems.get(j))) {
+ throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
+ }
}
}
- }
- this.menuItems = menuItems;
+ this.menuItems = menuItems;
+ }
}
+
+ assertThatThrownBy(() -> {
+ new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+ }).hasMessage("중복된 메뉴가 있습니다.");
}
- assertThatThrownBy(() -> {
- new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
- }).hasMessage("중복된 메뉴가 있습니다.");
- }
+ /**
+ * Collection의 distinct를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만든 코드입니다.
+ * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * 위 객체 분리에서 학습한 것처럼 메서드로 나타내는 것 보다 객체 자체로 나타내는 것이 더 좋은 방법일 수 있습니다.
+ * 객체 자체로 의미를 전달할 수 있는 방법은 없을까?
+ */
+ @Test
+ @DisplayName("객체 자체로 의미를 전달할 수 있는 방법은 없을까?")
+ void 객체_자체로_의미를_전달할_수_있는_방법은_없을까() {
+ class Menu {
+ private final List menuItems;
+
+ // TODO: 객체 자체로 의미를 전달할 수 있도록 코드를 작성해보세요.
+ public Menu(final List menuItems) {
+ if (menuItems.size() != menuItems.stream().distinct().count()) {
+ throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
+ }
- /**
- * Collection의 distinct를 사용하여 코드를 재사용하고 의도를 파악하기 쉽게 만든 코드입니다.
- * Collection API를 사용하면 코드를 재사용할 수 있고, 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * 위 객체 분리에서 학습한 것처럼 메서드로 나타내는 것 보다 객체 자체로 나타내는 것이 더 좋은 방법일 수 있습니다.
- * 객체 자체로 의미를 전달할 수 있는 방법은 없을까?
- */
- @Test
- @DisplayName("객체 자체로 의미를 전달할 수 있는 방법은 없을까?")
- void 객체_자체로_의미를_전달할_수_있는_방법은_없을까() {
- class Menu {
- private final List menuItems;
-
- // TODO: 객체 자체로 의미를 전달할 수 있도록 코드를 작성해보세요.
- public Menu(final List menuItems) {
- if (menuItems.size() != menuItems.stream().distinct().count()) {
- throw new IllegalArgumentException("중복된 메뉴가 있습니다.");
- }
-
- this.menuItems = menuItems;
+ this.menuItems = menuItems;
+ }
}
- }
- assertThatThrownBy(() -> {
- new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
- }).hasMessage("중복된 메뉴가 있습니다.");
- }
+ assertThatThrownBy(() -> {
+ new Menu(List.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+ }).hasMessage("중복된 메뉴가 있습니다.");
+ }
- /**
- * 객체 자체로 의미를 전달할 수 있는 코드입니다.
- * 자료구조를 활용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
- * Set 자료구조는 중복을 허용하지 않기 때문에 해당 객체를 생성하는 시점에 중복이 없는 것을 보장할 수 있습니다.
- * 적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다.
- */
- @Test
- @DisplayName("적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다")
- void 적절한_API를_사용하면_견고한_코드를_작성할_수_있습니다() {
- class Menu {
- private final Set menuItems;
-
- public Menu(final Set menuItems) {
- this.menuItems = menuItems;
+ /**
+ * 객체 자체로 의미를 전달할 수 있는 코드입니다.
+ * 자료구조를 활용하면 코드를 읽는 사람이 코드의 의도를 파악하기 쉬워집니다.
+ * Set 자료구조는 중복을 허용하지 않기 때문에 해당 객체를 생성하는 시점에 중복이 없는 것을 보장할 수 있습니다.
+ * 적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다.
+ */
+ @Test
+ @DisplayName("적절한 API를 사용하면 견고한 코드를 작성할 수 있습니다")
+ void 적절한_API를_사용하면_견고한_코드를_작성할_수_있습니다() {
+ class Menu {
+ private final Set menuItems;
+
+ public Menu(final Set menuItems) {
+ this.menuItems = menuItems;
+ }
}
- }
- assertThatCode(() -> {
- // Note: List 자료구조를 허용하지 않고 Set 자료구조만 허용하여 중복을 허용하지 않는다는 의도를 전달할 수 있다.
- // new Menu(Set.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
+ assertThatCode(() -> {
+ // Note: List 자료구조를 허용하지 않고 Set 자료구조만 허용하여 중복을 허용하지 않는다는 의도를 전달할 수 있다.
+ // new Menu(Set.of("쌈밥", "김치찌개", "쌈밥", "비빔밥"));
- new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
- }).doesNotThrowAnyException();
- }
+ new Menu(Set.of("쌈밥", "김치찌개", "비빔밥"));
+ }).doesNotThrowAnyException();
+ }
- /**
- * 적절한 API를 사용하는 것이 견고한 코드를 작성할 수 있다는 사실을 알게 되었습니다.
- * 그렇다고 너무 API를 사용하는 것에 매몰되면 부작용이 발생할 수 있습니다.
- */
- @Test
- @DisplayName("API에 매몰되어 과하게 사용하면 생길 수 있는 문제")
- void API에_매몰되어_과하게_사용하면_생길_수_있는_문제() {
- class Menu {
- private final Map menu;
-
- public Menu(final Map menu) {
- this.menu = menu;
- }
+ /**
+ * 적절한 API를 사용하는 것이 견고한 코드를 작성할 수 있다는 사실을 알게 되었습니다.
+ * 그렇다고 너무 API를 사용하는 것에 매몰되면 부작용이 발생할 수 있습니다.
+ */
+ @Test
+ @DisplayName("API에 매몰되어 과하게 사용하면 생길 수 있는 문제")
+ void API에_매몰되어_과하게_사용하면_생길_수_있는_문제() {
+ class Menu {
+ private final Map