본문 바로가기

[우아한테크코스] 프리코스 2주 차 회고: 자동차 경주

@yeong0jae2024. 10. 29. 23:59

 
 

 
 
10월 22일 2주 차 미션이 시작되었다.
 
2주 차 미션은 자동차 경주였다. 🚗
https://github.com/woowacourse-precourse/java-racingcar-7

 

GitHub - woowacourse-precourse/java-racingcar-7

Contribute to woowacourse-precourse/java-racingcar-7 development by creating an account on GitHub.

github.com

 
 
+ 제 PR 링크입니다
https://github.com/woowacourse-precourse/java-racingcar-7/pull/934

 

[자동차 경주] 김영재 미션 제출합니다. by yeong0jae · Pull Request #934 · woowacourse-precourse/java-racingcar-7

🧑‍🤝‍🧑 To Reviewers 이번 과제를 진행하면서, 읽기 쉬운 코드를 작성하려고 꽤나 노력했던 것 같아요. 1주차에 코드 리뷰를 하면서 정말 다양한 지원자들이 있구나 느꼈어요. 그래서 2주차에

github.com

 
 
 
 

🤔  계산기는 어땠지?


 
1주 차 계산기 미션이 끝나고, 우테코로부터 공통 피드백이 날아왔고 사람들과 코드 리뷰를 하는 시간도 가졌다.
 
공통 피드백에는 1주 차를 진행하며 잘 지켰다고 생각한 항목들도 있었고, 미처 생각하지 못했던 부분들도 있었다.
코드 리뷰를 하면서도 마찬가지였다. 내가 발견하지 못한 부분들, 다른 사람들은 어떻게 구현했고 왜 그렇게 구현했는지 각자의 생각들이 있었다.
 
그래서 나는 각 항목들에 대해 "나는 어땠지?" 생각해보는 시간을 가졌다.
 
 

📑 GitHub Desktop으로 읽기 좋은 커밋 메시지 남기기

 
공통 피드백에는 "좋은 리뷰를 위해 좋은 커밋 메시지를 작성한다"라는 내용과 함께 아래 글의 링크가 있었다.
 

 
https://meetup.nhncloud.com/posts/106

 

좋은 git 커밋 메시지를 작성하기 위한 7가지 약속 : NHN Cloud Meetup

git커밋

meetup.nhncloud.com

 
 
커밋 메시지의 목적은 "팀원 모두가 보기 쉽게 코드를 추적하자!" 이기에, 때로는 변경사항을 한 줄로 요약한 커밋 메시지가 더 효율적일 때도 있다.
하지만 "충분히 내용이 있고, 잘 갖춰진 커밋 메시지를 작성할 때는 어떻게 해야 할까?"에 대한 내용이 글에 담겨있다!
 
내용이 영문 기준으로 작성되어 있어서, "한글일 때는 어떨까?"에 대해 생각해 보며 읽었고 "본문은 어떻게보다 무엇을, 에 맞춰 작성하기"는 angularJs 커밋 컨벤션에 있던 내용이어서 다시금 상기할 수 있었다.
 
새롭게 알게 된 내용은 "제목과 본문을 한 줄 띄워 분리하기"였다.
그리고 깃허브 데스크탑(GitHub Desktop)을 사용하면서, 이걸 사용하면 제목과 본문을 자동으로 분리해 준다는 사실도 알 수 있었다.
 
커밋 메시지를 작성할 때 제목과 내용을 한 줄 띄어 분리하지 않으면, 커밋 로그가 아주 보기 싫게 망가진다.
 
깃 데탑을 사용해서 커밋할 때는 어떨까?

 
위는 깃 데탑을 이용해 commit 할 때의 인터페이스다.
제목과 내용을 입력하는 부분이 보기 좋게 나눠져 있다.
 
