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 menu; + + public Menu(final Map menu) { + this.menu = menu; + } - public int getPrice(final String menuName) { - // TODO: API에 매몰되어 과하게 사용하여 생긴 코드입니다. 간단한 코드로 리팩토링해보세요. - return menu.entrySet() - .stream() - .filter(e -> e.getKey().equals(menuName)) - .map(Map.Entry::getValue) - .findFirst() - .orElse(0); + public int getPrice(final String menuName) { + // TODO: API에 매몰되어 과하게 사용하여 생긴 코드입니다. 간단한 코드로 리팩토링해보세요. + return menu.entrySet() + .stream() + .filter(e -> e.getKey().equals(menuName)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(0); + } } - } - final var menu = new Menu(Map.of( - "쌈밥", 11_000, - "김치찌개", 9_000, - "비빔밥", 10_000, - "평양냉면", 15_000 - )); + final var menu = new Menu(Map.of( + "쌈밥", 11_000, + "김치찌개", 9_000, + "비빔밥", 10_000, + "평양냉면", 15_000 + )); - final int 평양냉면_가격 = menu.getPrice("평양냉면"); + final int 평양냉면_가격 = menu.getPrice("평양냉면"); - assertThat(평양냉면_가격).isEqualTo(15_000); + assertThat(평양냉면_가격).isEqualTo(15_000); + } } }