우테코 프리코스 2주차 회고

eora21·2022년 11월 8일
0

프리코스를 진행하며 고민한 내용들을 작성하였습니다.
자세한 코드는 해당 리포지토리를 참조해주세요.

요구사항

진행한 사항

코드 작성 전

코드를 작성하기 전에, 요구사항을 읽고 어떠한 형태로 진행할지 작성했다. 1주차에도 그렇듯 먼저 어떠한 기능을 만들지 전체적인 로직을 하나씩 정리하며 작성하다 보면, 코드에 늪에 빠져 길을 잃는 경우가 현저히 줄어들었다. 설령 내가 잘못한 걸 알아채더라도 금방 원복할 수 있는 장점이 있었다.

실제로 기능을 다 구현한 후에, 리팩터링을 반복하며 메서드를 옮기거나 다시 작성해야 하는 일이 있었다. 이 때 기능 목록을 보며 어떻게 하면 좋을지 먼저 생각하고 나니 예상보다 빨리 끝낼 수 있었다.

(반면 현재 리팩터링중인 프로젝트가 있는데, 기능목록 없이 머릿속으로만 구성해놨었다. 그러다 오랜만에 코드를 작성하려 하니, 어떤 작업을 어디까지 했는지 기억이 나지 않아 애를 먹었다. 어찌저찌 푸시하긴 했지만.. 다음부터는 이번 프리코스처럼 어디를 어떻게 고쳐나갈지 작성하며 실행하려 한다.)

처음 작성한 기능 목록

해당하는 내용들을 하나의 클래스에 전부 넣어 구성했었다. 다만 후술할 여러 이유들로, 자연스레 변형이 이루어졌다.

현재 작성한 기능 목록

게임이 진행될 때 필요한 메서드들을 묶어 하나의 역할을 부여하였다. 처음엔 나누지 않아도 괜찮겠지 싶었다가, 이것저것 생각하다 보니 나누게 되었다.

코드를 작성하며

게임을 실행할 클래스를 만들고, 작성한 기능 목록대로 구현하려다 Randoms 클래스 내부를 들여다보았다.
중복되지 않은 랜덤값 3개를 뽑는 예시 코드는 Randoms.pickNumberInRange()만을 이용하였으며, 중복된 값이 나올 경우 코드를 반복해야 했다.
그러나 Randoms.pickUniqueNumbersInRange라는 메서드는 유저가 원하는 범위값 내에서, 원하는 갯수만큼 중복되지 않은 값을 뽑아 반환해주는 녀석이었다.
이렇게 좋은 메서드가 있다니! 당장 사용해야지! 하고 코드를 구성했다.

그러나..

테스트코드 앞에서 좌절

기능을 다 작성하고, 테스트코드를 실행시켜봤다.
아뿔싸, 진행이 되지 않았다.
분명 코드 실행시키면 잘 뜨는데.. 어디가 문제지 싶어서 테스트코드를 확인했다.

뭔 코드야 이게..?

@Test
void 게임종료_후_재시작() {
    assertRandomNumberInRangeTest(
        () -> {
            run("246", "135", "1", "597", "589", "2");
            assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
        },
        1, 3, 5, 5, 8, 9
    );
}

해당 게임은 숫자를 랜덤으로 뽑고 그에 맞게 추리하여 맞추는 형태였다. 헌데 테스트코드에서는 값을 넣은 결과가 고정되어있다는 듯 떡하니 낫싱, 3스트라이크등이 작성되어 있었다.
그 밑에는 1, 3, 5, 5, 8, 9라는 숫자들이 적혀있었다. (IntelliJ 내에서는 value: 1, values: 3, 5, 5, 8, 9로 적혀있어서 처음엔 눈치채지 못했지만) 해당 값들은 차례로 135, 589라는 답을 지정하고 있는 듯 했다.

음.. 일단 그건 알겠는데, 왜 내 코드에선 안돌아갔을까? 하고 sout을 이용해서 확인해보니, 컴퓨터가 랜덤값이나 테스트코드의 지정값이 아닌, 아예 빈 리스트를 반환하고 있었다.

직접 돌려보면 잘 뱉어주는 걸로 봐서, 테스트코드 내부의 생김새가 다른 것 같았다. 따라서 코드 내부를 직접 뜯어봤더니, 해당 테스트코드는 pickNumberInRange()를 겨냥한 상태였다.

라이브러리 요구 사항 중, Random 값 추출은 camp.nextstep.edu.missionutils.RandomspickNumberInRange()를 활용하라고 적혀 있었는데 해당 부분을 제대로 읽지 못한 채 프로젝트를 진행했던 것이다.. 따라서 코드를 수정하고 돌려보니, 제대로 출력이 나오고 있었다.

