공부함

프리코스 1주차 미션 본문

우테코 6기 프리코스

프리코스 1주차 미션

찌땀 2023. 10. 29. 19:47

https://github.com/jd99iam/java-baseball-6

 

GitHub - jd99iam/java-baseball-6

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

github.com

프리코스 1주 차 미션은 숫자 야구 미션이었다. 

미션을 진행하면서 느끼고 배운 점을 정리해보고자 한다

 

검증을 어디서 해야 할까?

다양한 검증 기능 (입력 값 형태 검증, 입력 값이 숫자인지 검증, 입력 값의 볼 스트라이크 검증..)을 어느 클래스에서 구현해야 할까?

원래는 검증만 하는 Validator 클래스를 만들어서 여기서 모든 검증을 하려고 했다. 

 

하지만 입력 값의 볼, 스트라이크를 검증하는 것은 Answer 클래스에서 진행해야 한다고 생각이 바뀌었다.

왜냐하면 Answer 클래스의 필드값으로 정답 정보를 갖고 있다. 

Validator 클래스에서 검증을 하려면 이 값을 전달해줘야 한다. 

하지만 Answer 클래스에서 검증하면 그럴 필요가 없다. 

그리고 Answer 클래스의 상태(정답)인 필드값과 입력값을 비교해야 하므로

Answer 클래스에서 진행하는 게 맞다고 생각했다. 

 

반면 입력값 형태 검증은 Validator에서 진행하는게 맞다고 생각했다.

왜냐하면 사용자가 입력한 입력값을 필드로 관리하는 클래스가 있는 것이 아니었기 때문이다. 

 

나는 MVC 패턴을 사용하지는 않았지만, MVC 패턴에서 검증을 어디서 해야 할지 디스코드 채널의 토론하기에 질문이 있었다. 

이에 대해 참가자들은 

  • 그 값을 담당하는 객체가 검증해야 한다. "내 값은 내가 검증한다"가 객체의 책임이기 때문이다 
  • 뷰에서는 도메인 규칙과 관련 없는 입력 자체에 대한 것만 검증한다 
  • Input level 에서는 입력이 들어왔는지, 입력이 문맥에서 정확한 타입인지 검증한다 
  • Domain level에서는 비즈니스 정책에 따라 해당 데이터와 관련된 책임을 처리해야하는 객체가 생성시점에 객체에 대한 추가검증 없이 완전한 상태로 쓸수있는지 검증

이러한 의견들이 있었다. 어느정도 공감가는 내용들이다. 

 

기능 목록 작성 

나는 문제의 요구사항을 보고 Readme를 작성했다. 

이렇게 작성하고 기능별로 객체를 나눠서 구현하려고 생각했었다.

하지만 작성한 내용이 구체적이지 않아 구현하면서도 계속 설계를 고민하느라 구현하는데 시간이 오래 걸렸다. 

 

반면 상호 코드리뷰를 진행한 사람의 Readme이다. 기능을 훨씬 구체적으로, 자세하게 작성했다.

다른 사람들의 Readme를 보면서 나도 더 구체적으로 작성해야 겠다고 느꼈다. 

 

객체 필드로 선언할 값들 

public final class Answer {
    private final List<Integer> answer;
    private int ball;
    private int strike;
    ...
    }

원래 게임의 정답 정보를 담은 Answer 클래스는 위와 같았다. 

ball, strike 필드가 있는 이유는 채점 결과를 생성하기 위해서였다. 

채점을 Answer 클래스 내부에서 하기 때문이다. 

필드를 선언해서 사용하면 채점 결과를 생성할 때 메서드에 따로 인자를 전달할 필요도 없어서 좋다고 생각했다.

하지만 위와 같이 작성하고 commit 후 push 하려고 하자 Intellij에서 해당 필드는 local varaible로 선언할 수 있다고 경고를 줬다. 

확인해 보니 ball, strike는 채점 결과를 생성하는 메서드에서만 사용되고 있었다. 

즉 Answer 클래스의 상태가 아닌 것이다. 따라서 Answer 클래스의 필드로 선언하기 부적절하다고 생각해서 

필드에서 제외하고 로컬 변수로 선언했다. 

 

