조종 다음은 개발
article thumbnail

🏎️ 자동차 경주

https://www.kkday.com/ko/product/129639-formula-1-singapore-airlines-singapore-grand-prix-2023-marina-bay-street-circuit

2 주차 미션은 바로 자동차 경주였다.

우테코 4기 프리코스에서 제공된 문제였던 것 같은데 6기에서도 2 주차미션으로 주어졌다.

https://github.com/woowacourse-precourse/java-racingcar-6

 

GitHub - woowacourse-precourse/java-racingcar-6

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

github.com

 

내 PR 링크는 아래와 같다.

https://github.com/woowacourse-precourse/java-racingcar-6/pull/1130

 

[자동차 경주] 최원준 미션 제출합니다. by jhon3242 · Pull Request #1130 · woowacourse-precourse/java-racingcar-6

 

github.com

 

 

1 주차에서 2 주차로 넘어가면서 디스코드 채널도 열리고 코드 리뷰도 활성화되면서

한 껏 더 몰입할 수 있었다. 1 주차 코드 리뷰에 대한 내용은 1 주차 회고글을 통해 확인할 수 있다.

아무튼 2 주차 미션에 대해서도 회고글을 작성해 보려고 한다!

그러면 바로 시작하겠다~~!!

🧐 2 주차 미션에서 배운 것

📦 원시값 포장

이번에 원시값 포장에 대해서 알게 되어 이를 정리하고 실제로 미션에도 적용해 보았다.

실제로 원시값 포장을 적용하기 전에 Car 객체의 모습이다.

우선 원시값 포장을 활용하기 이전의 코드를 살펴보자.

 Car 객체에 안에 자동차 이름에 대한 검증이 구현되어 있기 때문에 복잡해 보인다.

만약 요구사항이 추가되어 position 에 대한 검증 로직도 위 코드에 추가된다면 어떨까??

(예를 들어서 “최대 위치는 10까지이다.”와 같은 요구 사항이 추가되면 위 코드는 훨씬 더 복잡해질 것이다.)

원시값을 포장하는 방식으로 리팩터링 한 Car 객체에 모습이다.

검증에 대한 로직을 각각의 객체에 위임하면서 Car 에는 비즈니스 로직만 남게 되었다.

매우 깔끔하게 바뀐 것을 느낄 수 있다.

이전에 있던 검증 로직이 CarName 객체로 위임되었다.

(추가로 자동차 이름 설정과 관련된 상수 MAX_NAME_LENGTHCarName 으로 이동하였다.)

이로서 자동차 이름과 관련된 모든 책임을 CarName으로 위임되면서 책임 분리에 성공했다.

 

원시값 포장에 대해서는 따로 정리하여 블로그에 포스팅하였다.

원시값 포장에 대해 알아보자!

 

원시값 포장에 대해 알아보자!

1 주차 회고글을 보다가 어떤 분이 원시값 포장에 대해 언급하신 것을 보게 되었다. 원시값 포장이 뭔지 잘 모르기 때문에 이번 기회에 정리해 보려고 이 글을 작성하게 되었다. 🤔 원시값 포장?

flight-developer-stroy.tistory.com

 

🪐 JUnit 활용

2 주차에서는 테스트 코드를 더 활용해 보았다.

코드 리뷰를 통해 JUnit 이 제공하는 다양한 기능에 대해서 알게 되었다.

그래서 이를 2 주차에서 활용해보고 싶었다.

 

그래서 우선 JUnit 에 대해서 알아보고 정리하였다. 🧐🧐

https://flight-developer-stroy.tistory.com/44

 

JUnit에 대해 알아보고 사용해보기 👀

1 주차 미션을 마무리하고 다른 분들의 코드를 리뷰하면서 테스트 코드에서 다양한 기능을 제공한다는 것을 배울 수 있었다. 그래서 이번 기회에 어떠한 기능들을 제공하는지 정리해보려고 한

flight-developer-stroy.tistory.com