근데 중요한 건 인터페이스가 아니라, 커밋 로그도 제목과 내용이 분리되어 찍히는 가? 이겠다.
그래서 확인해 보았다.
 

 
위는 git log를 실행했을 때이다. 
제목과 내용이 보기 좋게 분리되어 있다.
 
git log --oneline을 실행했을 때는 어떨까?

 
보기 좋게 커밋 제목만 한 줄(one line)로 찍힌다!
(좋네요😊)
 
 
 

🏃 우선 구현해 볼까?

 

 
공통 피드백 마지막에는 "1주 차 요구 사항을 어떻게 구현하는지"를 직접 보여주는 영상이 있었다.
 
1주 차 과제를 진행하면서 "다른 사람들은 실제로 어떻게 구현할까?"에 대한 호기심이 있었기에, 그냥 이렇게 저렇게 구현하나 보다.. 하기보다는 "어떤 생각을 근거로 구현을 해나가는지"를 중점적으로 보았다.
 
그리고 이는 2주 차 과제를 진행하는데 큰 도움이 되었다! 🙇‍♂️
 
영상에서 인상 깊었던 점 중 하나는 요구사항을 정말 잘게 나눠 구현한다는 것이었다.
나는 1주 차 과제를 진행할 때, "기본 구분자일 때 덧셈하기", "커스텀 구분자일 때 덧셈하기"처럼 기능을 나눠서 바라보고 구현해 나갔다.
(스스로 잘 나눠서 구현했다고 생각했다.. ㅋ ㅋ 😅)
하지만 영상에서는 “”⇒0을 구현하고 “1, 2”⇒3을 구현하는 등 내가 생각했던 것보다 요구 사항을 더 잘게 나눠 구현하고 있었다.
 
그래서 "이렇게 구현하면 어떤 장점이 있을까?" 궁금했고 2주 차 과제를 하면서 바로 적용해 보려 노력했다.
 
과정에서 느낀 점은 요구사항을 더 꼼꼼히 지킬 수 있다는 것이었다. 요구사항을 잘게 나눠 구현하다 보니, 큰 단위로 기능을 바라볼 때보다 자연스럽게 요구사항을 더 세세하게 바라보게 되고, 세세하게 바라보니 빼먹지 않을 수 있었던 것 같다.
그리고 더 재밌다는 것도 장점이었다! 😊
실제로 작은 기능 작게 바라보고 하나씩 구현해 나가면서, 작은 성취에 즐거움을 느꼈고 과제가 더욱 재밌게 느껴졌다. 그래서인지 1주 차 보다 몰입이 잘 됐던 것 같다.ㅎㅎ
 
 
또 인상 깊었던 점은 과한 설계를 미리 생각하지 않고 현재의 구현 수준을 고려해서 상수화, 메서드 분리 등의 클린 코드를 위한 작업을 해나간다는 것이었다.
즉, “설계를 어떻게 잘할까?”를 막연히 고민하기보다, 먼저 기능을 구현하다 보면 설계의 근거가 보인다는 것이다.
 
이 부분도 의식해서 2주 차 과제를 진행했다. 요구사항을 보고 큰 틀은 생각하되, 우선 작은 구현부터 해나가며 필요를 느낄 때 리팩토링을 거쳤다.
이렇게 구현하니 구현보다 앞서서 설계를 고려하느라 낭비되는 시간도 없었고, 정말 필요를 느끼는 부분에서 코드를 개선하다 보니, 불필요한 코드들이 점점 거둬져 가는 게 느껴졌다.
 
결과적으로 1주 차보다 훨씬 보기 좋은 코드가 나온 것 같다! (나만 읽기 쉬운 걸지도..? 😂)
 
 

💬 ↻ 리뷰를 리뷰하기

 
프리코스 첫 코드 리뷰였다.
지원 당시 나는 "소통하는 것을 좋아하고, 그러니 소통을 더 잘한다면 더 큰 즐거움을 느낄 수 있겠다!"라는 생각으로 소통 능력을 향상하기 위해 "리뷰에 대한 피드백"을 목표했다.
 
