우테코 6기 프리코스 1주차 회고

eora21·2023년 10월 25일
0

자세한 미션 내용과 코드 작성은 깃허브를 참고해주시면 감사하겠습니다.

시작 전

프리코스를 어떠한 마음가짐으로 진행할 것인가

그동안 여러 프로젝트를 하며 가장 아쉬웠던 것이 '기록 유무'였습니다. 깊게 고민한 내용들에 대해 작성하고 정리한 프로젝트는 회고나 포트폴리오를 작성할 때 많은 도움이 되었으나, 그렇지 못 한 경우 '해당 프로젝트에서는 어떤 것을 배웠나요?'라는 질문에 쉽게 대답하지 못 했기 때문입니다.
이번 프리코스에서는 고민한 내용을 충분히 정리하여 기록을 남기고자 합니다.

JAVA 버전의 변화

이번 프리코스에서는 가장 많이 쓰이던 자바 11버전이 아닌 17버전이 도입되었습니다. 스프링 버전이 3으로 올라가면서 최소 JAVA 버전이 17로 지정되었는데, 그 영향이 아닌가 싶습니다.
버전별로 어떠한 점들이 반영되었는지에 대해서는 해당 블로그 글을 참조하면 좋을 듯 합니다.

Record

11 -> 17 버전에서 가장 눈여겨 보아야 할 점은 Record입니다. 그동안 불변 객체를 선언하려면 private final@Getter를 작성하고, 그 외에도 EqalsHashcode 등을 Override했어야 했는데 이러한 것들을 간단히 적용하는 하나의 선언입니다.
따라서 이번 1주차 프리코스에서는 Record를 적극적으로 사용해보기로 하였습니다.

우테코 코드 스타일 적용

이번에 Mac을 새로 구입하였기 때문에, 코드스타일도 다시 적용할 필요가 있었습니다.
해당 링크에서 코드 스타일을 다운받은 후, 인텔리제이 설정 항목에 들어갑니다.

코드 스타일 항목을 찾아옵니다.

방금 다운받은 우테코 코드 스타일을 지정했습니다.

.gitignore 업데이트

Mac에서 생성하는 .DS_Store 파일과 README.md를 유용하게 수정하기 위한 Obsidian에서 생성하는 파일은 프로젝트와는 무관한 파일들이라 볼 수 있습니다. 따라서 git이 해당 파일들을 추적하지 않도록 .gitignore를 업데이트했습니다.

### DS_Store ###
.DS_Store

### Obsidian ###
docs/.obsidian/

git cz 설치

커밋메시지를 효율적으로 남기기 위해 git cz를 설치하였습니다.

진행하며

Record 사용

값의 집합으로 이루어진 불변 객체를 쉽게 정의할 수 있도록 고안되었습니다.
위에 작성했듯 매번 개발자가 작성하던 메서드들을 알아서 구현해줍니다.
이하 직접 사용해보며 알게 된 점들입니다.

private 생성자 불가

아쉽게도, Record는 생성자로 Record 자체와 동일한 가시성을 가지게끔 설계되었다고 합니다(이는 Record를 확인할 수 있는 곳이라면 어디서든 액세스할 수 있고, 만들 수 있도록 설계된 부분이라고 합니다).

따라서 private 생성자를 지정할 수 없었고, 정적 팩토리 메서드 패턴만으로 외부에서의 생성을 제한하고 싶은 경우에는 class로 지정해야 했습니다.

이는 옳은 설계라는 생각도 듭니다. 관련된 값들을 모아 불변으로 지니고 있는 단순 데이터 클래스인데, 이용에 제한을 두는 것이 더 이상합니다.

다만 의문이 생겼습니다. 만약 Record가 단순 데이터의 집합이라고 한다면, 랜덤한 Record를 만드는 것은 Record 자체의 책임일까요, 아니면 Record를 이용하는 특정 클래스의 책임일까요?

public record BaseballNumber(int value) {
    public static int START_NUMBER = 1;
    public static int END_NUMBER = 9;
    public BaseballNumber {
        if (value < START_NUMBER || END_NUMBER < value) {
            throw new IllegalArgumentException();
        }
    }

    public static BaseballNumber createRandomNumber() {
        return new BaseballNumber(pickNumberInRange(BaseballNumber.START_NUMBER, BaseballNumber.END_NUMBER));
    }
}

어디까지나 단순 데이터의 집합으로 본다면, 외부에서 제공되는 값으로만 해당 객체가 생성되어야 하기 때문에 내부적으로 랜덤값을 반환하는 것은 책임의 범위에서 벗어날 수도 있습니다.

다만 해당 Record는 제한된 범위(1~9)가 있으며, 해당 범위 내에서의 유효한 Record를 제공해 주는 것은 Record 자체의 책임이라 생각하여 위와 같은 코드를 작성하게 되었습니다.

다른 분들은 어떻게 생각하시는지 궁금합니다.

내부 값 할당 타이밍 주의

public record GameResult(int tryCount, int correctAnswerCount, int similarAnswerCount) {
    public GameResult {
        if (tryCount() < correctAnswerCount() + similarAnswerCount()) {
            throw new IllegalArgumentException();
        }
    }
}

게임 결과값을 만들 때 위와 같은 코드로 유효하지 않은 결과가 생성될 시 예외를 작성하도록 하였습니다.
그러나 테스트 코드 동작 시, 원하던 예외가 던져지지 않았습니다.

@Test
@DisplayName("유효하지 않은 게임 결과")
void invalidGameResultTest() {
    assertThrows(IllegalArgumentException.class, () ->
            new GameResult(3, 2, 2));
}

그 이유는 if문 내에서 메서드를 통한 값 확인을 했기 때문입니다.
Compact Constructor의 조건이 다 확인된 후 전달된 값이 내부 필드에 할당되는데, 아직 값 할당이 되지 않아 메서드를 통한 값은 모두 0이 넘어온 경우입니다.