그리고 해당 내용들을 활용하여 2 주차 미션에서 테스트 코드를 작성하였다.

단순히 @DisplayName 뿐만 아니라 @ParameterizedTest 에 대해 학습하여

파라미터를 통해 하나의 테스트로 다양한 값들을 테스트하는 코드를 작성해 보았다. 😁😁

 

 

😵‍💫 2 주차 미션에서 고민했던 것

🚚 값만 전달하는 메서드??

2 주차에서는 일급 컬렉션과 원시값 포장을 활용하였다.

그 결과 내가 구현한 도메인 구조는 위와 같았다.

  • Cars : List<Car> 를 가지고 있는 일급 컬렉션으로 전체 자동차를 관리
  • Car : 인스턴스 변수로 CarName , Position 을 가지고 있으며 자동차의 역할을 한다.
  • CarName : String 으로 자동차 이름을 가지고 있다. 자동차 이름에 대한 검증을 관리한다.
  • Position : int 로 자동차 위치 정보를 가지고 있다. 위치에 대한 검증을 관리한다.

객체 안에 객체가 구현되어 있는 형식이었다.

이와 같은 구조에서 자동차들 중에서 가장 멀리간 위치값을 구할 때 다음과 같은 상황이 발생했다.

Cars 객체에서 가장 멀리간 값을 구하기 위해서는

CarsCarPositionamount(int 형) 의 흐름대로 접근이 필요했다.

 

그렇다 보니 객체에서 단순히 내부 객체의 값을 전달하기 위한 메서드가 등장하게 되었다.

중간에 껴있는 Position 객체의 경우 단순히 amount 값을 Car 로 보내기 위해 메서드(getAmount) 구현이 필요했다.

그 결과 car.getPosition().getAmount() 와 같이 작성하게 되었다.

지금은 요구사항이 어느 정도 간단해서 위에서 끝났지만 만약 객체 구조가 복잡한 경우는 어떨까??

getUserList().getUser().getAddress().getState().getcity().getStreet().getNo()

매번 내부 객체를 호출하기 위해서 위와 같이 메서드를 구현해줘야 할까?

그렇지는 않을 것 같았다.

이에 대해 고민하던 중 다른 동료의 회고글에서 디미터 법칙에 대해서 알게 되었다.

이름은 혹시 모르니까 모자이크 처리했다.

디미터 법칙은 다른 객체가 어떠한 자료를 가지고 있는지 속사정을 몰라야 한다는 규칙이었다.

이 규칙을 지키면 객체는 자료를 숨기는 대신 함수를 공개하여 캡슐화를 높여 객체의 자율성응집도를 높일 수 있다.

이 규칙을 적용하여 기존의 Car 객체의 메서드 getPositionPosition 객체를 리턴하는 것이 아닌, PositiongetAmount 를 호출한 결과를 리턴하는 방식으로 리팩터링 하였다.

그 결과 getPosition 만 호출하는 방식으로 바뀌어 코드가 깔끔해졌다.

 

🚨 그러나 여전히 생각해 볼 문제는 남아있는 것 같다. 🚨

  1. 아직 단순히 값 전달을 위한 메서드는 Position 에 남아있다.
  2. Position 객체 자체가 필요한 경우와 Positionamount 가 필요한 경우를 어떻게 구분할 수 있을까?

2번 문제의 경우 메서드 명을 통해 어느 정도 해결 할 수 있을 것 같은데 1번 문제에 대해서는 계속 고민하고 있지만 명확한 해법은 나오지 않았다.

아무래도 내부 값을 가져오기 위한 전달 메서드는 어쩔 수 없이 발생하는 것 같다.

이에 대해서는 계속 고민해 봐야겠다.

🙄 상수를 별도로 관리해줘야 할까??

상수를 통해 관리

다른 동료들의 1 주차 과제를 보면서 대부분 상수들을 Enum이나 별도의 클래스로 관리한 것을 확인했었다.

나의 1 주차 미션 코드 중 상수들을 별도의 객체(GameOption)에서 관리

