대외활동/우아한 테크코스 6기

[우아한 테크코스 6기 - 프리코스] 2주차 회고

oxdjww 2023. 11. 2. 00:25
728x90
반응형

Overview

우아한 테크코스 6기 프리코스 중 2주차 자동차 경주게임을 구현하고, 이에 대한 회고이다.
Github Link

1. 설계와 개발패턴 정형화

2주차 미션에서는 1주차보다 설계에 많은 시간을 투자했다.


그렇게 리팩토링에 쏟은 시간이 상대적으로 적었고, 초반 설계에 대한 고민이 많아 처음 과제를 보고, 코드를 작성하기까지 2~3일은 걸렸다.

1주차에 비해 2주차에 추가된 조건에 가장 신경쓰이는 부분이 ‘기능단위 커밋’을 할 것이라는 조건이였다.


기능이라는 단어란 무엇일까?

영어로 function이고 이 말은 method를 지칭하는 function과도 문맥을 동일시한다.

그렇다면 커밋의 단위는 메서드의 구현인가?

그렇다면 기능 목록은 메서드 단위로 작성해야 하는가?


그것은 아니였다. 메서드 여러개의 조합으로 프로그램의 기능이 완성되는 경우도 있기에 답이 안 나오는 고민처럼 느껴졌다.

이렇게 복잡하게 느껴지는 문제일수록 본질에 치중해야 한다.


많은 레퍼런스를 찾아본 결과, 이 말이 가장 와닿았다.

‘커밋을 통해 변경사항을 한 눈에 알 수 있어야 한다.’

결국 커밋의 본질은 남들이 보았을 때 유의미한 변화를 알 수 있고, 이를 통해 작업 단위를 분리할 수 있게끔 하는 것이다.

이번 미션의 요구사항인 ‘기능단위 커밋’은 기능 요구사항들을 충족하는 것이 미션의 본질이고, 커밋을 통해 기능을 어떻게 순차적으로 구현하였는지 파악할 수 있으면 커밋으로서의 기능은 충분하다고 생각되었다.


그렇기에 README에 기능 목록을 구현하고, 커밋할 때 README 체크박스 수정을 통해 기능 단위 커밋을 잘 준수하였음을 보였다.

2. 어렵게 생각하지 않기

일급 컬렉션을 처음 접하고, 객체지향의 특징을 정말 잘 반영한 자료구조라고 생각되었다.

일급 컬렉션과 정적 팩토리 메서드를 사용하면 객체의 불변성을 보장하며 캡슐화를 통해 높은 응집성을 갖는 코드를 작성할 수 있었다.


이에, 설계시에 이런 생각을 했다.

자동차 경주 게임에서 모델을 추출하고, 이 모델 중 일급 컬렉션화할 수 있는 것들은 어떤 것이 있을까?

그렇기에 각 Car가 진행하는 Round를 묶어 Rounds로 구현하여 했다.


CarRound를 갖고 진행해야 하는데, 그럼 차의 위치를 저장하고 있어야할 곳을 정의하기 어려웠다. 각 라운드를 진행할 때마다 라운드가 갖고 있어야할 정보의 범위도 쉽게 정할 수 없었다.

Round는 각 차에 대한 랜덤 값을 다 갖고 있어야 하고, 랜덤 값에 대한 전진 여부도 판단해야 한다. 그렇게 Round에 필요한 기능을 하나하나 생각하다보니 이는 어쩌면 GameController의 역할을 대신한다고 생각되었다.


그렇기에 ‘객체가 행위를 한다.’는 규칙에 걸맞게 Cars 객체가 게임을 하고, 게임에 대한 정보를 관리하는 Round를 만드는 대신, Car에게 Location을 추가하여 객체의 특징을 더 잘 살릴 수 있었다.


디자인 패턴에 너무 국한되어 생각하게 되면, 오히려 더 무거운 설계를 통해 근본적인 기능을 할 수 없을 수도 있다는 점을 배웠다.

3. 코드리뷰를 통한 성장

