나의 독학은

[프리코스 1주차] 숫자 야구 게임 회고 본문

회고/우아한테크코스 6기 프리코스 회고

[프리코스 1주차] 숫자 야구 게임 회고

안종혁 2023. 10. 26. 14:59

😊 1주차 시작 전

취준 기간 중 1달이란 시간을 프리코스에 투자하는 만큼 꼭 성장하고 싶었기에 어떻게 하면 프리코스의 전보다 성장할 수 있을지 고민했고 이번 주에는 2개의 목표를 세웠다.

 

✅목표

첫 번째는 프리코스를 진행하며 모르는 용어들을 다 찾아보고 이해 하기.

두 번째는 최근에 학습한 람다식과 객체지향을 활용해서 1주차 보내기.

 

최소 이 2개를 취준생이어서 프리코스에만 집중할 수 있는 시간과 독학이란 길을 걸어온 노력과 근성을 가지고 이루고자 한다.

분명히 성장해있겠지!?

😊 숫자 야구 게임 구현

✅ 구현 중에 만난 오류 해결과 아쉬움

나는 이전 기수 프리코스 문제들 중 유일하게 딱 한 문제를 하루동안 풀어봤었는데, 그게 숫자 야구 게임이었다.

그러나, 구현하면서 계속 에러를 마주했었지만 해결하지 못했고, 프리코스를 시작했었다.

 

다행히(?)도 6기 프리코스 1주차는 숫자 야구 게임이였다. 이번에는 에러를 꼭 해결해야 한다는 마음에 덜컥 겁이났지만 이번에는 구현하면서 오류를 마주하지 않아서 기쁜 마음에 싱글벙글 테스트 케이스를 돌려봤다! 그러나 오류가 나며 실패했다...

 

오류는 [게임을 시작할지 말지 사용자가 입력하는 로직에서 잘못 입력한 경우 예외를 터뜨려준 곳]에서 발생했다. 나는 "예외를 터뜨려서 종료시키라는 요구사항에 맞췄을 뿐인데 왜 틀린거야!!" 했다.

 

다음 날, 우테코에서 제공한 테스트 코드의 메서드들을 내부적으로 하나씩 살펴보았고, 이거다!! 싶은 것을 발견했다.

 

테스트 코드를 뜯다보면 thenReturn이라는 메서드가 나온다.

테스트 케이스의 thenReturn 메서드

thenReturn 메서드의 설명에 checked exception (빨간 줄)을 발견하였고, 어노테이션에는 unchecked라는 것을 발견하였다.

@SuppressWarning의 unchecked는 경고 메시지를 무시하고 컴파일러에게 해당 부분을 경고하지 말라고 알려주는 것이고,

빨간 줄은 checked exception으로 무조건 예외를 처리해줘야 하는 예외이다.

그래서 나는 "게임을 새로 시작하거나 종료할 때 사용자가 잘못 입력을 하면 예외를 터뜨리지말고 잡아야 하는구나!! 예외를 안잡으면 경고 메시지를 생략하라!!" 라고 생각했다.

 

그래서, 아래에 있는 [게임을 새로 시작할지 말지의 메서드]를 try-catch문으로 감싸주어 예외를 잡아주었다.

게임을 시작할지 말지 정하는 메서드

이제는 테스트케이스 통과하겠지? 하면서 싱글벙글하게 돌렸지만 실패했다. 이번엔 "1볼 1스트라이크"라는 테스트가 통과하지 못하는 것이었다.

 

그래서, System.out.println을 모든 변수마다 넣으면서 디버깅하기 시작했고, 원인을 찾을 수 있었다.

 

다음은 컴퓨터가 생성한 랜덤 숫자를 받는 List<Integer>타입이다.

랜덤 컴퓨터 숫자를 받는 List

 

createRandomComputerNumber 메서드는 숫자 1개씩 만들어서 List에 넣었기 때문에 List는 [1, 3, 4]와 같이 숫자가 한 개씩 들어가 있을것이다.

 

그러나, 아래 사진처럼 List 에서 값을 꺼낼 때, 바보같이 get(0)하면 134가 한 꺼번에 나올 생각을 했던 것이었다..

바보같던 순간

그래서, 로직을 1,3,4가 나올 수 있도록 바꿔주었고, 정상적으로 동작할 수 있었다.

 

여기서 컴퓨터 랜덤 숫자를 만드는 로직 자체가 틀렸기 때문에 게임을 시작할지 말지 정하는 메서드의 테스트 케이스가 실패했을 수도 있겠다라는 생각이 들어 테스트 케이스를 다시 돌려보았다.

 

오류가 바뀐 것을 확인할 수 있었다.

원래는 오류메시지의 맨 윗줄이 user의 게임을 시작할지 말지 메서드였음