이렇게 특정 함수에서만 사용되는 값이 있다면 해당 값이 이 객체의 상태가 맞는지 확인해보고 아니라면 

지역 변수로 선언하는게 맞는 것 같다. 왜냐하면 객체의 필드는 객체의 상태를 저장하는 값이기 때문이다. 

 

불변 객체 

https://mangkyu.tistory.com/131

 

[Java] 불변 객체(Immutable Object) 및 final을 사용해야 하는 이유

클린코드를 읽어도, 이펙티브 자바를 읽어도, 개발을 잘하는 팀의 얘기를 들어도 항상 좋은 코드를 얘기할 때면 불변의 객체를 필연적으로 접하게 되는 것 같습니다. 그래서 이번에는 불변의 객

mangkyu.tistory.com

미션을 진행하던 도중 위 글을 통해 불변 객체에 대해서 알게 되었다.

글에서는 내부 상태가 변하지 객체를 불변 객체로 생성하면 다양한 이점이 있다고 설명한다. 

 

가장 와닿았던 것은 불변성이 고장된 코드 리딩이다. 

불변성이 보장된 객체는 내부를 보지 않아도 객체가 불변이라는 것을 알아 시간을 절약할 수 있으며, 

객체가 여러 메서드에 호출되어도 값이 변하지 않으리라는 확신을 갖고 호출할 수 있다. 

 

		// 컴파일 오류 
		final int num = 1;
		num = 10;
		
		
		// 컴파일 오류 X 
		final List<Integer> list = new ArrayList<>();
		list.add(10);

final 키워드를 통해 변수를 생성하면 불변성을 확보할 수 있다.

하지만 list.add처럼 상태를 변경하는 것은 가능하다. 

이러한 경우를 막기 위해 불변 클래스를 생성해주어야 한다. 

 

불변 클래스는 

  1. 클래스를 final로 선언하라
  2. 모든 클래스 변수를 private와 final로 선언하라
  3. 객체를 생성하기 위한 생성자 또는 정적 팩토리 메소드를 추가하라
  4. 참조에 의해 변경가능성이 있는 경우 방어적 복사를 이용하여 전달하라 

이렇게 4가지 규칙을 지켜서 선언하면 된다. 

 

특히 생성자를 private으로 돌리고 정적 팩터리 메서드를 활용할 수 있다.

 

Java에서는 배열이나 객체의 참조를 전달한다. 

참조를 통해 원본을 수정할 수 있기 때문에 이를 막기 위해 사용하는 것이 방어적 복사이다. 

방어적 복사는 참조가 아닌 배열이나 객체의 내부를 복사해서 반환하는 것이다.

 

    public List<String> getList() {
        return Collections.unmodifiableList(list);
    }

 

리스트를 전달할 때 이런 식으로 방어적 복사를 할 수 있다. 


아래부터는 리뷰받은 내용들+리뷰하면서 느낀 점들이다. 리뷰가 도움이 많이 됐다. 

불필요한 단계 

        Map<String, Integer> gradeResultMap = compareAnswerWithInput(input);

        return GradeResult.of(getBallCount(gradeResultMap), getStrikeCount(gradeResultMap));

나는 위와 같이 Map을 생성하고 Map을 토대로 GradeResult 객체를 만들었다.

하지만 리뷰어 입장에서 굳이 이렇게 한 이유를 모르겠다고 한다. 바로 GradeResult 객체를 만들어도 되기 때문이다. 

리뷰를 보니 나도 왜 그랬는지 모르겠다. 바로 GradeResult 객체를 생성하는 게 맞는 것 같다. 

 

    public static void validateAnswerInput(String answerInput) {

        if (invalidInputLength(ANSWER_INPUT_LENGTH, answerInput)) {
            throw new IllegalArgumentException();
        }

        List<Character> checkDuplicateList = new ArrayList<>();

        for (char input : answerInput.toCharArray()) {
            if (!Character.isDigit(input)) {
                throw new IllegalArgumentException();
            }
            if (input == INVALID_INPUT_CHAR) {
                throw new IllegalArgumentException();
            }
            if (checkDuplicateList.contains(input)) {
                throw new IllegalArgumentException();
            }
            checkDuplicateList.add(input);
        }
    }