프리코스 커뮤니티를 통해 코드리뷰를 하고, 받을 수 있었다. 남들의 코드를 하나하나 읽으며 내가 생각하지 못한 부분에 대해 깨달음을 얻을 수 있었다.

MVC View에 대한 책임을 명확히 하기, 테스트 코드에서 어노테이션을 적극 활용하여 반복코드 줄이기, 추상화를 통해 모듈간의 관계를 잘 구축하기, 한 줄에 . 한개만 쓰기 등의 객체지향 체조 원칙 등의 리뷰를 받았고,

동료들의 소중한 리뷰를 통해 이번 과제에서는 더 나은 코드를 작성할 수 있었다. 그 중 인상깊은 내용을 조금 정리해보았다.

  • 매직넘버 줄이기

매직넘버를 줄이기 위해 상수를 많이 사용했지만, 고려하여 구현한 부분 외에도 상수가 많이 사용된 모습을 볼 수 있었다.

이에, 이번 미션에서는 같은 실수를 하지 않으려 코드에 보이는 하드코딩된 숫자들에 주목했고, GAME_WIN_CONDITION , CARNAME_MAXIMUM_LENGTH 등의 변수들이 하드코딩 된 것을 확인할 수 있었다.

이런 하드코딩된 변수들의 특징을 파악해보니 게임 환경설정과 관련된 변수들이라는 것을 깨닫고 GameConfig를 만들 수 있었다.

결국 객체지향 체조 원칙을 지키다보면 다른 원칙까지 지키게 되는 것이 정말 신기했다

하나하나가 직/간접적으로 연결되어 있었고, 이를 통해 더 좋은 코드를 작성할 수 있게 되는 것 같다.

동료의 코드를 리뷰하며 내가 챙기지 못한 부분도 확인할 수 있었다. 우테코에서 제공한 Console library를 사용하다보니 Scanner를 사용한다는 사실을 망각하여 Scanner.close()를 호출하는 것을 망각하였는데, 이번 미션에서는 잘 처리해주었다!

  • 검증의 위치

기존에는 검증 로직을 일급 컬렉션에서 사용할 때, 검증자를 따로 구현하지 않아도 된다고 생각하였다.

검증을 일급 컬렉션 내부에서 하지 않고 외부로 분리하는 것은 일급 컬렉션의 특징에 어긋난다 생각했기 때문이다.


하지만 검증자를 구현하고 검증자를 일급 컬렉션 내부에서 객체 생성시 사용하면 모듈을 분리하고 클래스의 책임을 분리하여 더 객체지향적 코드에 가까워질 수 있었다.


그렇기에 이번 미션에서 검증자를 새로 구현하게 되었다. 차의 이름에 대한 검증을 해야하기 때문에 처음에는 이를 Car 클래스의 생성자에 추가했다.

하지만 차의 이름은 단순히 String값이 아니라, 비즈니스 로직상 여러 유효성을 검증할 필요가 있는 특수한 단위라고 생각되었다.


그렇기에 CarName을 따로 분리하였고, 이를 통해 더 역할을 잘 분리할 수 있었다.

같은 맥락으로 Location 클래스도 분리하였다. 이 프로그램에서는 단순히 move()를 통해 값을 증가하고, 이를 출력하는 역할 뿐이지만 나중에 최대 거리 및 최소 우승조건이나, 뒤로 가기 등의 기능이 추가되어 거리에 음의 값에 대한 검증 필요 등의 상황을 고려했을 때, 이렇게 Location을 분리한 버전이 확장성이 훨씬 좋은 코드가 되리라 확신한다!

4. 함수 분리

본 미션에서 선택한 코드를 함수로 분리해주는 단축키인 option+command+m을 정말 많이 썼다.

최대한 기능을 세분화하여 함수를 분리하고, 한 메소드당 인덴트를 2로 하려 노력했다.

