우아한 테크코스 2주차

MINJU·2022년 11월 8일
1
post-thumbnail

기능 단위 분리를 위해 일단 무작정! 필요 코드를 작성해보고자 한다.
처음부터 제대로 하려고하면 .. 더 진도가 나가지 않음을 알아냈기 때문에 ..^___^

실행 결과 예시

숫자 야구 게임을 시작합니다.
숫자를 입력해주세요 : 123
1볼 1스트라이크
숫자를 입력해주세요 : 145
1볼
숫자를 입력해주세요 : 671
2볼
숫자를 입력해주세요 : 216
1스트라이크
숫자를 입력해주세요 : 713
3스트라이크
3개의 숫자를 모두 맞히셨습니다! 게임 종료
게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
1
숫자를 입력해주세요 : 123
1볼
...

보면

(1) : "숫자 야구 게임을 시작합니다.
(2) : 숫자 입력 받기
(3) : 결과 확인
(4) : 결과 출력
(5) : 숫자를 맞히면 숫자 다 맞췄다는 문장 출력
(6) : 게임 종료 후 선택지 입력 받기
(7) : 게임 다시 시작하거나
(8) : 아예 종료

그리고 컴퓨터(상대방) 입장에서 봤을 때
(1) : 게임 시작시마다 랜덤으로 숫자를 뽑는 과정이 필요하다.

수도 코드

논리 로직을 수도코드로 작성해보면 다음과 같다.


메인(){
	게임 시작한다는 문장 출력 ()	

    while(true){
    
        컴퓨터 랜덤 숫자 입력 ()
        
        사용자 숫자 입력 받기 ()
        
        결과 출력()
        
        if(3스트라이크면){
            게임종료 문장 출력()
            게임 선택지 입력()
            if(게임 선택지 == 0){
            	while문 종료}
            
        	if(게임 선택지 == 1){
            	while문 지속}
        }
        
    }

예외 상황 정의

생길 수 있는 예외 상황을 정의했다.

  1. 컴퓨터 랜덤 숫자 입력 예외

    • 1부터 9까지의 숫자가 아님
    • 세 자리의 숫자가 아님
    • 서로 다른 숫자가 아님
  2. 사용자의 입력 예외

    • 1부터 9까지의 숫자가 아님
    • 세 자리의 숫자가 아님
    • 서로 다른 숫자가 아님
  3. 게임 종료 후 사용자의 입력 예외

    • 1과 2가 아님
    • 한 자리의 숫자가 아님

작성한 기능 목록

기능 목록 구현 - 1

우선 Application에 모든 코드를 다 작성해보았다. 코드는 아래와 같다 :)
Application에 몰아넣은 커밋

해당 코드를 작성하며 고민했던 부분은 다음과 같다.

  • 컴퓨터 입력의 경우, 3개의 유효한 숫자를 만들 때까지 반복하게 구현했기 때문에 따로 사이즈 검증을 진행하지 않았다.

  • 컬렉션 값의 경우 callByValue로 객체의 참조값이 넘어가게 되므로, 매개변수에 컬렉션을 넘겨주는 방식으로 처리할 것이지만, 해당 컬렉션이 너무 여러 곳에서 사용되는 것 같으면 static으로 선언하고자 했다.

  • 사용자 입력이 옳지 않을 때, IllegalArgumentException이 제대로 발생하는지 확인하기 위한 테스트 코드를 작성하는데, 콘솔에서 입력 받는 "사용자 입력"을 어떻게 테스트 해야하는지에 대한 고민이 있었다. 검색을 통하여 해당 레퍼런스를 발견하게 되었고, System.in은 콘솔창에서 사용자가 입력하는 값을 InputStream으로 담는 역할을 하고, Scanner는 이 스트림을 사용해 읽어들인다는 원리를 이해하게 되었다.
    따라서 String을 -> 바이트코드로 바꾸고 -> 이를 ByteArrayInputStream에 담는 과정으로 테스트 코드를 작성했다. 활용한 코드는 아래와 같다!

    private void setSetIn(String input) {
        ByteArrayInputStream in = new ByteArrayInputStream(input.getBytes());
        System.setIn(in);
    }
        @Test
        @DisplayName("숫자 3개 보다 더 많이 입력")
        void 숫자_3개보다_더_많이_입력() {
            String input = "1234";
            setSetIn(input);
            Assertions.assertThatThrownBy(() -> userInput.getNumberList())
                    .isInstanceOf(IllegalArgumentException.class);
        }