pickUniqueNumbersInRange()에선 왜 돌지 않았을까 하고 찾아보니, Mock이란 녀석은 지정된 클래스를 상속받은 객체를 만든 후 실제 코드가 돌아갈 때 스리슬쩍 끼어들어서 결과를 뱉어내는 녀석이었다. 따라서 테스트코드에서는 내가 작성한 녀석이 아닌, MockpickNumberInRange()가 돌고 있었던 것이다. pickUniqueNumbersInRange()는 따로 지정해주지 않았으므로, 빈 값을 반환하고 있었으니 테스트가 수행될 리 없었다.

요구사항을 제대로 읽지 못해 생긴 일이지만, 그래도 무언가를 배울 수 있었으니 좋았다.

리팩터링

테스트코드를 통과한 후, 어떤 식으로 해야 더 좋은 구조를 가질 지 생각했다.

메서드명

내가 아닌 다른 사람이 봤을 때, 어떤 메서드인지 알아챌 수 있다면 좋겠다고 생각했다. 헌데 모든 사람이 한눈에 알아보게끔 명명하는게 언제나 쉽지 않은 것 같다.. 그래도 최대한 알아볼 수 있었다면 좋겠다는 마음으로 조금씩 이름을 변경했다.

변수명

변수명도 조금씩 신경썼는데, 예로 for문에 자주 사용되는 i 또한 의미를 알기 힘들다는 것을 코수타(코치와 수다 타임)를 통해 알게 되었다.
확실히 i는 숫자 관련이면 여기저기 들어가다보니, 이번에는 i 대신 idx로 나름 뜻을 내포하도록 했다.

정규표현식

1부터 9까지의 3자리 입력은 정규표현식으로 손쉽게 해결할 수 있을 것 같았다. 하지만 정규표현식을 잘 몰랐기에, 처음에는 문자열을 확인하는 코드를 직접 작성했다. 코드가 동작되는 걸 확인한 후, 프로그래머스의 정규표현식 강의를 풀어보았다. 입문용으로 나름 깔끔했고, 해당 코드들도 정규표현식으로 나름 간단하게 작성할 수 있었다.

상수 고민에서 파생된 클래스 분리

하나의 클래스에 모든 메서드를 담는 것은, (지금 생각하면) 좋지 않은 방법이었으나 당시의 나는 이것저것 생각하다 클래스를 분리하지 않기로 했었다.
게임은 전체적인 룰 내에서 돌아가고, 현재 값을 받거나 연산을 하는 등의 모든 과정이 게임의 룰 밑에서 돌아갔었기 때문이다.

그러나 토요일 오전에 공부도 해볼 겸 다른 분들의 1주차 코드를 보며 피어리뷰를 참여하고 있었는데, 상수 집합 분리에 대해 말씀해주신 분이 계셨다. 검색해보니 enum을 뜻하는 듯 했다. 우형 기술블로그 및 기타 블로그 글들을 참고해보며 특징과 장단점에 대해 찾아보았다.

상수가 많아지면 많아질수록 겹치는 값이 있을 수 밖에 없었고, 해당 사항을 인터페이스나 클래스별로 나눠 구분할 수는 있었지만 그만큼 관리가 번잡해지기에 해당하는 사항들을 모두 해결하는 방법으로 enum까지 도달하게 되는 듯 했다.
기본적인 enum은 단순 나열값을 정의하기 좋았으며, enum클래스는 특정 상태에 따라 달라지는 구현값(게임 난이도에 따라 적들의 체력 및 드랍되는 액수의 증감)형태에 굉장한 강점을 지니고 있었다.

그러나 이번 문제에 사용되는 상수들을 모아서 보니, 규칙에 대한 상수와 볼&스트라이크에 대한 상수로 나뉘긴 했으나 연속된 값이 아닌 특정 값을 지정하는 형태였기에 현재 코드 상태에서는 겹치는 값이 없었기도 했고, 해당 상태에 따라 구현값을 달리 할 부분이 딱히 없어 보였다.
또한 가독성도 저하되는 듯 했다. enum을 구현한다면 기존에 START_NUMBER로 작성한 값을 Rule.START_NUMBER.getValue() 등으로 작성해야 했고, 내게 너무 난잡해보였다.

또한 상수 하나씩 객체를 생성하여 관리하는 게 오히려 더 낭비가 아닐까 하는 생각이 들게 되었고, 결국 enum 대신 기존 상수 형태를 선택하는 대신에 클래스를 숫자야구 진행을 맡을 클래스, 게임의 룰에 적합하게 값들을 관리해줄 클래스, 볼과 스트라이크를 판정할 클래스로 나누기로 결정했다.
(enum에 대한 해당 판단이 뒤집히게 될 줄은 상상도 못했다..)

