우아한테크코스 자동차 게임 회고

홍혁준·2023년 2월 24일
0

우테코에서 첫 미션이 끝나고 일주일 정도 지났다. 시간이 많이 지나기 전에, 회고를 하려고 한다.

미션 진행 내역

자동차 경주 Repository

GitHub - hong-sile/java-racingcar at step2

1단계 PR

https://github.com/woowacourse/java-racingcar/pull/459

2단계 PR

https://github.com/woowacourse/java-racingcar/pull/613

자동차 경주 게임

기능 요구사항

  • 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
  • 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
  • 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
  • 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
  • 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.

프로그래밍 요구사항

  • 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
  • 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.를 지키며 구현한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
  • 규칙 2: else 예약어를 쓰지 않는다.를 지키며 구현한다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
  • 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.

페어 프로그래밍

페어 프로그래밍은 애자일 개발 방법론 중의 하나로 하나의 개발 가능한 PC 에서 두 명의 개발자가 함께 작업하는 것을 말합니다. 네비게이터(navigator)가 전략을 제시하고 드라이버(driver)가 실제 코드를 작성하며, 이 열할을 각자 번갈아가며 수행합니다. 짝 프로그래밍이라고도 합니다.

우아한 테크코스만의 독특한 점이 있는데, 모든 미션이 페어 프로그래밍으로 진행된다는 것이다.

나 역시 이번 미션에서 페어와 함께 미션을 진행했다.

페어 프로그래밍을 하며 되게 좋은 경험을 할 수 있었지만, 첫 페어 프로그래밍이다 보니 미숙한 점이 많아 아쉬운 점도 있었다.

좋았던 점

나의 첫 페어는 실무 경험이 많은 크루였다. 서비스를 만든 경험이 있고, 아직도 그 서비스를 유지하고 있다.(대단한 크루다)

덕분에 많은 것을 배울 수 있었다. 여러 인텔리제이 단축키부터, 다양한 패턴들까지 경험에서 나오는 기술들을 많이 배울 수 있었다. 그리고 페어가 경청을 엄청 잘해줘서, 나도 의견을 내는데 거리낌이 없었다.

페어 프로그래밍 특성 상 한 코드여도 드라이버와 네비게이터 둘 모두 이해하고 있어야 했는데, 소통이 잘 되어서 코드에 대한 이해가 잘됬었던 것 같다.

아쉬웠던 점

이번이 첫 페어 프로그래밍이기에, 거기서 나오는 미숙함이 너무 많았다. 페어 프로그래밍 시작전에 어떤 식으로 진행하는지 검색이라도 할껄

빠르게 코드를 작성해야한다는 강박에 코치님들의 설명만 듣고 바로 페어를 시작했다. 역할 전환(드라이버 ↔ 네비게이터) 시간도 안 정하고, 하다가 “음 이정도면 바꿔볼까요?” 하면서 역할 전환을 했었다.

네비게이터와 드라이버의 역할을 빈번하게 바꿔 코드에 대한 이해가 강제(?)되도록 했어야 했는데, 그 점이 좀 아쉬웠다. 그 외에도 룰을 정했어야 했는데, 그러지 못한게 아쉬웠다.

이런 경험을 겪고나서 다음 미션에서는 시작전에 페어와 꼭 룰을 정하고 시작했다. 페어 전환하는 주기부터 쉬는 시간까지 또 사람마다 다르기 떄문에 서로 알아가면서 룰을 정하는게 엄청 중요한 것 같다.

미션 진행

step1 구현

첫 미션이었던만큼 실수가 되게 많았던 것 같다. 최대한 프리코스 때 학습한 지식을 다 때려넣으려고 했다.

최대한 확장 가능하게 설계하고 구현하는 것은 많은 지식을 필요로 하기에, 나는 무조건 확장성이 높고, 쓸 수 있는 기술을 다 적용하는 것이 좋은 방법이라고 생각하였다.

다른 분들에게 리뷰를 받는 것이기에, 더더욱 그렇게 이번 미션을 구현하였고, 이러한 생각이 문제가 있다는 것을 나중에 알게 되었다.(이 부분은 뒤에 더 자세히 이야기 하겠다.)

이번 미션자체는 어려운 로직이 들어가진 않아서, 기본적인 기능을 구현하는데는 큰 어려움이 없었다.