리뷰에 대한 피드백이라고 하면.. (피드백을 피드백하는(?)) 것이라고 생각한다. 즉, 리뷰 내용 자체도 중요하겠지만 "리뷰가 어땠는지" 생각해 보는 것이다.
 
그래서 1주 차 코드 리뷰를 하고 나서 리뷰에 대해 피드백을 해보는 시간을 가졌다.
도움이 되었던 리뷰가 무엇이었는지 골라내어, 왜 그 리뷰가 유익했는지에 대한 이유를 알아보았다.
도움이 되었다고 생각되는 리뷰에는 공통점이 있었다.
 
바로 의견에 근거가 포함되어 있다는 것이었다. 왜 근거를 남기는 것이 더 좋았을까? 생각해 보았는데,
의견만 달랑 남긴다면, 리뷰어가 왜 어떤 생각으로 이런 의견을 제시한 건지, 리뷰어가 리뷰어의 의도를 파악하기 힘들기 때문인 것 같다.
즉, 의도가 파악되어야 "아 나는 이런 근거로 구현의 방향을 잡았는데, 리뷰어님은 나와는 다른 근거를 갖고 있었구나, 저렇게 생각할 수도 있구나!" 이런 사고의 선순환이 일어난다고 생각한다.
 
그리고 이러한 사고의 선순환은 "근거가 포함된 리뷰"를 나눌 때 일어난다는 것을 1주 차 코드 리뷰를 하며 경험할 수 있었다.
그래서 2주 차 코드 리뷰에는 근거를 포함해 더 의미 있는 리뷰를 남겨보려고 한다.
그리고 2주 차 코드 리뷰가 끝나면, 또 리뷰를 리뷰(?)해서 더 좋은 리뷰가 뭘까? 생각해봐야겠다.
 
 
 

🤔  과제를 진행하며 고민했던 사항


 

👀 1. 랜덤 숫자 생성기 사용할 사람 손?

이번 과제를 하면서 가장 재밌었고, 오래 고민한 부분은 “Car와 Race 중 누가 랜덤 생성기에 의존해야 하는가?"였다.
나는 Car를 자동차로서 경주에서 각각의 움직임을 담당하는 객체로 정했고, Race는 여러 대의 자동차(Car)를 관리하고, 모든 자동차가 회차마다 전진을 시도하도록 하는 객체로 정했다.
 
처음에는 Car가 랜덤 생성기에 의존해야 한다고 생각했다.
자동차는 액셀을 밟아 스스로 움직이는 자율적인 존재이므로, Car 객체가 직접 move를 결정해야 한다고 판단했기 때문이었다.
반대로, Race가 랜덤 생성기에 의존해야 한다는 생각도 들었다.
이는 경주 내의 모든 자동차가 같은 확률로 이동해야 공정한 레이싱이 되지 않을까..?라는 관점이었다.
 
오랜 고민 끝에, Car가 랜덤 생성기에 의존하도록 구현했다.
이유는 위처럼 move라는 행동은 개별 자동차가 자율적으로 수행해야 하며, 랜덤 값 생성도 그에 따라 Car가 직접 담당하는 것이 적합하다고 판단했기 때문이었다.
그리고 같은 랜덤 생성기에 의존하는 것이 공평하다기보다, 더 전진 확률이 높은 랜덤 생성기에 의존하는 Car가 더 성능이 좋은 자동차가 된다는 관점으로 바라보았다! 이 설계가 레이싱 도메인에 더 부합한다고 느꼈다.
 
나는 이렇게 도메인에 대해 생각하는 시간을 정말 재밌어하는 것 같다..!! 앞으로 과제도 기대된다. 😊
 

NumberGenerator를 주입받는 Car
List<Car> 만을 관리하는 Race

 
 
 