클래스 이름을 정하는데 굉장히 오래 걸렸다. 볼과 스트라이크 판정은 간단하게 심판(Referee)로 했는데, 게임의 룰에 맞게 값들을 관리하는 걸 어떻게 불러야할까 싶었다. 처음에는 랜덤값 확정 및 사용자의 값을 정리해주는 녀석이므로 투수, 배터리(포수 + 투수) 등으로 했으나 느낌이 오지 않았다. 한참 생각하다, 값들을 관리하는 녀석이니까 Staff는 어떨까 했는데 나름 괜찮은 것 같았다.

이후 클래스에 따른 기능 목록을 재작성했고, 해당 단계에 따라 코드도 재차 구현했다.

클래스 분리에서 파생된 상수 재고민

클래스를 분리하며, 해당하는 상수들은 전체적으로 게임의 룰과 관련되므로 Rules 클래스를 따로 지정하고 그 안에 모두 작성했다.

헌데 시간이 지날수록, 과연 하나의 클래스에서 전역 상수를 유지하는 것이 옳은걸까 싶었다. 구글링을 해봤을 때 '안티패턴이 아니다'는 글을 보고 안심했었지만, 프로젝트가 커질 때마다 전역 상수의 관리가 굉장히 어렵고 더럽다는 글을 읽게 되었다. 아.. 더 이상 전역 상수를 유지하기 싫었다.

전역 상수가 싫다면 모든 클래스들이 Rules를 상속받거나, 혹은 각각의 클래스에서 지닐 상수값을 결정하여 코드를 리팩터링해야 했다.
(물론 프로젝트가 더 커지면 다르겠지만) 상속은 그게 그거 아닌가 싶었고, 어떤 클래스가 어떤 상수값을 지닐지 결정하는게 좋은 것 같았다.

헌데, 값을 뽑는 횟수인 PICK_COUNT(= 3)가 맘에 걸렸다. 해당 값은 NumberBaseball 클래스에서 3가지 답을 맞췄다는 출력에 쓰이고, Staff에서는 랜덤값을 뽑는데 쓰이고, Referee에서는 스트라이크 갯수를 확인하는데 쓰였다.

Referee에서는 스트라이크 갯수 자체를 반환하여 연관성을 없앨 수 있었고, NumberBaseball에서는 해당 출력을 Staff 내로 가져간다면 없앨 수 있었다. 그러나 위치를 옮기자, 전과 달리 코드를 보며 게임의 진행을 한눈에 알아챌 수 없는 듯 했다.

그렇다고 해당 상수들의 대부분을 NumberBaseball에서 관리하고, Staff에게 생성자를 이용해 값을 전달해주자 테스트코드에서 문제가 생겼다.
생성자에 들어오는 값이 없어야, 제대로 된 테스트코드를 구현할 수 있었다. 따라서 해당 방법도 좋지 않았다.

결국 해당 값만을 전역 상수로 두려고 했으나.. 단점을 설명했던 글이 눈앞에 아른거렸다.
따라서 다시금 enum을 만들었고, 가독성을 늘리기 위해 import와 toString()을 오버라이드했다.

4~5시간의 고민에 의해 코드의 구조가 원복되었고.. 사용하지 않으려 했던 enum을 구축하게 되었다.
만감이 교차했다. 분명 더 좋은 구조와 방향성이 있을 텐데, 내가 찾지 못한 것일까, 이게 최선이었을까 하는 생각이 계속 떠올랐다.

느낀 점

현타가 정말 많이 왔다. 난 왜 그동안 프로젝트를 진행하며, 이런 고민을 하지 않았을까 하는 생각이 계속 들었고, 내가 과연 프로그래머라 할 수 있을까 싶은 생각도 들었다.

하지만 그와 동시에 즐겁기도 하다. 시야가 넓어지는 느낌이 들면서, 내일 피어리뷰를 돌며 다른 사람들의 해결법을 열심히 내 것으로 만들고 싶다는 생각이 든다.

더 좋은 코드, 더 나은 해결법, 더 탄탄한 프로그래머..
이전에는 단순히 공부할 게 많다고 생각했고, 하나씩 천천히 진행해보자고 내 자신을 다독였으나 막막함을 어쩔 수 없었다.
사실 지금도 막막함은 여전하지만, 그리고 내가 진행한 코드들에 대해서도 부끄럽긴 하지만, 이제라도 깨달은 게 어딘가.

만약 프리코스를 접하지 않았다면 이런 고민도 못해봤을 것이고, 근시안적으로 계속 코드를 짜내려갔을지도 모른다.
매 순간 깨달으면 된다. 그렇게 하면 언젠간 내가 원하는 이상향에 가까워졌다는 걸 알아차릴 수 있을 테니까.

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

1개의 댓글

comment-user-thumbnail
2022년 11월 10일

잘보고갑니다!

답글 달기