숫자 야구 게임 미션 회고

손효재·2023년 2월 8일
1

구현한 미션을 리뷰하며 어떤 것을 배웠고 어떤 점이 부족했는지 정리합니다.
숫자 야구 게임 미션 코드 Github

✅ 도메인 로직 테스트 커버리지

🚀 미션 개요 - 기능 요구 사항

기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.

  • 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼,같은 수가 전혀 없으면 포볼 또는 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
  • 상대방(컴퓨터)는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다.
    게임 플레이어는 컴퓨터가 생각하고 있는 3개의 숫자를 입력하고,
    컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
  • 이 같은 과정을 반복해 컴퓨터가 선택한 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, @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에서 관리하면서 해당 객체가 범위를 관리하고,
자연스럽게 객체를 꺼내쓰지 않고, 객체에 메시지를 보내서 사용할 수 있도록 개발할 수 있을 것이다.

테스트만을 위한 생성자나 setter 사용에 대한 고민

3회의 스트라이크로 게임이 종료되는 테스트를 위해 reportCount(STRIKE) 메서드를 3번 반복하여 3회의 스트라이크를 만드는 과정이 비효율적이라고 생각하여 setter를 사용하려했다.
하지만, 오직 테스트를 위해 setter 메서드를 사용하더라도 setter를 열어놓는건 좋지 않다고 판단했다.
setter를 사용하지 않고, 생성자를 사용해 해당 상태를 가지는 객체를 만드는 방법도 있을 것 같다.
하지만 이또한 생성자를 열어 특정 상태를 가지는 객체를 만들 수 있기 때문에,
과연 테스트만을 위해 생성자나 setter를 여는 것이 좋은것인지에 대한 고민이 남아있다.

private 메서드의 핵심로직이 있다면, 이를 테스트하기 위해 default 메서드로 변경해서 테스트할 수도 있다.

  1. 하지만, 과연 private 메서드를 테스트하기 위해 default로 여는게 좋은 것인가?
  2. private 메서드를 테스트해야한다면 해당클래스가 아닌 다른 클래스에 존재해야하는 것은 아닌지도 생각해볼 수 있겠다.

✍🏻 회고

이번 미션을 통해 객체지향적인 개발에 대해 학습하고, 테스트코드를 꼼꼼하게 작성하면서
리팩토링 하기 쉬운 구조로 만들기 위해 노력했다.
학습한 내용을 가져가면서 고민했던 내용과 부족한 점을 앞으로 개발을 진행하면서 계속 신경써야겠다.

  • 잘 만들어 놓은 테스트 코드는 리팩토링하는데 있어 높은 신뢰도와 리팩토링 속도를 체감할 수 있었다.
  • TDD로 시작이 어려울때는 TODO LIST를 작은 단위로 쪼갠뒤에, 작은 단위부터 구현해 나가자!
  • 객체에서 데이터를 직접 꺼내쓰려하지말고, 객체에 메시지를 보내는 습관을 가지자!
  • 매개변수로 더 많이 넣어가면서까지 dept를 줄이는게 좋은것인가?
    어떤 방법이 클린한 코드인지 계속 고민해보자

💻 실행결과

0개의 댓글