모든 로직에 테스트를 작성하라는 요구사항을 구현하는 것이 어렵긴 했다. 특히 이번 미션엔 랜덤성과 같은 제어할 수 없는값이 나와 이 부분이 문제였는데, 페어와 나 모두 전략패턴이라는 적절한 방법을 알고 있었고, 해당 방법을 이용해 랜덤 값에 대한 테스트도 진행할 수 있었다.

step1에 대한 피드백

나는 도메인과 뷰의 의존성을 분리하기 위하여, 도메인에서 뷰로 값을 전달하는데 dto라는 방식을 사용했다.
뭐 dto를 사용할 수준의 프로젝트인가를 제쳐두고, 나는 dto를 사용하는 목적이 하나 더 있었다. 바로 getter를 쓰지 않기 위해서이다.

객체지향 프로그래밍에 대해 당시 얕은 지식이 있었고, 이를 미션에 적용하려했다. 바로 디미터 원칙이다.
디미터 원칙은 객체의 값을 가져와 원하는 일을 처리하지 말고, 객체 자체에 메시지를 보내 원하는 일을 처리하라는 원칙이다.
나는 여기서, 객체의 값을 가져오지 말라는 의미를 왜곡해서 받아들여, 극단적으로 getter를 쓰지 말아라 라고 이해했다. (이래서 아무것도 모르는 사람보다 책 한권만 읽은 사람이 무섭다..)

뭐 다른 도메인 로직이야 어찌어찌 getter를 쓰지 않고, 메시지를 보내는 식으로 처리할 수 있지만, 화면에 모델의 값을 출력해야 했기에 뷰에 데이터를 전달하기 위해 getter를 써야했다.

하지만, 패턴에 잡아 먹혀버린 나는, 그냥 객체에 getter라는 메서드를 만드는 것을 싫어했고, 페어를 억지로 설득해, private한 필드를 넘기기 위해 그 클래스에서 dto를 생성해 넘겨버리는 방식을 채택하게된다.

(view가 domain에 의존하지 않지만, domain이 view에 의존해버리는 아이러니한 상황….)

디미터 법칙이 전하고자 하는 바를 이해하지 못하고, 그저 법칙만 무지성으로 따르다가 일어난 일이다.

나중에 아예 dto를 삭제하고, getter를 이용해 도메인에서 뷰에 데이터를 전달하게 하였다.

step2 구현


원래 step2에서 기존 step1에서 추가적인 요구사항이 들어간다. 하지만 이번에 들어온 요구사항은 뷰와 모델의 의존성 분리였고, step1에서 이미 뷰와 모델의 의존성을 분리하였기에, 나는 테스트 코드 작성에 집중하였다.

테스트 코드에 대해서 프리코스 때 처음 배웠고, 미션 요구사항에도 테스트 코드를 작성하라는 내용이 있었기에, 가능한 모든 메서드에 대해서 테스트를 진행하려고 하였다.

테스트 코드와 프로덕션 코드


그런 나에게 가장 큰 고민은 “테스트를 위해 어디까지 프로덕션 코드가 변경될 수 있는가?”였다.

테스트 코드에 대해 공부하면 공부할 수록, 테스트 코드가 구현의 보조적인 부분이 아니라, 구현의 필수적인 부분으로 생각하게 되었다.
그래서 나는 “테스트 코드를 위하여, 실제 설계 코드의 구조가 변경될 수 있다!”고 생각한다.
아마 다들 이 말에 동의할 것이다.

책임이 잘 분리되고, 설계가 좋은 코드는 테스트하기 쉽다.

실제로 각 메서드가 하나의 책임만을 갖고 있다면, 테스트를 짜기가 쉽다.

테스트하기 어려운 코드는 책임이 잘 분리되지 않았고, 설계가 좋지 못하다.

이 말은 이전에 나온 말에 대한 대우관계이다. 애당초 테스트 코드를 짜기 어렵다면, 프로덕션 코드의 설계 자체가 잘못되어 있을 확률이 높다.

그 외에도, 어느정도 테스트를 위해서 구조를 변경하는게 코드를 작성하는데 비용이 더 적게 든다.

자 여기까지 읽었으면, “테스트 코드를 위해 실제 설계 코드의 구조가 변경될 수 있다”는 사실에 동의 할 것이다. 그렇다면 이제 다른 문제를 고민해야 한다.

🧑‍💻 “설계 코드의 구조가 변경될 수 있다면 어디까지 프로덕션 코드에 변화를 줄 수 있을까?”