👀 2. 캡슐화와 협력의 균형 맞추기 (feat. Runnable로 행동 전달받기..😅)

사실 이 부분은 과제를 하면서 어찌어찌 구현하긴 했지만, 이게 과연 객체지향적으로 옳은 방법으로 해결한 걸까? 뭔가 께름칙한 부분이 남아있었다.
 
그리고 과제가 종료된 후 코드를 돌아보면서 역시나.. 잘못됨을 인지했다..ㅋ
 
나는 시도할 횟수를 Rounds라는 객체로 포장했다. 그리고 Rounds가 시도 횟수의 유효성 검사와 레이싱의 반복을 책임지도록 구현했다. 
그리고 경주의 준비, 시작, 종료를 담당하는 Racing 객체에서, Race의 moveAll 메서드를 Rounds의 count 만큼 반복해야 했다.
 

Racing의 경주 시작 메서드

 
하지만 나는 이를 Race와 Rounds의 캡슐화를 지킨답시고.. (뇌가 순간 뒤틀렸다보다 🧠)
"어떻게 Race와 Rounds가 서로 의존하지 않도록 구현하지?"를 최선을 다해 고민했다...
🙈
고민 끝에 나온 결과가 moveAll이라는 함수만 넘겨줘서, 이를 Runnable로 받고 수행하는 것이었다. (구현하고 천잰가 싶었다.)
 

Rounds의 repeat 메서드
Race의 moveAll 메서드

 
하지만 "객체지향이 추구하는 방향이 무엇인가?"를 다시 생각해 보면서 잘못되었음을 깨달았다.
 
객체지향의 핵심은 객체가 메시지 주고받음으로써 협력을 구성하는 것이다. 그런데 단순히 Runnable을 열어서 moveAll이라는 행동을 전달받는 방식은 객체가 직접 자신의 행동을 수행하는 것이 아닌, 외부에서 넘겨받은 행동을 수행하는 일이다.
객체의 자율성을 완벽히 저해하는,, 판단이었다.
 
이번 과제를 하면서 getter를 최소화하는 것에 시간을 정말 많이 쓰다 보니, 너무 과하게 캡슐화를 하려 했고, 결과적으로 이런 구조가 나왔던 것 같다.
너무 어렵게 생각했던 것 같다. Race든 Rounds든 공용 인터페이스를 열어서 서로 협력하게 구현하는 것이 자연스러웠을 텐데.. 좀 아쉬웠다!
 
그래도 이번 고민을 경험으로 캡슐화와 객체 협력의 그 경계를 잘 지키는 것이 어떤 느낌인지 감을 잡기 시작한 것 같다ㅎㅎ😊
 
 
 

👀 3. 불필요한 getter를 최소화하기 (feat. 테스트는 설계를 돕더라~)

이번 고민은 테스트 코드로부터 시작됐다.
1주 차 회고를 통해 2주 차 목표로 "주어진 테스트 외에 필요한 테스트를 구현하는 것"을 목표했었고, 테스트 코드로 프로덕션 코드를 피드백하고자 하였다.
그래서 의식적으로 작은 기능을 구현하기부터 테스트를 함께 작성했고, 테스트를 작성하며 프로덕션 코드에 잘못된 부분은 없는지 의식적으로 도움을 받아보려 했다.
 
실제로 과제를 진행하며 테스트 코드로부터 도움을 받아 불필요한 getter를 없앨 수 있었다.
 
먼저 불필요한 getter는 너무나 public 하게 외부에서 객체의 상태에 접근할 수 있게 된다는 점에서 캡슐화가 깨진다.
당연히 필요한 부분에서는 getter를 써야겠지만, 지양하려는 노력을 해야 한다고 생각한다.
 