해당 코드를 작성하며 마주한 기록할만한 에러 상황은 다음과 같다.

(1)

Strike, Ball, Nothing의 갯수를 기록하고자 Map을 선언했다. 보다 간결한 코드를 위해, 온보딩 미션 테스트 코드를 통해 학습한 Map.of를 활용하고자 아래와 같이 코드를 작성하였는데

    public Map<String, Integer> getTypeScoreMap(){
        return Map.of("Strike", 0, "Ball", 0, "Missing", 0);
    }

해당 map에 값을 집어넣는 아래의 코드에서

  private void putResult(Map<String, Integer> typeScoreMap, String digitResult){
        int prevScore = typeScoreMap.get(digitResult);
        System.out.println(digitResult);
        System.out.println(typeScoreMap);
        typeScoreMap.put(digitResult, prevScore+1);
    }

다음과 같은 UnSupoortedOperationException이 발생했다.

해당 에러는 new Map<>()과 같이 생성자를 활용하여 생성하지 않은 map에 값을 추가하는 것과 같은 연산을 하고자하니 발생하는 에러라는 것을 알아냈다.
Map.of(), List.of()와 같은 코드는 테스트코드와 같이 값이 변화하지 않는 컬렉션을 선언할 때 사용하면 가장 좋다는 것을 깨닫고, Map 초기화 함수를 아래와 같이 변경하여 해결하였다.

    public Map<String, Integer> getTypeScoreMap(){
        return new HashMap<>(){{
            put("Strike", 0);
            put("Ball", 0);
            put("Nothing", 0);
        }};
    }

(2)

위의 코드 설명을 보면, 내가 구현하고자하는 로직에선 Map을 활용해 점수를 저장하고 계산하고 있음을 파악할 수 있을 것이다.
해당 코드를 활용해 3스트라이크, 2볼 1스트라이크와 같은 결과 String을 만들어내려면 아래와 같은 코드가 필요한데

private void updateStrikeBallStr(StringBuilder sb, String type, Map<String, Integer> typeScoreMap){
    sb.append(typeScoreMap.get(type))
            .append(type)
            .append(" ");
}

Strike, Ball과 같이 영어로 넘어오는 type값을 그대로 String에 append 해주면 요구하는 출력 형식과 어긋나기 때문에
"Strike"를 "스트라이크"로 "Ball"을 "볼"로 변환해주는 코드가 필요했다.

추가적인 컬렉션을 만들어 해결해볼까하다가, 그렇게 된다면 타인이 내 코드를 봤을 때, Ball이 연결됨을 직관적으로 파악할 수 없을 것 같아 Enum 클래스를 도입했다.

package baseball;

public enum BallType {
    Strike("Strike", "스트라이크"),
    Ball("Ball", "볼"),
    Nothing("Nothing", "낫싱");

    private final String english;
    private final String korean;

    BallType(String english, String korean){
        this.english = english;
        this.korean = korean;
    }

    public String getEnglish(){
        return english;
    }

    public String getKorean(){
        return korean;
    }

}

영어 이름 표기 형식 (ex. Strike, STRIKE...)이 변경될 수도 있고, 로직은 동일한 상태에서 명칭이 변경될 수도 있다는 생각에 따로 english, korean 필드를 만들어 관리해주고자 하였다! 변경이 발생하면 해당 Enum에서 생성자 필드 값만 변경해주면 될 것 같아서!

