후기

[우테코 프리코스] 1주차 피드백

GYUD 2023. 10. 31. 16:48

1주차에서는 기능명세서 작성법, 커밋메시지 작성법, 자바 코드 컨벤션에 초점을 맞추어 과제를 진행했었다.

 

아래는 내가 작성한 1주차 숫자 야구 게임의 코드이다.

https://github.com/Gyu-won/java-baseball-6

 

GitHub - Gyu-won/java-baseball-6

Contribute to Gyu-won/java-baseball-6 development by creating an account on GitHub.

github.com

 

과제를 제출한 이후 코드 리뷰를 진행하였는데, 이 과정에서 코드 작성을 통해 얻었던 것 만큼 많은 점을 배우고 느낄 수 있어서 이를 기록하였다.


잘 작성한 코드는 무엇인가?

아래 코드는 리뷰했던 코드들 중 하나로, 서로 다른 숫자 3개를 정하기 위해 정의한 Triple이라는 콜렉션의 일부이다. 

public record Triple<T>(
        T first,
        T second,
        T third
) {
    public static final int MAX_SIZE = 3;

    public static <T> Triple<T> fromSet(final Set<T> set) {
        final List<T> list = set.stream().toList();
        return Triple.fromList(list);
    }

    public static <T> Triple<T> fromList(final List<T> list) {
        validateTripleSize(list);
        return new Triple<>(
                list.get(0),
                list.get(1),
                list.get(2)
        );
    }

 

내 실력이 부족한 이유도 있겠지만 코드를 처음보자 마자 든 생각은 '너무 어렵다' 이다. Triple이라는 이름 그 의미를 바로알기 어려웠고, 메서드를 본 후에야 숫자 3개를 뜻하는 것임을 이해할 수 있었다. 또한 generic을 사용하여 다양한 데이터 타입을 지원할 필요가 있는지에 대해서도 의문이 들었다. 숫자 야구 게임에서는 올바르지 않은 입력에 대해서 예외를 처리하라는 요구사항이 있기 때문에, T에는 올바른 integer 입력만 들어오게 된다. 이런 경우에는 정확하게 타입을 명시해줘서 가독성을 높이는 것이 더 좋겠다고 생가하였다.

 

다양한 기능을 활용해서 코드를 간단하게 작성하고, 확장성을 고려하는 것도 중요하다. 요구사항이 확정되지 않거나 변경 가능성이 있는 환경에서는 효과적인 유지,보수를 위하여 확장성을 고려하는것이 좋다. 하지만 숫자 야구 게임과 같이 요구사항이 명확하고 간단한 문제에서는 확장성을 고려하는 것 만큼이나 코드의 가독성도 중요하다고 생각한다. 프리코스를 진행하면서 읽기 쉬운면서 유지 보수에 용이한 코드를 작성하는 것을 목표로 설정하고, 이를 달성하기 위해 열심히 노력할 것이다.

 


참고한 코드

https://github.com/woowacourse-precourse/java-baseball-6/pull/1613

 

[숫자 야구 게임] 변해빈 미션 제출합니다. by h-beeen · Pull Request #1613 · woowacourse-precourse/java-baseball

⚾  Precourse-Week1 Mission [숫자 야구] [README.md] 코드리뷰 전 꼭 확인해주세요!! [우아한테크코스] 1주차 숫자야구 풀이 코드 해설 [우아한테크코스] 1주차 숫자야구 사전 기록 [우아한테크코스] 1주

github.com

 

변해빈님의 코드를 리뷰하면서 나와 비슷한 고민을 하면서 코드를 작성하신 것을 느낄 수 있었다. 코드 리뷰를 고민에 대해 같이 이야기 하고, 학습한 내용을 내 코드에 적용해 보면서 훨씬 더 크게 성장할 수 있었다. 코드 리뷰를 통해 학습한 내용과 느낀점을 아래 정리하였다.

 

일급 컬렉션

일급 컬렉션이란 특정한 컬렉션만을 멤버 변수로 갖는 클래스를 의미한다. 1주차 과제에서 일급 컬렉션을 사용하지 않고 사용자 입력에 대해 유효성을 검증한다면 아래와 같이 코드를 작성할 수 있다.

// playerInput을 질의 형식에 맞는지 유효성을 검증하는 코드
validate(playerInput);

// strToIntegerList는 String을 List<Integer> 형태로 변화해주는 코드
List<Integer> guessList = strToIntegerList(playerInput);

 

위 코드의 문제점은 playerInput이 여러개라면 그 때마다 위의 코드를 작성해줘야 한다는 점이다. 이 과정에서 유효성 검사 과정을 생략하고 playerInputList를 생성한다거나, playerInputList의 값이 중간에 변경될 수도 있다. 그리고 validate라는 메서드의 이름만 봐서는 어떤 형식에 대한 유효성 검사를 하는 것인지 알 수 없기 때문에 validateAsGuess라는 형식으로 메서드의 이름을 변경해주어야 그 의미가 명확해진다.

 

이러한 문제점은 아래와 같이 일급 컬렉션을 사용하면 해결할 수 있다.

public class Guess {

    private final List<Integer> guesses;

    public Guess(String playerInput) {
    	validate(playerInput);
        List<Integer> guesses = strToIntegerList(playerInput);
    }
}

위와 같이 Guess라는 클래스를 따로 생성하여 List 컬렉션을 가지는 final 필드를 만들고 유효성 검사를 내부에서 수행하도록 한다. 이렇게 하면 Guess라는 객체가 생성되었다면, 해당 객체는 유효성 검사를 통과한 객체임을 보증할 수 있다. 또한 final로 선언하였기 때문에 guesses의 내용은 변경될 수 없다는 장점도 가진다. 

 

그 외에도 아래와 같은 장점을 가진다.

 

  •  간접 노출: 컬렉션을 반환 할 때 원본을 반환하는 것이 아니라 컬렉션의 복사본을 반환하기 때문에 외부에서 받은 내용을 변경하더라도 원본 내용의 안전성이 보장된다
  • 내부 처리: Number라는 객체를 따로 선언하였기 때문에 모든 컬렉션 관련 비즈니스 로직을 클레스 내부에서 처리할 수 있다.

결국 일급 컬렉션 사용을 통해서 객체와 메서드의 역할을 명확하게 정의할 수 있는 것이다.

 

하지만 일급 컬렉션을 공부하면서 한가지 의문점이 들기도 하였다.

  1. Guess와 같은 인스턴스는 유효성 검증 후에 다른 Model에 전달되었을 때는 단순히 컬렉션의 값을 가져오는 용도로만 쓰이는데 꼭 인스턴스로 만들어서 메모리를 많이 사용해야 하는가?
  2. 실제로 숫자야구를 할 때는 유효성 검증은 질의를 받은 사람이 수행하는데 Guess 객체의 역할이 맞는가?
  3. getter를 사용하지 않는것이 좋다고 하였는데 그러면 어떻게 값을 가져올 수 있는가?

1번과 2번에 대한 궁금증을 찾아보던 중 디스코드에 일급컬렉션과 관련된 글이 있어서 질문하였고 다음과 같은 답을 받을 수 있었다.

즉, 컬렉션의 크기가 너무 크지 않은 한 메모리와 관련하여서 유의미한 차이는 없고, 객체 지향적 설계로 얻는 이점이 더 많다는 것이다. 실제로도 메모리와 관련된 이슈는 정적캐싱과 같은 다른 방법으로 해결 할 수 있었다.

또한 검증을 어디서 하는지는 상관이 없지만 결국 Gues에 대한 검증을 하는 것이기 때문에 객체 내부에서 하는 것이 맞다고 생각하였다. 

 

3번 getter의 사용을 지양하라는 말은 결국 Guess 객체만이 guesses라는 필드값에 접근할 수 있도록 하라고 해석하였다. 즉, 객체간의 역할을 분명히 하고 역할에 따라 객체를 분리하라는 원리와 직결된다.

 

하지만 Guess의 guesses라는 필드와 Answer(컴퓨터가 생성한 정답) answers라는 필드의 값을 비교하여 strike와 ball의 개수를 계산하기 위해서는 두 인스턴스의 필드에 모두 접근해야 하고 이를 위해서 한개의 class로 통합해야 한다. 이렇게 구현한다면 Guess class는 유효성 검사의 메서드만 가져도 되는데 strike와 ball을 계산하는 메서드까지 가지는 것은 객체가 명확하게 분리되지 않은 것이라고 생각하여 1주차 코드에서는 getter를 사용하여 값을 가져오고 strike와 ball 계산은 다른 Result라는 객체에서 수행하였다.

 

코드리뷰를 하면서 나와 비슷한 고민을 하고 있는 분들을 찾을 수 있었고 아래와 같은 방법을 추천 받았다.

위의 내용을 반영하여 2주차 과제에서는 일급컬렉션을 적용해보기로 하였다.

 

 

MVC 패턴

1주차 과제의 코드 리뷰를 진행하며, 많은 사람들이 MVC 패턴으로 객체의 역할을 분리했다는 것을 알 수 있었다. 나 역시 과제를 진행하면서 MVC 패턴에 대해 고민하였지만, 간단한 문제를 해결하는데 View까지 분리하며 역할을 나눌 필요가 없다고 생각하여 적용하지 않았다.

 

아래는 1주차 과제에서 작성한 Computer class의 일부이다.

public class Computer {
    private static final boolean ROUND_ONGOING = true;
    private static final String GAME_START_MESSAGE = "숫자 야구 게임을 시작합니다.";
    private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해주세요 : ";
    private static final String ANSWER_RESULT = "3스트라이크";
    private static final String ROUND_OVER_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료";
    private static final String ASK_TO_CONTINUE_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";

    private static final Player player = new Player();
    private Answer answer;

    public void startGame() {
        System.out.println(GAME_START_MESSAGE);
        boolean continueGame = true;
        while (continueGame) {
            startRound();
            System.out.println(ASK_TO_CONTINUE_MESSAGE);
            continueGame = player.makeRestartFlag().toBoolean();
        }
    }

    private void startRound() {
        boolean roundOngoing = ROUND_ONGOING;
        generateAnswer();
        while (roundOngoing) {
            System.out.print(INPUT_NUMBER_MESSAGE);
            Guess playerGuess = player.makeGuess();
            String resultString = generateResultString(playerGuess);
            System.out.println(resultString);
            roundOngoing = isRoundOngoing(resultString);
        }
        System.out.println(ROUND_OVER_MESSAGE);
    }

 

코드의 상단에는 출력과 관련된 상수들이 선언되어 있고, 중간중간에 출력문이 작성되어 있다. 프로그램의 흐름에 맞게 작성하기 결과를 출력하기 위해서 위와 같이 작성한 것이지만 출력부분은 Controller가 수행해야하는 역할은 아니라고 생각되었다.

 

처음에는 출력 관련 상수를 포함한 모든 상수들을 저장하는 클래스로 분리하는 방법을 생각하였다. 하지만 쓰임이 다른 상수들을 한곳에 모으는 것은 상수의 의미 전달을 방해한다고 생각하여 적용하지 않았다.

 

하지만 출력과 관련된 코드를 분리한 코드를 보면서 객체의 역할이 더 명확하게 정의되었다고 생각하였다. 그래서 View라는 객체에서 출력과 입력의 역할을 수행하도록 분리하였고, 그 결과 자연스럽게 Model, View, Controller 구조로 나뉘어졌다. 무작정 MVC 패턴을 적용하는 것이 아닌 필요에 의해서 구조가 이렇게 변경된 것이다.

 

따라서 2주차 과제도 MVC 패턴을 적용하여 문제를 해결하였다. 그 과정에서 객체간의 의존성을 최소화하며 효율적인 MVC 패턴을 어떻게 구현할 수 있을지 깊게 고민하였다.

 

커스텀 예외 사용

1주차 과제에서는 잘못된 입력에 대한 IllegalArgumentException 예외를 발생시키라는 요구사항이 있었다. 그래서 사용자가 입력을 잘못 할 때마다 아래와 같이 예외를 발생시키도록 코드를 작성하였다.

if (input.isEmpty() || input.length() > MAX_LENGTH) {
    throw new IllegalArgumentException();
}

하지만 IllegalArgumentException이라는 예외명은 사용자의 잘못된 입력이라는 오류를 나타내기는 적적하지 않다. 뿐만 아니라 예외를 IllegalArgumentException이 아니라 RuntimeException으로 수정해야 한다면 코드를 하나한 일일이 바꿔야한다는 단점이 있었다.

 

이 문제 역시 코드리뷰를 통해 답을 얻을 수 있었다. Custom Exception을 정의하여 이러한 문제를 해결하신 분들을 찾을 수 있었고, 이를 2주차에 적용하였다.

package racingcar.exception;

public class InvalidInputException extends IllegalArgumentException {

    private InvalidInputException(ErrorMessage errorMessage) {
        super(errorMessage.getMessage());
    }

    public static InvalidInputException with(ErrorMessage errorMessage) {
        return new InvalidInputException(errorMessage);
    }
}


public enum ErrorMessage {
    INVALID_LENGTH("Car name's length is empty or longer than 5"),
    DUPLICATE_NAME("Car name is duplicated."),
    INVALID_NUMBER("Number of move should be non-negative integer");

    private final String message;

    ErrorMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

 


마무리

코드 리뷰를 하는 과정에서 내가 고민했던 부분들을 똑같이 고민한 흔적을 쉽게 찾을 수 있었다. 그리고 고민에 대한 해결법을 각자 다른 방식으로 코드에 표현하였다.

 

이 중에서 가장 잘 작성했다고 생각한 코드는 읽기 쉬운 코드였다. 코드를 읽는 입장에서 바라보니, 어려운 문법을 사용하여 작성한 코드 보다는 프로그램의 순서가 명확하게 이해되는 코드가 가장 눈에 띄었다. 코드 리뷰를 통해서 한번 더 읽기 쉬운 코드의 중요성을 체감할 수 있었고, 다양한 방식의 접근법 역시 배울 수 있었다.

 

앞으로

코드리뷰를 통해 학습한 내용을 2주차 과제에 적용해 볼 것이다. 1주차에서는 내 코드를 보여주는 것이 부끄러워서 주로 다른 사람의 코드를 리뷰하며 학습하였다. 코드 리뷰 과정에서 다양한 의견을 공유하는 모습을 보면서 리뷰를 통해 훨씬 더 크게 성장할 수 있음을 깨달았고, 내 코드에 대해서도 같이 이야기 하고 싶다는 생각을 하였다. 2주차에는 다른 사람의 코드를 리뷰하는 것 뿐만 아니라, 내 코드도 공유하여 함께 이야기해보고 싶다.