따라서 메서드 대신 전달된 파라미터 값을 통해 예외를 발생시키도록 수정하였습니다.

public GameResult {
    if (tryCount < correctAnswerCount + similarAnswerCount) {
        throw new IllegalArgumentException();
    }
}

메서드를 사용할 때도 마찬가지

public GameResult {
    if (isOverTryCount() || isOneSimilarAllOthersCorrect()) {
        throw new IllegalArgumentException("존재할 수 없는 게임 결과입니다.");
    }
}

private boolean isOverTryCount() {
    return tryCount < correctAnswerCount + similarAnswerCount;
}

private boolean isOneSimilarAllOthersCorrect() {
    return tryCount == correctAnswerCount + similarAnswerCount && similarAnswerCount == 1;
}

예외 조건을 하나 더 붙이면서, 검증 메서드를 하나 더 추가하면서 if문 내에 검증 메서드들을 넣었습니다.
그러나 위 항목과 같이, 아직 내부 값에는 아무것도 할당된 게 없기 때문에 모두 0이 들어가 있으므로 예외를 제대로 잡아낼 수 없습니다.

public GameResult {
    if (isOverTryCount(tryCount, correctAnswerCount, similarAnswerCount)
            || isOneSimilarAllOthersCorrect(tryCount, correctAnswerCount, similarAnswerCount)) {
        throw new IllegalArgumentException("존재할 수 없는 게임 결과입니다.");
    }
}
private boolean isOverTryCount(int tryCount, int correctAnswerCount, int similarAnswerCount) {
    return tryCount < correctAnswerCount + similarAnswerCount;
}
private boolean isOneSimilarAllOthersCorrect(int tryCount, int correctAnswerCount, int similarAnswerCount) {
    return tryCount == correctAnswerCount + similarAnswerCount && similarAnswerCount == 1;
}

이렇게 파라미터로 값을 전달해주는 방식으로 변경하여야 제대로 반영됩니다.

public GameResult(int tryCount, int correctAnswerCount, int similarAnswerCount) {
    this.tryCount = tryCount;
    this.correctAnswerCount = correctAnswerCount;
    this.similarAnswerCount = similarAnswerCount;
    
    if (isOverTryCount() || isOneSimilarAllOthersCorrect()) {
        throw new IllegalArgumentException("존재할 수 없는 게임 결과입니다.");
    }
}
private boolean isOverTryCount() {
    return tryCount < correctAnswerCount + similarAnswerCount;
}
private boolean isOneSimilarAllOthersCorrect() {
    return tryCount == correctAnswerCount + similarAnswerCount && similarAnswerCount == 1;
}

혹은 이처럼 값을 먼저 할당한 후 검증 메서드를 사용하여 가독성을 높이는 법이 있을 것 같습니다.
두 방법 중 어떤 게 더 나아보이신가요?

View의 역할을 어디까지 가져갈 것인가?

초기 설계 시 View는 어디까지나 값을 받고, 보여주는 것으로 생각하였습니다.
그러나 현재와 같은 Console이 아닌, 터치스크린 등으로 값을 받고 출력하는 경우 많은 문제점이 있을 것이라 판단하여 View의 역할에 대해 고민하기 시작했습니다.

Console의 범위에서 벗어나자면

Console을 통해서는 단순 String값을 받고, 내보내면 됩니다.
다만 다른 입출력 장치를 통해 해당 작업들이 시행될 경우, 메시지를 보여주는 방식부터 차이가 날 수 있습니다.
알림창이나 전광판 등으로 보여줄 수도 있고, 아예 점등하는 LED를 통해 몇 개를 맞췄는지 혹은 화려한 이펙트를 통해 완벽한 답을 이뤄냈는지 등으로 말이죠.

그렇다면 값을 보여주는 것은 단순 sout 메서드 하나만으로 해결되지 않으리란 것을 깨달았습니다. 따라서 게임 시작, 숫자 요구, 게임 결과 확인, 게임 종료 메시지 등을 각각의 메서드로 만들어 구현했습니다.

public interface OutputView {
    void gameStart();

    void requestNumber();

    void showGameResult(GameResult gameResult);

    void goodGame(final int targetSize);

    void areYouWantStopGame(final String moreValue, final String stopValue);
}

게임 중단 여부 메서드는 값을 작성하는 입력 장치 내에서 결정한 값이 필요했기에 파라미터로 전달받을 수 있도록 했습니다.

값 입력 역시 사용자의 숫자를 받거나 게임 중단 여부를 결정해야 했으므로 메서드를 분리하였습니다.

public interface InputView {
    List<Integer> getPlayerNumbers();

    String moreGameValue();

    String stopGameValue();

    boolean isWantStopGame();
}

또한 내부적으로 게임을 더 할지, 중단할지에 대해 어떤 입력으로 결정했는지를 전달하도록 value값을 반환하는 메서드 또한 작성했습니다.

List<Integer>boolean을 반환하도록 한 것도 View가 요구사항에 대한 올바른 데이터 타입을 반환하도록 하기 위함이고, 이 또한 View의 책임 범위라는 생각을 바탕으로 진행하였습니다.

마치며

코드 작성은 몇시간밖에 걸리지 않았으나, 고민하는 시간이 훨씬 길었습니다.
문제에 대한 구현보다, 어떠한 구조를 지니는 것이 더 나은지에 대해 스스로 결정하고 이를 다른 사람도 수긍할 수 있게끔 하려 했습니다.
하지만 오히려 고민이 더더욱 커지는 시간이었다고 생각합니다. 다른 분들의 코드 구조를 보고 많이 배우고자 합니다.

profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글