위 문제에 대해서 정말 많이 고민을 하였지만, 명확한 답을 내리기 힘들어 리뷰어분께 헬프를 청해봤다…

진짜 리뷰를 하나씩 받고 질문에 대한 리뷰어분의 이야기를 들을 때마다 우테코 너무 좋아진다.

이 글을 읽고나서, 아직 명확하다고 말할 정도는 아니지만, 어느정도 테스트 코드를 짜기 위해 어떻게 해야할지에 대한 방향성을 잡을 수 있었다.

단위 테스트에서 메서드의 의존성


또, 테스트를 할때 메서드간의 의존성에 대해서도 고민을 많이했었다.
내가 만약 상태를 변화시키는 “명령”을 하는 메서드의 실행여부를 확인하려한다면 어떻게 해야할까?

getter를 쓰거나, “조회”를 하는 메서드를 만들어야 할것이다.

하지만 view에 전달될 도메인 클래스에 getter를 만드는 거까지는 납득했지만, 그냥 테스트를 위해서 getter를 만든다? 이건 진짜 아니라고 생각했다.
아무리 테스트를 위해 프로덕션을 변경할 수 있다곤 하지만, 프로덕션 코드에서 사용될 여지가 없는(아 물론 쓰면 몇몇 로직이 간편해지겠지만 그것은 객체지향적이지 못하기에) getter를 만드는 것을 납득하지 못했다.
아 view에 전달하기 위해 getter가 있는 도메인 객체에서는 getter를 사용해 테스트 하였다.

그렇다고 “조회”를 하는 메서드를 이용해 “명령”을 한 메서드의 실행결과를 확인한다?
만약 “조회”를 하는 메서드가 정상적으로 동작을 하지 않는다면?
목표로 한 “명령”을 하는 메서드를 정상적으로 테스트했다고 말하기 어려울 것이다.
바로 메서드간의 의존성이 생겨서 이게 좋지 못하다고 생각했다.

그렇게 고민하다가 결국 두 가지를 떠올렸는데

  • Mocking을 하여 내부의 메서드가 원하는 방식대로 사용되었는지를 확인한다(행위 검증)
    → 현 프로젝트에선 mock 라이브러리가 없음…
  • 그냥 getter를 만들어서 쓴다.(상태 검증)

두 개 다 만족스럽지 않았다. 근데 로직상 잘 찾아보니 상위 객체에서 정의된 getter를 활용할 수가 있었어서, 목표한 객체를 필드로 갖는 상위객체를 정의하고 그 곳에 목표 객체를 주입하여 해결하였다.

etc. AssertJ의 extracting에 대해서 알고 있었지만 반환형태가 AbstractObjectAssert 였기에, List에서 사용할 수있는 contains와 같은 메서드를 사용하지 못했다.
지금은 asInstanceOf라는 메서드를 알게되어, 진짜 getter를 하나도 쓰지 않고, 상태에 대한 검증을 할 수 있게 되었다!!!

step2 피드백


step2와 같은 경우는 크게 변경할 점이 없어서, 따로 큰 피드백이 없었다. 자잘한 코딩 컨벤션 또는 주석에 대한 내용이어서, 꼼꼼하게 체크하면 해결될 문제였다.

총정리


첫 번째 미션이었던 만큼 열정적으로 미션을 진행하였지만, 제대로 프로젝트를 진행해본 적도 없고, 페어 프로그래밍도 진행해본적이 없었기에 많은 실수들이 있었지만, 그 실수들로부터 배우는 것들이 많았다.

이 글을 쓰는 시점에, 2번째 미션을 끝냈는데, 1번째 미션에서 배웠던 것들을 다 적용할 수 있었어서, 좋았다. 성장해가는 것이 실시간으로 느껴진다. 지속적으로 셀프 피드백을 하며 성장해 나가야지.

profile
끊임없이 의심하고 반증하기

4개의 댓글

comment-user-thumbnail
2023년 2월 24일

진짜 덕분에 extracting / asInstanceOf 알고갑니다... 무한한 감사....
많은 고민을 하고 많은 시도를 하신게 녹아든 회고인 것 같아요!
정말 잘 보고 갑니다 ㅎㅎㅎ

1개의 답글
comment-user-thumbnail
2023년 2월 26일

스스로 엄청나게 고민하며 풀 수 있는 걸 알 수 있는 글이였습니다..
저도 다음번엔 하나하나 다 의심하며 풀어봐야겠네요 ㅎㅎ

1개의 답글