나 또한 1 주차 미션동안 게임과 관련된 int 형 상수들은 위와 같이 별도의 객체를 통해 관리해주었다.

 

이를 확장해서 나는 2 주차에서는 문자열도 상수들로 관리해 주었다.

저번 미션에서 인스턴스화 방지에 대한 피드백을 받아서 이를 적용하였다.

위와 같이 객체를 통해 관리를 해주니까 깔끔해져서 좋아 보였다.

 

🧐 과연 가독성이 향상된 걸까?

그렇게 만족을 하면서 내 코드를 다시 읽어보고 있었다.

그러다 문득 이렇게 상수화 하는 게 가독성에 도움이 될지 의문이 들었다.

상수를 사용한 방식
문자열 그대로 사용한 방식

둘 중 어느 것이 더 가독성이 있다고 느껴질까??

나는 둘 중 문자열 그대로 나타낸 두 번째 버전이 가독성이 더 좋다고 느껴졌다.

상수로 선언되어 있으면 해당 상수가 어떤 값인지 한 번 더 탐색하는 과정이 필요하기 때문에 한눈에 어떤 역할을 하는지 안 느껴졌다.

 

물론 상수로 선언하면 값이 변경되었을 때 관리하기 편하다는 장점이 있다. 그리고 인텔리제이에서는 ‘command + 클릭’을 통해 해당 변수가 선언된 곳으로 바로 이동하여 어떤 값을 선언했는지 쉽게 파악이 가능하다.

 

그런데 내 코드를 읽을 대부분의 사람들은 PR을 통해서 읽을 것이다. 그런데 깃허브 PR 에서는 ‘command + 클릭’ 기능을 제공하지 않는다. 결국 해당 상수가 어떤 값인지 탐색하기 일일이 객체를 탐색하는 과정이 필요하고 이는 가독성 저하를 야기할 것이다.

 

메시지를 제외한 상수들은 각 객체에서 관리

그래서 나는 상수들을 최대한 관련된 객체에서 관리하는 방식으로 리팩터링 하였다.

이렇게 리팩터링 하면 해당 상수를 찾기 위해서 다른 객체를 탐색할 필요가 없기 때문에 가독성에 크게 저하되지 않는다고 판단하였다.

 

 

그러나 ViewMessageErrorMessage 는 기존대로 별도의 객체에서 관리하는 방식을 유지하였다.

 

ErrorMessage 의 경우 특정 객체에 국한되지 않기 때문에 만약 각 객체에서 관리하면 유지보수에 어려움이 생길 것 같았다.

 

ViewMessage 의 경우 InputViewOutputView 에서 관리해 줘도 되지만 그렇게 되는 경우 내부에서

public static final String 으로 선언된 변수들이 너무 많아져 결국 별도의 객체 분리가 필요해질 것 같았다.

또한 출력 메시지의 경우에는 변수명으로도 충분히 어떤 값이 들어있을지 요구사항을 통해 파악이 가능하다고 생각했다.

 

이러한 이유로 ViewMessageErrorMessage 는 기존대로 별도의 객체에서 관리하는 방식을 유지하였다.

그 외의 값들은 각각의 객체에서 관리하는 방식으로 리팩터링 하였다.

😣 테스트 코드 활용의 불편??

🫠 시작은 작은 리팩터링에서,,

나는 Car 를 리스트로 가지고 있는 Cars 를 구현하였다.

그리고 Cars 객체에서 요구사항에 맞게 60 % 확률로 전진을 시키는 메서드를 구현하였다.

전진할지 말지에 대한 책임이 Cars 에게 있다.

그러다가 문득 전진을 할지 말지에 대한 책임이 과연 Cars 가 가지고 있는 게 맞을지 의문이 들었다.

Cars 객체는 리스트에 있는 Car 객체들 전체를 관리하는 책임을 가지고 있다.

그렇기에 Car 가 전진할지 말지에 대한 책임은 각각의 Car 객체 스스로가 가지고 있는게 옳다고 느껴졌다.

 