그렇다. [게임을 시작할지 말지] 메서드는 잘 구현한 것이었고, 원인은 [1,3,4] 중 하나의 숫자만 꺼내서 문제였던 것이었다😂

 

그러나, 문제는 해결되었지만 List 에서 잘못된 값을 꺼냈는데 List와는 상관없는 [게임을 시작할지 말지]의 메서드에서 왜 오류가 났는지 확인을 못했다.😥

코드를 복기해서 전의 코드로 돌아가봐도 처음에 만난 오류를 다시 마주할 순 없었다..

궁금증이 남은채로 이렇게 1주차가 마무리되어 아쉽다. 또 다른 배움을 얻을 수 있었을텐데!

오류를 만나면 당황하지 말고 배울 것이 생겼다고 기뻐해보자!

✅ 테스트 코드의 필요성

어느 로직에서 오류가 생긴지는 몰랐기에 모든 변수의 값을 print문으로 출력하며 디버깅해서 원인을 찾을 수 있었다.

이 과정이 너무 귀찮았다. print문 작성하고 애플리케이션 돌리고 입력값 넣고하는 과정이 반복의 반복을 거듭했다.

이로 인해 얼핏 알고만 있던 로직을 구현하기 전에 테스트 케이스를 먼저 작성하는 TDD가 생각났다.

만약, 기능을 구현하면서 테스트 케이스를 작성했다면 어느 로직에서 오류가 났는지 바로 알았을텐데 말이다.

 

앞으로 있을 프리코스 2주차 인가 3주차에 테스트 케이스도 같이 만들라는 요구사항이 왜 있는지 알게 되었고, 기능을 하나 만들 때 마다 기능이 예상한대로 잘 동작하는지 확인해야지!

✅ Javadoc문서는 위로 읽기

마지막으로, 내가 착각했었던 이 메서드를 짚고 넘어가자.

@SuppressWarning의 unchecked는 경고 메시지를 무시하고 컴파일러에게 해당 부분을 경고하지 말라고 알려주는 것이맞다. 즉, 예외 처리와 상관이 없었다.

내가 오해한 빨간 줄은 thenReturn이 아닌 밑에 있는 thenThrow에 대한 설명이었다!

메서드의 설명을 아래로 읽는 것으로 착각했을 때

즉, JavaDoc의 설명은 보고싶은 메서드를 기준으로 위에 설명 되어 있는 것도 알게 되었고, thenReturn메서드는 예외 처리에 대한 내용이 없음을 확인했다!!😊 

thenReturn 메서드의 주석란은 위에 있었다!

😊 숫자 야구 게임 리팩토링

나는 지금껏 알고리즘 문제들만 풀면서 한 클래스에서만 코드를 작성해왔다. 최근에 학습한 '객체지향의 사실과 오해'를 읽고, 처음으로 코드에 적용한 것이 숫자 야구 게임이다. 다행히 dfs와 완탐문제를 풀며 메서드를 생성하는 것은 익숙했기에 메서드 분리는 어렵지 않았다.

✅ 숫자를 상수화 하기

숫자 야구 게임에는 사용자가 1을 입력하면 재시작, 2를 입력하면 종료를 하도록 하는 요구사항이 있다.

구현을 하면서 여러 클래스에 1과 2가 중복된 것이 신경쓰였고, 1과 2가 100과 1000으로 변경이 되더라도 쉽게 변경할 수 있는 코드를 만들고 싶었다. 그래서, 1과 2를 상수화 하였다.

 

상수란 변하지 않고 고정된 값을 담는 변수이다.

상수화 하는 방법은 아래 예시와 같이 static 과 final을 붙이고, 값(1과 2)에 이름을 붙여 대문자로 작성해주면 된다.

 * final : 한 번 값을 할당하면 재할당이 불가해서 변경되지 않는 것이다.

1과 2에 이름을 붙여 상수화 시켜주기

✅ public static 과 private static 의 순서

상수화를 하다보니 아래 사진과 같이 public인 상수와 private인 상수가 같이 있었다.

public static과 private static

이들 간의 순서도 상관 없는지 궁금해서 찾아보았고, 표준 자바 컨벤션에서 궁금증을 해결할 수 있었다.

 

그러나, 프리코스의 요구사항 중 하나는 구글 자바 컨벤션을 지키는 것이다.

그래서, 공부했던 자바 코드 컨벤션의 3.4.2 클래스 내용 순서를 다시 읽어보았다.

3.4.2 내용에는 클래스 내부 순서는 상관 없지만 단지 "설명할 수 있을 정도의 순서를 지켜야 한다"라고 명시 했다. 즉, 설명할 수 있을 정도면 되는 것이라고 이해했다.

 