validateAnswerInput은 입력값을 검증하는 메서드다. 나는 입력받은 String을 1차로 위 메서드에서 검증한다. 

그 과정에서 char 배열로 변환해 isDigit을 사용해 입력값이 숫자 형태인지 검증한다. 

 

    public GradeResult gradeInput(String answerInput) {

        List<Integer> input = new ArrayList<>();

        for (char number : answerInput.toCharArray()) {
            input.add(charToInt(number));
        }
        
        Map<String, Integer> gradeResultMap = compareAnswerWithInput(input);

        return GradeResult.of(getBallCount(gradeResultMap), getStrikeCount(gradeResultMap));
    }

그리고 나서 채점 결과를 생성하기 위해 위와 같이 String -> char -> Integer의 단계로 입력을 변환했다. 

즉 toCharArray를 두번 사용하게 된다. 

 

    private Integer convertStringToInteger(String input) {
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(INPUT_NOT_NUMBER);
        }
    }

반면에 내 코드를 리뷰해주신 분의 코드다. 

입력을 Integer로 변환하면서 NumberFormatException이 터지는지 확인해 입력 형태를 검증하고, 

Integer로 반환해 그 값을 사용한다. 

이렇게 짜는 편이 훨씬 효율적인 것 같다. 

 

불필요한 싱글톤 

나는 게임 전체를 관리하는 GameManager 객체를 싱글톤으로 구현했는데 그 이유를 리뷰어가 물었다. 

그런데 막상 대답하려니 이유를 제대로 설명할 수가 없었다.

왜냐하면 "당연히 GameManager는 하나만 있어야 하는거 아닌가?" 라고 생각하고 구현했기 때문이다!

싱글톤은 객체를 여러 곳에서 공유할 때, 생성자가 여러 곳에서 호출되는데,

이 때 객체 생성이 하나만 되도록 하는 데 의미가 있다. 

따라서 숫자 야구 미션에서 GameManager를 여러 곳에서 사용하거나 생성자를 호출하는 것이 아니므로 굳이 싱글톤이 필요없었던 것 같다. 

 

String 생성 

`StringJoiner`나 `StringBuilder`, `String.format`을 나는 거의 사용해보지 않았다. 

무식하게 `+`로 생성해왔는데, 리뷰어가 위 방식들을 사용해 볼 것을 추천했다.

그래서 2주차 미션은 위 기능들을 적극적으로 사용해보고자 한다! 

https://blog9909.tistory.com/16

 

StringBuilder, StringJoiner, String.format

우테코 프리코스 1주차 미션을 하면서 나는 +를 이용해 문자열 생성을 했다. 리뷰어가 StringBuilder, StringJoiner, String.format을 사용해서 관리하는게 어떻겠냐고 조언을 해줬다. 그래서 각 내용에 대해

blog9909.tistory.com

StringBuilder, StringJoiner, String.format에 대해 간단하게 정리한 글이다. 

 

상수 파일 따로 분리하기 

package baseball.constant;

public class PrintMessage {

    public static final String GAME_START = "숫자 야구 게임을 시작합니다.";
    public static final String USER_INPUT = "숫자를 입력해주세요 : ";
    public static final String GAME_OVER = "3개의 숫자를 모두 맞히셨습니다! 게임 종료";
    public static final String IS_RESTART = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";
    public static final String BALL_FORMAT = "%d볼";
    public static final String STRIKE_FORMAT = "%d스트라이크";
    public static final String NOTHING_FORMAT = "낫싱";

}

 나는 클래스에서 필요한 상수를 클래스 내에 몽땅 집어 넣었는데 이렇게 상수 파일을 따로 분리하는게 훨씬 깔끔한 것 같다! 

특히나 여러 곳에서 사용하는 상수 파일이라면 분리해서 재사용성을 높이는게 맞는 것 같다. 

 


1주차 미션을 진행하면서 많이 배웠다. 

특히 설계과정과 코드리뷰 과정에서 많이 배운 것 같다. 

이제 힘내서 2주차 미션을 진행해보자.

우하하

'우테코 6기 프리코스' 카테고리의 다른 글

MVC 패턴  (0) 2023.11.12
1주차 숫자야구 피드백  (0) 2023.11.06
프리코스 2주차 미션  (0) 2023.10.30