그리고 내 우승자 판별 기능 테스트 코드에는 race.getCars().get(0).move() 라던지, winners.get(0).getName()와 같이 Race, Cars, Car를 관통하면서 모든 상태로 깊은 접근을 하는 코드가 존재했다.
심지어 Race의 getCars()는 테스트에서만 쓰고 있는 코드였다.
그래서 더 문제 삼게 되었다.
 
과연 테스트만을 위해 getter를 여는 것이 맞는가? 난 절대 아니라고 생각했다.
테스트를 위해 getter를 쓰면 테스트를 위해 프로덕션 코드가 있게 돼버리는 것 아닌가..? 나는 지금 프로덕션 코드를 위해 테스트 코드를 짜고 있다고 생각했다.
 
그럼 문제의 원인은 무엇이었을까? Race의 구조였다. 
Race가 List<Car>를 매개변수로 받아서 생성되어야만, 우승자 판별 테스트 시 given으로 개별의 Car를 생성해 move한 후 winners.getFirst()만으로 우승자가 올바른지 확인할 수 있다. 아래처럼..!
 

테스트 코드의 개선
Race의 구조 개선

 
이전에는 Race 객체의 생성자가 과한 책임을 지고 있었기에, 테스트에서 Car에 접근하려면 Race를 통해 캡슐화를 해치며 깊이 접근해야 했고, 불필요한 getter를 쓰게 된 것이었다.
 
따라서 carNames를 파싱하고 cars를 생성하는 역할을 Input과 Racing으로 분리했다.
이 문제를 해결하면서, 테스트 코드가 없었다면 과연 구조를 개선할 수 있었을까?라는 생각이 들었다.
 
테스트는 단순히 기능의 동작뿐만 아니라, "기능이 올바른 구조인가?"에 대한 판단도 도와준다는 것을 실감할 수 있었다.
 
 

👀 4. MVC라는 네이밍 버리기, 누구나 읽기 좋은 코드란?

1주차 과제를 하면서는 구조를 계산기 컨트롤러, 뷰, 그리고 도메인 모델들로 구성했다. 과제를 구현하다보니 MVC라는 구조로 점점 다가갔고, 네이밍도 그렇게 짓게 되었다.
 
그런데 2주차 과제를 시작하기 전 느낀 것은, 오히려 MVC라고 잡고 시작해버리니까 내가 MVC에 갖혀서 생각하게 되지 않을까?
나는 MVC에 얽메이기보다, 지원 당시 목표처럼 객체지향 프로그래밍을 나대로 이해하고 적용하고 싶었다.
책임이 컨트롤러에 들어가야할지, 모델에 들어가야할지 고민하기 보다는, 내가 스스로 주어진 도메인을 이해하고 메시지를 생각한 후 객체를 선택하고 역할을 부여해나가고 싶었다.
 
그래서 2주차에서는 MVC라는 네이밍을 버렸다.

 
위와 같이 작은 기능부터 만들어나가면서, 필요성을 느낄 때마다 대로 분리해나갔다. 처음엔 domain으로 시작해서, 입출력을 담당하는 view를 만들게 되었고, 외부 시스템인 Randoms는 domain에서 external 분리하였다. 대신 domain에서는 숫자 생성기 인터페이스에 의존한다.
 
나는 이번 과제를 진행하면서, 읽기 쉬운 코드를 작성하려고 꽤나 노력했던 것 같다.
 
1주차에 코드 리뷰를 하면서 정말 다양한 지원자들이 있구나 느꼈다. 그래서 2주차에는 내 코드를 읽는 사람이 비전공자든, 전공자든 상관없이 잘 읽히는 코드로 만들고 싶은 마음이 컸다.
 
특히 나만 아는 네이밍을 쓴다거나, 구현 수준에 맞지 않는 과한 설계를 한다면 오히려 리뷰어 입장에서 읽기 어려운 코드가 되지 않을까? 생각했다.
 
그래서 2주차는 프로그래밍을 잘 모르는 사람이 읽더라도, 직관적으로 읽을 수 있도록 네이밍과 함수 분리, 객체 간의 협력 관계를 중심으로 많이 노력했던 것 같다.
 
 