함수를 분리하는 과정의 중요성은 익히 알고 있었다. 최대한 분리하는 과정을 통해 한 메서드가 한 가지의 책임만을 가질 수 있었다. 정말 ‘한 가지’의 기능만 하게 함수를 구현하다보니 함수명이 길어지고, 복잡하게 느껴지는 경우도 있었다.

하지만 오히려 함수의 역할이 더 잘 드러나게 되어 다른 로직에서 가져가서 사용하기에도 용이하고, 가독성 또한 높아질 수 있었다.

자바 코드 컨벤션 중 인덴트를 3이하로 유지하는 것, 메서드의 라인이 15라인을 넘어가지 않는 것, else를 쓰지 않는 것, Do-while을 사용하지 않는 것 등의 규칙은 결국 메서드를 최소한의 책임을 갖게 하는 것으로 귀결되었다.

그럼에도 의문을 가졌다.

단순히 함수 여러개를 호출하는 것을 묶기 위해 함수를 만드는 게 의미가 있을까?


미션에서, Cars객체가 게임을 진행하는 함수와 라운드 결과를 출력하는 함수가 반복되게 구성하였다.

private static void proceedRounds(Cars cars) {
cars.playRacingGame();
OutputView.printRoundResult(cars);
}

두 함수를 단순히 호출만 하는 로직을 묶어 하나의 기능으로 치부하였다.


처음에는 그저 분기하는 횟수만 늘려 성능만 오히려 안 좋아질 수 있다고 생각했다.

하지만 기능은 결국 또다른 기능들의 합으로 구성되어 있고, 이를 메서드의 단위로 분리할 수 있다고 생각했다.


개인적으로는, 메서드로 만들 수 있는 유의미한 기능이면 메서드로 분리하는 것은 당연하다

라는 결론을 내릴 수 있었다.

util 함수를 따로 구현한 것도 함수를 분리하는 과정에서 터득할 수 있었다.

저번주 미션과 달리 객체의 생성이 필요하지 않고, 비즈니스 로직상 필요한 기능들을 util로 묶어 구현하였다.

사용자로부터 입력을 받고 이를 비즈니스 로직에서 사용하기 위해 데이터를 재조립하는 과정에서 convertStringToList, convertStringToInt , generateRandomValuesForCarGame , joinStringWithComma 등의 함수를 분리할 수 있었다. 비슷한 기능을 하는 함수끼리 묶어 응집성을 보장하고, 유지 보수성이 좋은 구조의 프로그램을 구현할 수 있었다.

5. 함수별 테스트

위와 같이 함수를 철저히 분리하고 나니 함수에 대한 테스트를 작성할 때 매우 용이했다.

테스트의 단위에 대한 고민을 저번주에 이어서 할 뻔 했으나, 함수의 분리에 중점을 두고 구현을 했기에 테스트를 해야할 함수가 한 눈에 들어왔다.

private 메서드는 public 메서드에서 호출되게끔 비즈니스 로직을 구성했으므로 public 메서드를 위주로 테스트 코드를 작성했다.

지난주에는 테스트 코드를 작성하며, 로직에서 버그를 발견하고 수정하는 과정을 밟았다.

이번주에는 비교적 구현을 늦게 시작했기에 테스트 코드를 작성하는 과정에서 사실 버그가 발생하면 이를 해결할 생각에 시간이 부족할까 조금 겁이 났다. 그렇기에 테스트 코드를 구현하며 버그를 찾으려 했지만 버그를 찾을 수 없었다.

실제로 설계에 힘을 실었기에 리팩토링 과정에서 논리 오류를 빠르게 발견할 수 있었기 때문이라고 생각한다. 테스트 코드의 구현까지 마친 현재에서는 이는 설계에 힘을 쏟은 게 너무 뿌듯하지만, 당시에는 버그가 나오지 않는 것이 오히려 더 불안했었다.

저번 미션에서는 테스트가 요구사항에 없었음에도 연습을 위해 테스트 코드를 작성했고, 테스트 케이스의 범위, 구조, 프로덕션 코드 수정에 대한 깊은 고민을 했고, 이번주에는 적용할 수 있었다.


감사합니다.

728x90
반응형