TDD,클린코드 with 자바

[TDD,클린코드 with JAVA] 자동차 경주 피드백 모음

딤섬뮨 2022. 11. 21. 17:54
728x90

자동차 경주 피드백 모음

 

GitHub - next-step/java-racingcar: 자동차 경주용 게임을 관리하는 저장소

자동차 경주용 게임을 관리하는 저장소. Contribute to next-step/java-racingcar development by creating an account on GitHub.

github.com

넥스트 스텝 15기 클린 코드를 수강하며 정리하는 글입니다.

넥스트 스텝에 참여하며 [자동차 경주]를 시작으로 처음 리뷰를 받았고 , OOP가 뭔지 모르는 무지의 상태에서 첫 시작을 도와주신 리뷰어님께 아주아주 감사한 자동차 경주 교육이었다 :)
이때의 나와 지금의 나는 정말 200% 다른 듯..?! 아님 말고

코드는 주소로 가면 볼 수 있습니다  나중에 시간 되면 처음부터 다시 구현해보고 싶다.

많이 느끼는 사항은 객체지향은 정말 관점에 따라 휙휙 바뀐다..!

몇가지 생각해볼 사항은 to be continue로 남겨두겠다!

변수

  • 변수명은 줄여쓰지 않는 습관(CarNum 이런 것도 X CarNumber0)
  • 막연한 이름을 사용하면 역할 또한 막연하게 많이 가져갈 수밖에 없다. 다른 역할은 분리하자
  • is- 네이밍은 boolean 값을 반환하길 기대한다. :몇 네이밍을 찾아보자.
  • 지역변수로 선언할 것과 필드 변수로 선언할 것의 차이를 잘 생각해보자

코드 포맷팅

서비스 레이어 존재 이유

어쩌면 이 리뷰가 내 개발 습관을 완벽히 바꿀 수 있는 리뷰가 아녔을까?
프로젝트를 진행할 때 Service에 비즈니스 로직을 다 때려 넣었던 지난날들이었다.
마찬가지로 이번에도 그렇게 구현을 했더니 , 서비스 레이어에 필요한 관점에 대해 자세히 피드백해주셨다.
의미 없는 복붙은 하고 싶지 않았지만 이건 정말 좋은 말이기에 그대로 인용해보겠다.

정확하게 제가 현재 미션에서 서비스를 지양하는 이유는 아래와 같습니다.

사람들은 흔히 비즈니스 로직을 서비스에 담는다고 표현하기 때문에 객체지향에서 객체로 풀어내야 할 것을 서비스에 풀어내는 유혹에 빠지기 쉽습니다.
질문하면 비즈니스 로직이 들어가야 하는 곳이 서비스라면 객체들은 무슨 일을 하나요? 데이터 덩어리고 비즈니스 로직은 서비스에 담기는 게 맞나요? 아니라면 서비스의 역할은 정확하게 무엇인가요? 위 질문에 명확하게 답변이 나오지 않는다면 굳이 이 개념을 이 미션에서 공부해야 하나 라는 생각이 가장 먼저 들어요. 말씀하신 대로 위 유혹에 빠지지 않고 단순히 게임의 흐름을 제어하기 위해 여러 객체를 호출하고 조합하는 역할만 하도록 구현한다면 컨트롤러보단 서비스에 있는 게 더 적절합니다.
다만 저는 1번의 이유로 인해서 서비스를 두는 것 자체가 현재 단계에서 필요하지 않다고 생각합니다. 1번을 잘 지키신다면 서비스를 두고 지금처럼 작성해주셔도 됩니다.
서비스에서 해야 하는 역할은 프로그램의 흐름을 결정하는 역할이라고 생각해요. 프로그램이 정상 동작하기 위해 어떤 도메인들을 호출하여야 하는지를 알고 호출하는 역할을 담당한다고 생각해요. 디비와 엮인다면 트랜잭션의 단위가 될 수 도 있습니다. 현재도 여러 객체를 호출하는 형태로 잘 되어 있습니다. 현재 구조에서 아쉬운 부분은 carService.prePare()로 호출한 값으로 다시 moveCar를 호출하는 부분인데, 애초에 한 메서드에서 동작할 수 있다는 의미가 아닐까요? 추가로 서비스라는 개념 없이 컨트롤러에서 메서드들을 호출해도 충분할 것 같습니다. 구현 레벨로 본다면 carService.move(gameRound, totalCarNumber, strategy)를 호출하는 게 되겠죠?


기억에 남는 문구 두 개로 대체하겠다

  • 객체는 데이터 덩어리가 아니다. 능동적인 객체로 만들어야 했다.
  • 서비스는 프로그램의 흐름만 결정하는 딱 거기까지의 역할일 것이다.

사실 이 피드백을 받을 때 까지도 무슨 말인지 잘 몰랐다. 그런데 [객체지향의 사실과 오해] 책을 읽으면서 아.. 그때 말씀하신 게 이런 말이구나를 깨달았다.

난수의 테스트

자동차 경주는 랜덤 한 값을 받아서 자동차가 운전을 하게끔 한다.
따라서 랜덤한 난수의 테스트 어려움에 봉착한다.
초반에 이에 대해 테스트는 항상 성공하는 테스트를 작성하셔야 합니다. 우리가 제어할 수 있는 부분(난수를 제외한)은 테스트할 수 있게 되고 그 부분이 테스트되어야 하는 대상입니다.라고 했다.
테스트하기 어려운 값은 추상화하여 최상위로 분리하면 그 하위에 있는 모든 곳에서 테스트를 할 수 있게 된다.