그래서, 나는 코드 컨벤션이라는 의미를 한 번 더 생각해서 클래스 내부 순서를 표준 자바 컨벤션을 사용한다면 순서를 설명할 수 있을 뿐아니라 컨벤션을 하는 의미도 지킬 수 있지 않을까라 생각했다.

 

표준 자바 컨벤션의 클래스 내부 순서는 다음과 같고, 이렇게 지키려고 노력했다.

1. public static 상수

2. private static 변수

3. private instance 변수

4. public method → private method(자신을 호출하는 public 함수 직후)

✅ 객체에 static 메서드는 많고, 멤버 변수는 없다?

테스트 코드가 성공하는 내 코드의 객체에는 멤버변수(상태)가 없어서 static 메서드가 많았다.

 

예를 들어, Computer 객체가 랜덤 숫자를 만드는 메서드 (createRandomComputerNumber) 이다.

이 메서드를 만든 이유는 Game 객체가 Computer 객체에게 "랜덤숫자를 만들어줘"라고 책임 부여를 하고 싶었다.

그러나, static 메서드로 정의하니 User 객체도 Computer 객체에게 "랜덤숫자를 만들어줘"라고 책임 부여를 할 수 있는 것이었다.

이는 내 설계의 의도와는 맞지 않았고, 누군가 내 코드를 수정 하게 된다면 나의 설계의도를 파악하지 못할 것이라 판단했다.

 

아래 사진은 메서드를 static으로 정의했을 때의 코드이다. 코드 전문은 클릭

public class Computer {
    ...
    // 멤버변수대신 지역변수로 List을 만든 모습
    public static List<Integer> createRandomComputerNumber() {
        List<Integer> computerNumber = new ArrayList<>(); 
    }
}
public class User {
    ...
    // 멤버변수대신 지역변수로 List을 만든 모습
    public static List<Integer> createUserNumber() {
        List<Integer> userNumber = new ArrayList<>();   
    }
}

그래서, 리팩토링을 하면서 '무엇을 건드려야 static 메서드를 줄이고, 객체마다 멤버변수를 생성할 수 있을까?'를 고민했고,  "무엇부터 수정해야 할지? 이렇게 된 근본적인 원인은 무엇인지?"에 대한 답을 찾고자 하였다.

첫 번째로 한 시도는 구글링을 하고, 자바의 정석을 공부하며 노트에 정리했던 static 부분을 다시 읽었다. 그러나, 내가 알고 있는 정보만을 다시 습득할 뿐이었다.

두 번째는 디스코드에 올라온 글들을 다 읽어보았다. 이 과정에서 일급 컬렉션을 알게되어 List를 Wrapping 하는 과정에서 멤버 변수가 생기는 건줄 알고 일급 컬렉션을 계속 읽고 이해했지만 내 상황과는 맞지 않았다. 

세 번째는 학교 도서관에서 '클린 코드'와 '좋은 코드, 나쁜 코드'의 목차를 보며 해결할 수 있을 거 같은 목차들을 추려내 내용을 읽어보았지만, 문제를 해결하는데에는 도움이 되지 않았다.

네 번째로 요구사항을 천천히 다시 읽고 설계를 다시 짜보았다. 그러나, 똑같은 설계를 할 뿐 설계가 나아지진 않았다. 설계를 잘하는 법도 구글링에 찾기도 하고, 객사오의 요약본을 읽으며 설계도 따라 해보았지만 나아지지 않았다.

다섯 번째로 SOLID 5원칙을 설명한 블로그를 읽고, 인프런 강의도 보았지만 근본적인 원인을 찾을 수 없었다.

여섯 번째로 객체지향을 잘 이해한게 아닌가 싶어 '객사오' 2회독 했을 때의 쓴 글을 다시 읽어보았고, 드디어 해결의 실마리를 찾게 되었다.

 

나는 멤버변수로 int나 String형 같은 원시타입을 상태로 갖는 객체는 만들 수 있지만, 숫자야구의 Computer, User 같은 객체와 객사오 요약본에 있는 Coffee, Barista 같은 객체를 상태로 갖는 객체를 만드는 것이 어렵다는 것을 알게 되었다.

이 상태로 숫자야구게임을 구현하다 보니 객체는 상태(멤버변수)를 가지지 못했고 자연스럽게 필요한 변수들은 지역변수가 되고, 메서드들이 static 이 되는 것이었다.

 

그렇다. 분명히 '객체지향의 사실과 오해'를 2회독 할 때는, 객체지향을 충분히 이해했다고 생각했지만 막상 이해한 것을 바탕으로 코드에 적용하는 것은 어려웠던 것이었다.

