구현한 미션을 리뷰하며 어떤 것을 배웠고 어떤 점이 부족했는지 정리합니다.
숫자 야구 게임 미션 코드 Github
기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.
메서드 명과 메서드의 동작이 일치하도록 구현하고, 그에 맞는 하나의 책임만 가지는 메서드를 만들도록 신경써야겠다. 메서드 명으로 동작이나 값을 예상하기 쉬운 좋은 코드를 만들어야 한다.
특히, 자료구조를 메서드나 변수명에 많이 사용했는데(xxList, xxSet) 그렇게 되면 해당 자료구조를 사용했다고 생각할 수도 있고, 자료구조가 변경되면 메서드명이나 변수명도 함께 수정해야할 일이 생기게 된다.
이를 개선하면서 관심사에 맞는 클래스명, 변수명을 사용하게 되었다.
우리의 관심사는 내부에 있는 값임을 명심하자!
아래의 랜덤값을 만드는 클래스에서 1~9까지의 숫자 생성이 자주 일어날 것으로 예상된다.
따라서, 한번 만들어두고 재사용할 수 있도록 정적 변수로 초기화하여 계속 사용할 수 있게 리팩토링했다.
public class GenerateNumber {
private static final List<Integer> NUMBERS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
public List<Integer> getRandomNumbers() {
Collections.shuffle(NUMBERS);
return NUMBERS.stream()
.limit(3)
.collect(Collectors.toList());
}
}
이처럼, 랜덤값뿐만 아니라 자주 만들어질 수 있는 공의 번호도 만들어두고 재사용할 수 있겠다.
공의 번호는 일급 객체(BallNumber)로 만들어 정적 팩터리 메서드를 사용해서 리팩토링 할 수 있겠다.
공을 비교할 때 테스트하기 어려운 랜덤값과 관련된 부분을 분리하여 테스트하기 쉬운 구조로 만들었다.
controller 에서 생성된 랜덤값을 파라미터로 가져와 Ball 객체에서는 파라미터로 넘어온 공의 번호와 비교하도록 구현하면, 랜덤값과 분리되어 테스트하기 쉬운 구조로 개선할 수 있다.
아래와 같이 @ParameterizedTest와 @ValueSource, @CSVSource를 활용하면,
하나의 테스트 메서드로 여러개의 파라미터에 대해 테스트할 수 있어 중복이 줄고, 가독성이 좋아진다.
테스트 값으로 경계값을 사용해 꼼꼼하게 테스트하자!
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
@DisplayName("1~9까지 번호를 가진다")
void number_range_테스트(int number) {
assertThat(new Ball(number)).isEqualTo(new Ball(number));
}
@ParameterizedTest
@ValueSource(ints = {0, 10})
@DisplayName("1~9이외의 번호는 예외를 발생한다")
void number_range_exception_테스트(int number) {
assertThatThrownBy(() -> {
assertThat(new Ball(number)).isEqualTo(new Ball(number));
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("올바른 숫자가 아닙니다");
}
또한, 객체가 같음을 테스트하기 위해 equals는 자주 재정의된다.
재정의된 equals 메서드로 같음을 비교하여 같은 객체임을 테스트하자
일반적인 메서드 순서를 따라서 구현했지만, 코드의 가독성이 좋다고 생각되지 않는 곳들이 있었다.
중요한 로직의 코드에서 메서드 분리로 인해 호출하는 private 메서드는 아래로 내려가게 되면서,
로직을 이해하기 위해 가독성이 떨어짐을 느꼈다.
접근 제어자에 얽매이지 않고, 관련된 메서드를 연속해서 배치하여 가독성을 높이는 것도 하나의 방법이라고 생각했는데, 어떤 메서드 순서의 배치가 좋은지 계속 고민해야겠다.
* 일반적인 메서드 순서로 리팩토링
1. static 변수 → instance 변수 (public-protected-private)
2. 생성자
3. main메서드
4. static 메서드 → 메서드 (기능 및 역할별 분류) → 스탠다드 메서드 (toString, equals, hashcode)
5. getter, setter 메서드
개발하면서 객체에서 데이터를 직접 꺼내서 사용할때가 많았던 것 같다.
객체지향적으로 개발하기 위해 객체에 메시지를 보내는 습관을 가지면서 개발해야함을 느꼈다.
원시값을 포장해서 일급 객체로 관리하는 것이 부족했다.
공(Ball) 객체가 가지는 숫자(number)를 BallNumber 일급 객체로 관리하도록 리팩토링할 수 있겠다.
그로인해, 숫자의 범위를 Ball이 아닌 BallNumber에서 관리하면서 해당 객체가 범위를 관리하고,
자연스럽게 객체를 꺼내쓰지 않고, 객체에 메시지를 보내서 사용할 수 있도록 개발할 수 있을 것이다.
3회의 스트라이크로 게임이 종료되는 테스트를 위해 reportCount(STRIKE) 메서드를 3번 반복하여 3회의 스트라이크를 만드는 과정이 비효율적이라고 생각하여 setter를 사용하려했다.
하지만, 오직 테스트를 위해 setter 메서드를 사용하더라도 setter를 열어놓는건 좋지 않다고 판단했다.
setter를 사용하지 않고, 생성자를 사용해 해당 상태를 가지는 객체를 만드는 방법도 있을 것 같다.
하지만 이또한 생성자를 열어 특정 상태를 가지는 객체를 만들 수 있기 때문에,
과연 테스트만을 위해 생성자나 setter를 여는 것이 좋은것인지에 대한 고민이 남아있다.
이번 미션을 통해 객체지향적인 개발에 대해 학습하고, 테스트코드를 꼼꼼하게 작성하면서
리팩토링 하기 쉬운 구조로 만들기 위해 노력했다.
학습한 내용을 가져가면서 고민했던 내용과 부족한 점을 앞으로 개발을 진행하면서 계속 신경써야겠다.