👀 5. 디버거를 활용하기

과제를 하면서, 공통 피드백으로 새로 배운 디버거의 장점을 크게 체감했다.. (지금까지 몰랐던 게 부끄럽다 😅)
 
문제 상황은 Race 객체의 생성자에서 this.numberGenerator가 null이라는 오류였다.
원래 같으면 절대 발견 못했을 것 같다. (system out 찍고 있었으니..)
 
오류를 발견하고 바로 디버거를 이용해 프로그램의 시작점부터 step into, step over로 흐름을 추적했다.
 

 
덕분에 numberGenerator가 초기화되기 전에 Car가 생성된 것이 문제의 원인임을 빠르게 파악할 수 있었다.
단순히 생성자에서의 값 초기화 순서 문제였다.
 
2주 차 과제를 하며 디버거를 처음 활용해 보았는데 이런 기능이 있는 게 참 놀라웠고, 실제 개발할 때도 디버거와 같은 도구를 활용하는 차이가 크겠구나 참 와닿았던 경험이었다. 🙇‍♂️
 

 

 

 

🏁 👀 나의 고민은 끝나지 않는다2.. + 깃발 다시 꽂기


 
2주 차는 시험 기간이 겹쳐 시간적으로 정말 빠듯했고, 그래서 온전히 이틀 밖에 쓰지 못했다.. 그런데 오히려 이틀간 확 집중해서 그런지 시간이 많았던 1주 차보다 더 과제에 빠져들었다.
1주 차는 여러 가지 사실들을 넓게 학습하고 배웠다면, 2주 차는 하나하나에 대해 정말 깊게 고민하는 시간이었던 것 같다.
그만큼 배운 것들이 많은 자동차 경주였다.
 
프리코스를 이제 반절 지나왔지만, "목표를 얼마나 달성했나?"라는 질문에 대한 답이 잘 나오지 않았다.
많이 달성했다고 하기에는 여전히 목표에 비해 부족함이 보이고, 적게 달성했다고 하기에는 지금까지 배운 점이 너무 많기 때문이다.
과제를 하며 여러 가지 문제에 직접 부딪히고 해결하는 과정을 거치다 보니, 내가 예상하지 못했던 부족한 점과 더 학습해야 할 부분이 계속해서 드러나는 듯하다.
 
대신 “목표를 향해 얼마나 잘 나아가고 있는가?”라고 스스로에게 질문하고 싶다.
'얼마나 잘 나아가고 있는지'의 기준은 내가 설정한 목표가 정말 나에게 필요한지, 그 목표를 달성했을 때 얼마만큼의 성장이 기대되는지, 그리고 현재 내가 그 과정을 즐기고 있는지를 매 순간 고민하며 나아갔는지에 있다고 생각한다.
이런 측면에서 충분히 잘 나아가고 있다고 스스로에게 말해주고 싶고, 프리코스 덕분에 하루하루가 뿌듯하다. 😊
 
지금까지 2주 동안 과제를 해오면서 완벽하지 않은 상태에서도 한 발씩 나아가는 것이 중요하다는 것을 깨달았다.
완벽한 설계나 정답을 미리 찾으려 하기보다, 완벽하지 않은 상태에서도 작은 기능을 구현하면서 점진적으로 설계를 개선하는 과정을 거칠 때 더 재미가 느껴졌다.
 
앞으로도 이런 배움들을 잊지 않고 나머지 과제들도, 내 삶에서 일어나는 모든 일들도, 작은 성취에 즐거움을 느낀다면 내가 미래에 뭘 하게 되든지 일도 행복해지고 삶도 행복해지지 않을까 생각한다.
 
 

 
 

yeong0jae
@yeong0jae :: yeongjae’s dev

서버 개발을 공부하는 대학생입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차