변경 된 값 추가 로직은 아래와 같다 :)

    private void appendScore(StringBuilder sb, String type, Map<String, Integer> typeScoreMap){
        int typeScore = typeScoreMap.get(type);
        if(type.equals(BallType.Nothing.getEnglish())){
            if(typeScore == NUMBER_LENGTH){
                sb.append(BallType.valueOf(type).getKorean());
            }
        }


리팩토링

Application에 로직을 몰아 넣으니, 가시성이 떨어졌다. 그리고 제한 사항이었던 들여쓰기 최대 2 규칙을 지키기가 어려웠다 :( 따라서 해당 로직을 큰 기능 단위별로 묶고자 시도했다.

어려울 것이라 예상했지만, 생각보다 이 과정에 힘을 많이 들였던 것 같다. 타인이 이해하기 쉽도록 하면서, 기능 단위로 묶는다 라는 것이 생각보다 더 모호했기 때문에🙄


while문을 기준으로 기능을 나누면 더 명확하겠다는 생각에, 기존 Application 코드를 아래와 같이 정리해보았다.

main {
	야구 시작{
    	게임 시작 문장 출력;
        게임 시작{	
        	컴퓨터 랜덤 숫자;
            라운드 시작{
            	사용자 입력;	
                결과 출력;
                if(맞음) break;
                }
            사용자 재시작 입력;
            if(끝냄) break;
            }
       }
}

  1. 야구를 시작하면, 게임 시작 문장을 출력하고, 게임을 시작함.

  2. 게임을 시작하면, 컴퓨터 랜덤 숫자를 할당하고, 해당 랜덤 숫자를 맞추는 라운드를 시작함. 그리고 숫자를 맞춰서 라운드가 끝나면 재시작할건지 입력받고 재시작이라면 게임을 다시 시작, 끝이면 게임을 끝냄

  3. 라운드를 시작하면, 사용자 입력을 받고, 결과를 출력하고, 맞췄으면 라운드를 끝냈고, 틀렸으면 계속 진행함


이를 기반으로 클래스를 BaseBallGame 그리고 Round로 나눴다.


play 패키지에 게임과 관련된 BaseBall, Game, Round 클래스를 배치하였고,

input 패키지에 컴퓨터 랜덤 숫자 입력과 관련된 ComputerInput, 사용자 랜덤 숫자 입력 및 사용자 재시작 옵션 입력과 관련된 UserInput 클래스를 배치하였다.

output 패키지엔 콘솔에 가이드 문장을 출력하는 함수가 모여있는 Guide 클래스와, 사용자 숫자와 컴퓨터 숫자를 비교하여 결과를 출력하는 것과 관련된 Result 클래스를 배치하였다.

validation 패키지엔 숫자 유효성 검증, 사이즈 검증 등 다양한 검증 로직이 포함된 Validation 클래스를 배치하였으며

나머지는 상수를 관리하는 Constants 클래스, 위에서 설명한 BallType 클래스로 구성했다.

이 과정에서 고민했던 부분은 다음과 같다.

(1)

IllegalArgumentException을 던지는 코드를 모두 Validation 클래스에 몰아 넣었다. 기존에는

if(! Validation.isValidateNumber(n)){
	throw new IllegalArgumentException()}

과 같이 코드를 작성했는데, 큰 로직에 해당 코드가 있으니 가독성도 떨어지고 보기에도 안좋은 것 같아

while문의 조건 문으로 들어가는 검증 로직 (ex. isRestart 등)을 제외하곤 validate~ 형식으로 구현을 완료해, 검증하고 해당 로직과 맞지 않으면 예외를 발생시키고, 적합하면 void를 반환하는 메소드로 구현을 완료하였다. 코드

(2)

기존 온보딩 미션에서 public static을 고민없이 사용한 것에 대한 (스스로의) 피드백으로 접근제어자를 유의미하게 사용하고자 노력했다.

검증 로직은 다양한 클래스에서 사용되므로 아래와 같이 public static 메소드들로 구성하여 여러 곳에서 사용할 수 있게 만들었지만, (Validation.isRestart())

UserInput이나 ComputerInput과 같은 경우엔 어떻게 사용해야할지 감이 잘 잡히지 않았다.
고민 끝에 Game 로직에서만 한 번 사용되는 ComputerInput 메소드들과, Game과 Round 두 클래스에서 사용되는 UserInput의 메소드는 많은 곳에서 사용되고 있지 않으므로 그냥 new ComputerInput()과 같이 필요한 곳에서 생성해서 사용하도록 구현을 완료하였다.

공통적으로 많은 로직에서 사용되고 있지 않기 때문에, 굳이 자바의 객체지향 개념에 벗어나는 단순히 메소드만 가진 클래스르 만들 필요가 없다고 생각되었기 때문이다.

야구 시작 -> 게임 객체 한 번 생성 과 같은 로직으로 진행되기 때문에 ComputerInput과 UserInput을 인스턴스 변수로 선언하여 사용할 수 있도록 구현하였다. 코드

(3)

숫자는 "세"자리여야 한다, 숫자는 "1"에서 "9"사이의 숫자여야 한다와 같은 로직은 언제든지 변경될 수 있다는 생각에 변경이 보다 용이하게 만들기 위해 상수화를 진행하였다

에러 메세지에서 출력하는 문장, 검증 로직에서 활용되는 숫자 모두 해당 클래스의 상수를 사용하도록 변경하여 변경에 용이한 코드를 만들고자 노력하였다! 코드

(4)

사용자의 입력을 받고, 이를 컴퓨터 숫자와 비교하여 결과를 출력하는 함수를 구현해야했다.

단순히 "문장"만 출력할 것이 아니라 이것이 "정답"인지 또한 판별해야했기 때문에 가독성 좋은 코드를 작성하기가 가장 어려웠던 것 같다

고민하던 중 생각하게 된 부분이 바로 Result 클래스 생성자를 만드는 것이었다.

코드와 같이 Result에 String result, boolean isCorrect 필드를 선언해, result를 만들고 해당 result가 정답시 출력되는 문장과 동일한지 확인하여 isCorrect를 업데이트하는 로직을 작성하였다.

이렇게 만드니, 해당 Result를 사용하는 Round 클래스에서는 다음과 같이

public class Round {
    UserInput userInput = new UserInput();

    public void run(List<Integer> computerNumberList) {
        boolean isCorrect;
        Result result = new Result();

        do {
            List<Integer> userNumberList = userInput.getNumberList();
            result.updateResult(userNumberList, computerNumberList);
            /* ---여기!!--- */
            result.printResult();
            isCorrect = result.getIsCorrect();
            /* -------------*/
        } while (!isCorrect);

        Guide.printEndingGuide();
    }
}

유저 넘버와, 컴퓨터 넘버를 활용해 Result를 업데이트하고, 그것이 정답인지 get을 통해 가져오면 되는구나, 하고 쉽게 파악할 수 있게 되었다. 일단 내 입장에선 보다 가독성있어진 것 같다...고 생각한다 😄

(5)

로직을 클래스로 나눈 만큼, static 메소드가 아닌 인스턴스 메소드를 가진 클래스는 언제 생성해야할지?에 대한 고민이 들었다.

앞서 설명한 RoundResult의 경우,
라운드는 사용자가 정답을 맞출 때까지 계속해서 Result를 업데이트해야하기 때문에, 아래와 같이 while문 안에서 Result를 생성하는 기존 코드는 비효율적이라는 생각이 들었다.

public class Round {
    UserInput userInput = new UserInput();

    public void run(List<Integer> computerNumberList) {
        boolean isCorrect;
        Result result = new Result();

        do {
            List<Integer> userNumberList = userInput.getNumberList();
            Result result = new Result(userNumberList, computerNumberList);
            result.printResult();
            isCorrect = result.getIsCorrect();
        } while (!isCorrect);

        Guide.printEndingGuide();
    }
}

실제로 updateResult()라는 코드도 있기 때문에~..
그냥 로직을 아래와 같이 Round.run() 을 하면 Result가 생성되고, while문 안에서 계속 업데이트하는 방식으로 변경하였다.

public class Round {
    UserInput userInput = new UserInput();

    public void run(List<Integer> computerNumberList) {
        boolean isCorrect;
        Result result = new Result();

        do {
            List<Integer> userNumberList = userInput.getNumberList();
            result.updateResult(userNumberList, computerNumberList);
            result.printResult();
            isCorrect = result.getIsCorrect();
        } while (!isCorrect);

        Guide.printEndingGuide();
    }
}

다시보니 좀 더 직관적인 것은 위의 코드인 것 같기는하다 ~.. 앞으로 미션 진행하면서 가독성과 효율성 관련된 것을 조금 더 고민해봐야겠다.




마무리

온보딩을 경험삼아 더 많은 것을 고민해보고자 했는데, 이것이 기록으로 잘 남게 된 것 같아 뿌듯하다!!!!

  • 기능 단위로 커밋 메세지를 작성해보자 (내 기준을 세워보자)

  • 가독성 좋은 코드를 만들어보자

  • 클래스로 나눠보자

  • 다양한 엣지 케이스를 생각해보자

와 같은 미션 진행 전 다짐한 부분을 이행하고자 많이 노력한 것 같고, 그 과정에서 확실히 많이 성장한 것 같아서 기분이 좋다아
모르는 것을 검색하고, 그냥 복사붙여넣기 하는 것이 아니라 다양한 대안을 생각하고 그 중 내 기준(ex. 이번 미션에서는 가독성!)에 맞게 대안을 선택하는 과정에서 많이 배울 수 있었다

이러한 태도를 기반으로 앞으로의 미션에서도 더 !! 성장해보려고 한다🍀

다들 화이팅
화이팅
화아아이팅
💌

2개의 댓글

comment-user-thumbnail
2022년 11월 10일

잘보고갑니다! 화이팅!!!😺

1개의 답글