그래서, 일곱 번째로 객체지향을 설명하면서 코드가 같이 있는 '오브젝트'를 읽었다.

오브젝트의 입장 티켓과 영화 상영 예제 코드를 객체지향을 생각하며 따라 치니 어려워서 멈추고, 생각하고의 반복이었다. 추가로 메시지 파트를 잘 이해하지 못한다는 것을 알게 되었다. 내가 쓴 객사오 리뷰 글에서도 메시지에 관한 설명은 없는 것이었다!!(이제는 추가했다!)

메시지를 이해하지 못하니 객체간의 의존 관계를 설정할 수 없었고, 이는 의존 관계를 어떻게 설정할지에 대한 궁금증으로 이어졌다. 이 궁금증을 갖고 책을 읽어 나갔고, p.85에서 드디어 내가 학습한 모든 것들이 실로 연결되었다.

 

p.85 - 협력 관계 속에서 다른 객체에게 무엇을 제공해야 하고 다른 객체로부터 무엇을 얻어야 하는지를 고민해야만 훌륭한 책임을 수확할 수 있다.

 

나는 객체에게 무엇을 제공해야 할지만을 고민해왔기 때문에 훌륭한 설계를 하지 못했고, 훌륭한 책임을 수확하지 못했다.

훌륭한 책임을 바탕으로 객체간의 의존 관계를 설정하고, 메시지를 보낼 수 있었다.

 

곧바로 숫자 야구 게임의 설계를 다시 하였고, 이전 설계와는 확연히 차이가 났다!!

아래의 표는 달라진 설계이다. 

이전 설계 바뀐 설계
랜덤 숫자를 생성하라 → Computer
숫자를 입력하라 → User
숫자를 비교하라 → Game
결과를 출력하라 → Game
App → 게임을 실행하라 → BaseballGame
BaseballGame → 랜덤 숫자를 생성하라 → Computer
BaseballGame → 숫자를 입력하라 → User
BaseballGame → 결과를 출력하라 → Result
User → 숫자를 검증하라 → Validator

바뀐 설계를 가지고 문제를 구현하기 시작하자 책에서만 봐오던 상태와 행동이 있는 예제 코드들처럼 내 코드가 완성되어갔다!

이 때의 짜릿함과 뿌듯함, 깨달았을 때의 소름과 즐거움이 너무 좋다. 끝끝내 해결했을 때 오는 이 감정들이 최고다😁

 

하지만, 제일 중요한 검증을 하기가 어렵다. 내가 잘한건지, 좋은 깨달음을 얻은건지를 확실하게 알지 못하는 것은 아쉽다.

남은 3주 동안 꾸준히 고민해봐야지!

 

*바뀐 코드의 전문

😁1주차를 마치며

구현할 때의 자바독의 문서의 일부분만 읽고 판단한 점과 '객사오'의 메시지 파트를 확실히 이해하지 않은 점으로 미루어 볼 때, 나는 성급한 것 같다.

 

성급하다는 것을 어떻게 보완할지를 고민했고 결론을 내렸다.

1. 글을 천천히 읽고, 대충 읽지 않기!

2. 이해한 것에서 끝나지 않고 어떤 것에 먼저 적용해보기!

이 2가지를 이용해 보완해야겠다.

 

무엇보다 이해한 것에서 끝나지 않고 적용을 할 수 있어야 성장을 이룰 수 있다는 것을 깨달았다.

객체지향을 충분히 이해했다고 생각했지만 적용할 때는 다른 것처럼 말이다.

무언가를 학습하면 꼭 적용을 해보겠다는 마음으로 앞으로 학습한 것들을 꼭 코드에 적용해서 지속적으로 성장하자! 

 

그리고, 코드를 작성할 때, "왜?"를 항상 생각해야 겠다.

왜 static 메서드로 선언하려는지 고민을 했다면 코드를 완성한 후가 아니라 학습 도중에 원인을 찾았을 것이라 생각이 든다.

그 동안, 간단한 알고리즘 문제들을 풀면서 느끼지 못했지만 이 경험을 통해 Java언어의 사소한 키워드들이 코드에 큰 변화를 만들어 냄을 알 수 있었다.

왜 static으로 선언하려고? , 왜 상수로 선언하려고?, 왜 List 로 선언하려고? 등등 내 코드 한줄 한줄에 나의 생각을 담아 고민을 담아 작성하지 않으면 위에 같은 문제가 다시 발생하겠구나를 뼈저리게 느꼈다.

 

앞으로 있을 프리코스에 다양하고 심도 있는 공부를 해서 내 코드에 활용시키기 보다는 코드 한 줄 한 줄 작성할 때마다 생각과 고민을 하고, 이 과정에서 생기는 궁금한 것들을 학습해 나가야 겠다.