(이 부분에 대해서는 TDD 라이브 강의 정리에 내가 정리해놓았다)

FIRST와 SRP문제

  • SRP 이 궁금증은 이전 controller SRP 게시물에도 게시를 했었다.

이번에도 한번 여쭤보았다.

gameStart() 함수는 작은 기능(자동차 운전, 랜덤 값 생성)들을 가지는 메서드를 모아서 하나의 큰 기능(경기)을 시행하는 main과도 같은 메서드처럼 구현했습니다.
어떤 기능을 구현해도 이와 같은 메서드가 나오길 마련인데 이런 메서드는 어쩔 수 없이 항상 SRP 원칙을 위배하는 것일까요?

해당 메서드의 책임이 무엇이라고 생각하시나요? 어떤 책임을 가지고 있는지에 대해 먼저 고민해봐야 SRP도 같이 고민할 수 있을 것 같아요.

말씀하신 기능이 프로그램 흐름을 담당한다.(직접 수행하는 것이 아니라 호출해야 할 대상을 알고 호출하며 흐름을 진행한다)라고 정의한다면 SRP라고 볼 수 있지 않을까요?

 

: 관점의 차이다. 어떤 책임을 가지고 있는지 살펴보자

[이전 게시물]
2022.08.11 - [한 이음 프로젝트] - [한 이음] Controller는 SRP(단일 책임 원칙)을 지키는 걸까..?

 

[한이음]Controller는 SRP(단일 책임 원칙)을 지키는걸까..?

최근 다음과 같은 로직의 코드를 짰다. 처음에 리팩토링이 아니라 통짜로 코드를 짜서 멘토님께서 리팩토링을 하라고 하셨다. 이 일은 내게 엄청난 고민을 안겨주었고 사실 해결은 못했지만..

sienna1022.tistory.com

  • FIRST는 TDD를 구현하다가 생긴 나의 궁금증이었다.
기능 : carInitStatus()는 자동차 이동 상태를 1로 세팅하기 위한 함수입니다.

노력 : 실제 테스트 코드의 통과 여부(상태가 1인가?)를 확인하기 위하여 carService.carStatus() 함수 <상태 반환 함수>를 호출 실제 값이 잘 들어있나 확인을 하는 Assertion을 적용하였습니다.

고민: 클린 코드 책을 보니깐(혹시 책이 있으시다면 pg.167)에 단일 테스트 원칙[F.I.R.S.T) 중 Independent를 위배하는 게 아닌가 싶었습니다. carInitStatus()를 테스트하기 위해 또 다른 메서드인 carStatus()를 호출하는 격이니깐요..

이럴 경우엔 테스트를 어떻게 하면 좋을지 고민이 되어 질문드립니다!


이에 대해 답변을 다음과 같이 주셨다

테스트는 주어진 조건에서 어떤 행위를 했을 때 어떤 결과가 나온다를 검증하는 작업이기 때문에 기본적으로 독립적일 수는 없습니다. 다만 말씀 주신 원칙은 우리는 어디까지는 믿어도 되는 영역이며 어디는 믿을 수 없는 영역인가에 대한 경계가 필요할 것 같아요. 예를 들어 객체의 상태를 변경하고 getter를 호출하는 것도 independent라고 할 수 있지만 getter는 이미 검증된 메서드라는 가정이 있기에 이 테스트는 dependent 하다고 볼 수 있을 것 같습니다.

다만 말씀 주신 것처럼 테스트하는 메서드와 검증하는 메소드 두가지 모두 구현해주신 메소드이고 A가 B를 사용하는 레벨이 아니기 때문에 Independent하다고 보여집니다. 이 부분은 리팩토링 이후에 동일한 현상이 발생하면 같이 고민해보면 좋을 것 같습니다.

테스트를 위한 메소드와 hash and equals() 오버 라이딩 관점

  • 테스트를 위한 메서드는 제거하자. 차라리 값을 검증하기 위해 getter를 사용하는 것은 괜찮습니다. 차라리 getter를 사용해보자
  • 강의에서 hash and equals()를 빈번히 사용하는 것을 볼 수 있다. 그런데 리뷰어님은 오버 라이딩이 필요한 부분에서 사용할 줄 알아야 한다고 하셨다.

그렇다면 언제 hash and equals()를 사용해야 하는가..?

일단 이 함수를 쓴다는 것은 , 참조값이 아닌 객체의 데이터로 값을 비교하게 된다.
 
비교의 목적이 어디있냐에 따라 달라지는 문제이다.
예를 들어 자동차의 '이름'을 비교하고 싶었는데 new Car("car1",1)과 같이 Car 객체로 비교하기 위해 position값도 설정하게 되면 이름만 비교하고 싶었는데 position까지 같이 끼게 되어  의도치 않게 객체들의 같음을 판단하는 조건이 달라질 수 있다.
물론 데이터 값으로 재정의해서 비교해도 괜찮지만 
객체의 값을 비교하기 위해서 재정의하는 것과, 객체를 비교할 때 주소 값이 아닌 값을 통해 비교를 하겠다고 생각하여 재정의하는 것은 다르다는 걸 인지하셨으면 좋겠다는 의도였습니다.

ETC

예외를 던지는 게 좋을까 default값을 던지는게 좋을까 생각해보자.

728x90