그래서 60% 확률로 전진하는 책임을 Car 가 가지도록 리팩터링 하였다.

이렇게 리팩터링 하고 만족하고 있었다.

🚨 문제 발생!

그런데 문제는 테스트에서 발생하였다.

아래 테스트는 3개의 자동차 객체 중 하나만 전진시키고 우승자를 계산하였을 때 하나의 자동차만 있는지 확인하는 테스트이다.

리팩터링 이전에는 이동할지 말지에 관한 책임이 Cars에게 있기 때문에 Car는 moveFoward라는 메서드가 호출되면 항상 이동되었다.

그런데 Car 가 60% 확률로 전진하는 방식으로 리팩터링 하면서 테스트도 60% 확률로 통과되었다.

이를 해결하기 위해 100% 확률로 전진시키는 별도의 메서드를 구현하면 간단하게 해결된다.

그런데 테스트 만을 위한 코드를 작성하는 게 맞을지 의문이 들었다.

 

그래서 이에 대해서 구글링을 해보았다.

그 결과 테스트만을 위해 코드를 추가하는 것은 비즈니스 모델과 구현 스펙에 차이를 만들기 때문에 지양하는 것이 맞다는 결론에 도달했다.

😯 Assertions 를 활용해서 문제 해결 시도

이러한 문제를 해결하기 위해서 Assertions 에서 제공되는 assertRandomNumberInRangeTest 를 활용하였다.

(Assertions 에 대한 설명은 이전에 포스팅한 글을 참고하면 좋을 것 같다.)

 

이를 통해 하나의 객체만 100% 확률로 이동하도록 제어하였다.

이러한 경우를 위해서 프리코스에서 Assertions 를 제공하구나 생각하게 되었다.

 

이렇게 또 문제를 해결했다 생각하면서 나는 또 만족하고 있었다😏

그러나 실무에서도 이렇게 해결할 수 있을지 의문이 들었다.

실무에서는 아마도 우테코 API 인 Assertions 를 활용하지 않을 것이다.

그렇다면 지금 상황과 같이 제어할 수 없는 테스트의 경우 어떻게 할 수 있을까?

 

🥳 제어할 수 없는 값은 외부에서 주입받는 방식으로 문제 해결

이에 대해서 고민하면서 또 열심히 구글링을 하였다. 그러다가 ‘테스트하기 좋은 코드’에 대한 블로그 글을 보게 되었다.

2. 테스트하기 좋은 코드 - 제어할 수 없는 코드 개선

 

2. 테스트하기 좋은 코드 - 제어할 수 없는 코드 개선

1편 을 통해 테스트하기 어려운 코드에 대해 이야기를 나눴다. 이번 편에서는 테스트하기 어려운 코드 중 첫번째인 "제어할 수 없는 코드를 개선하는 법"을 이야기해보자. 2-1. 문제 상황 먼저 앞

jojoldu.tistory.com

이 글을 보면서 제어할 수 없는 값을 외부에서 주입받도록 하면 문제를 해결할 수 있다는 것을 알게 되었다.

그래서 이에 맞게 또 리팩터링을 하였다.

Car 가 전진하는 메서드의 이름을 moveRandomlymoveForwardIfTrue 로 바꾸었고, 파라미터로 true 를 받는 경우 전진하도록 리팩터링 하였다.

이와 같이 리팩터링 해주면서 이제 테스트에서는 Assertions 를 활용하지 않고 자동차의 움직임을 제어할 수 있게 되었다.

드디어 문제를 제대로 해결하였다. 🥳🥳

계속 고민하고 검색해 보면서 스스로 문제를 해결한 것 같아서 뿌듯했다.

동료들과 같이 고민했더라면 더 빠르게 문제를 해결할 수 있었을 것이다.

하지만 이러한 내용은 미션에 대한 얘기이기 때문에 나눌 수가 없었서 조금 아쉬웠다 🥲

 

 

 

profile

조종 다음은 개발

@타칸

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!