로또 미션을 진행하면서 다시 페어가 바뀌었다. 이번에는 10분씩 번갈아가면서 하는 것이 아니라 메소드 하나씩 잡고 번갈아가면서 코딩하는 방식으로 진행했다. 초반에는 쉬운 기능목록만 해서 괜찮았는데 마지막 쯤 로또의 결과를 구하는 부분에 와서는 수익률 계산과 Enum, 일급컬렉션을 어느 객체에 구현할 지와 도메인 설계가 부족했던 탓인지 멘붕이 왔다.

이게 한 번 멘붕에 빠지니까 코딩을 제대로 할 수가 없었다. TDD 또한 메소드부터 작성하려니 마음처럼 잘 안됐다. 마감 시간 전까지도 페어를 하며 구현하는 것이 힘들어서 서로 동의 하에 페어를 깨고 각자 진행하는 것으로 했다. 늦지 않게 제출하긴 했지만 마감 시간이 조금 더 길었다면 마음 편하게 페어를 했을텐데 하는 아쉬움이 남았다.

TDD에 대한 두려움을 떨쳐내자. 일단 해당 기능의 메소드부터 작성하고 구현해보자. 자신감을 갖자

우테코의 말말말

페어를 하면서

  • 좋았던 점
    • 적극적으로 피드백을 줬을 때
    • 자신을 솔직하게 드러냈을 때
    • 잘 모르는 부분은 내가 잘 이해할 수 있을 때까지 알려주고 기다려줬을 때
  • 아쉬웠던 점
    • 적극적이면 좋겠다
    • 자신감을 갖고 개발하면 좋겠다
    • 자신의 생각과 다른 것도 수용하자
    • 코딩할 때 급하게 하지 말자
    • 의견을 더 구체적으로 말하자

포비의 말말말

  • 개발자로 자수성가하려면 스타트업에 가라
  • 집에 가서 공부할 수 있는 환경을 만들어라
  • 원래 페어로 할 때 힘들다. 마감기한이 있으니 코드의 퀄리티를 낮추더라도 일단 미션을 제출해라
  • 서로 간의 감정상태를 솔직하게 공유하자
  • 기록은 짧아도 상관없으니 무엇이든 꾸준하게 기록해라
  • 책에 있는 내용을 모두 정리하려고 하는 생각은 버리고 문제를 어떻게 해결했는지 어떤 감정을 가졌는지, 자기 생각을 기록하자

내가 받았던 피드백

  • 가독성 향상을 위해 숫자의 경우 천 단위마다 언더스코어(_)를 붙이자
  • 클래스명, 메소드명, 변수명, 테스트명 등 여러 부분에서 네이밍에 신경쓰자
  • 사람이 보기 쉽게 코드를 짜자
    • 인자로 아무 것도 주지 않는 것보다 인자를 줌으로써 더 이해가 잘 간다면 인자를 넣도록 하자
  • 선언할 때는 좀 더 넓은 범위인 인터페이스로 선언해서 호출하거나 확장하는데 편한 코드를 작성하자
  • 클래스에 검증로직이나 비즈니스 로직이 없다고 해서 잘못된 건 아니다
  • 객체의 역할에 따라 비즈니스 로직을 설계하자
  • 모든 원시값과 문자열을 포장하자- 어떤 클래스로 포장했다면 그 클래스로 리턴하는 것도 생각해보자
    • 포장한 클래스로 리턴한다면 메서드 내부를 보지 않고 반환 값만 보고 더 쉽게 판단할 수 있다.
    • 또한 컬렉션의 경우 put과 같이 상태를 변경하는 메서드를 노출시키지 않도록 한다
    • 객체의 역할 분배를 하기 위한 이유도 있다
  • intString 으로 바꾸고 싶다면 + "" 가 아닌 String.valueOf() 를 쓰자
  • 상수화에도 무작정 private 을 쓰는 게 아니라 여러 곳에서 사용된다면 public 으로 써도 된다. 다만 어느 객체에서 사용할 것인지는 잘 생각해봐야 한다.
    • 예를 들어, 이번 로또 같은 경우 MIN_RANGE, MAX_RANGE 의 책임은 LottoNumber 가 가져가야 한다
  • 생성자가 2개 이상 늘어나면 정적 팩토리 메서드를 생각하라
    • 정적 팩토리라면 생성자를 private으로 만들어라
    • 생성자는 하나만 있게 하고, 정적 팩토리 메서드를 2개 만들어서 처리를 해주자
    • 정적 팩토리 메서드를 사용하면 미리 저장해둔 값과 비교하는 테스트코드도 짜야한다.
      • assertThat(LottoNumber.from(1) == LottoNumber.from(1)).isTrue();
  • 검증코드가 생성자에 있건, 정적 팩토리 메서드에 있건 상관없다
  • 테스트 메서드 네이밍 컨벤션을 적용해서 테스트하려는 메서드_테스트하려는 상태_기대하는 동작 의 형식으로 쓰자

다른 크루들의 피드백

  1. 메소드 명으로 무엇보다 크고 작음을 나타낼 때 LessThan, LessThanEqual, GreaterThan, GreaterThanEqual 으로 짓는 것이 관례다
  2. 테스트코드의 변수명을 실제 값은 actual, 기대한 값은 expected 로 짓는 것이 관례다
  3. DTO와 generator를 도메인으로 보지 않는다
  4. Money를 값객체로 활용한다면 동등성을 보장하기 위해 equals, hashcode도 구현하는 게 좋다
  5. 정상적인 경우도 테스트하자
  6. 클래스의 구현 순서(보통 public 이 먼저오고 그 다음에 private이 온다)
    class A {
     // 클래스 변수
     // 인스턴스 변수
     // 생성자
     // 정적 팩토리 메소드
     // 메소드(접근별로 하는 게 아닌 기능별 순서로)
     // 기본 메소드 (equals, hashCode, toString)
     // getters and setters
    }
    
  7. 숫자를 비교할 때 Integer.compare() 를 이용하자
  8. 기능이 동일하다면 컬렉션에 메서드로 존재하는 contains 를 메서드 명 그대로 짓는 게 좋다
  9. 탭 혹은 스페이스 바로 일관되게 공백을 쓰는 게 좋다. 들여쓰기를 하나로 통일하자
  10. Collectors.joining() 에 delimiter 뿐만 아니라 prefix, suffix 를 정해줄 수 있다
  11. List<LottoRank> lottoRanks 가 선언되어 있을 때 lottoRanks.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) 를 통해 Map<LottoRank, Long> 형태로 반환해줄 수 있다
  12. HashMap에서 getOrDefault, putIfAbsent 를 활용하자
  13. 항상 null safe를 생각하자.
  14. null이 반환될 수 있는 곳에는 Optional 을 생각하자
  15. 생성자가 정상적으로 생성됐다는 것을 테스트 해줄 때는 assertThat(actual).isNotNull(); 로 해주자
  16. 에러에 해당하는 값(ERROR)이 Enum 안에 포함되는 것은 좋은 방법이 아니다. orElseThrow() 를 활용하자. 다만 아무것도 당첨되지 않았을 때 아무것도 없음(NONE)에 해당되는 값이 Enum 안에 포함되는 건 현 미션에 경우에는 괜찮다.
  17. enum 필드에는 final을 붙이는 것이 관례다
  18. 생성자에서 List<Lotto> lottos 를 받아줄 때 this.lottos = new ArrayList(lottos); 를 통해서 null safe한 로직을 짜줄 수 있다
  19. TICKET_PRICE = 1000; 와 같은 상수명도 티켓 하나의 값이라는 의미이므로 LottoTIcket 클래스에 public으로 PRICE = 1000; 이라고 선언해서 LottoTicket.PRICE 을 통해서 갖고 오도록 하자

배운 내용

로또 미션의 도메인 설계

  • 처음에 가장 하위노드인 LottoNumber를 도출하는 것이 어렵지만 도출할 수 있다면 TDD로 개발하기 쉽다.
  • 상태를 가지는 도메인 객체를 설계하는데 집중해라. 이것만 가지고 조립을 하면 된다.
  • Money 객체로 원시값으로 포장하면 돈과 관련된 모든 로직을 Money에 넣어줄 수 있다
  • 클래스 다이어그램
    • 처음부터 메서드를 도출하기는 쉽지 않다.
    • 클래스와 상태를 도출하도록 노력하자.
    • 의존관계를 어떻게 맺는지 이해하자
    • LottoTicket은 LottoNumber에 의존관계를 가진다.
    • 1 : N이라면 Collection이 들어가야된다는 걸 알 수 있다.
    • 메서드 내부에서 의존관계를 갖는 경우, 점선으로 표기한다.
    • 실선은 강하게 의존관계를 갖는 경우다. (상태값에 다른 클래스 이름이 들어있으면)
    • 클래스 다이어그램을 짜고 나서 어디서부터 TDD로 할 것인지 생각하자
  • LottoTicket을 생성한다면 LottoTicketFactory라고 명하자. 뭐 그 안에 create, getInstance, of 등등…
  • 설계는 점진적으로 업그레이드 해나가자

점진적인 리펙토링

  1. 기능 목록을 작성할 때 간단하게 해도 괜찮다
    • 로또 전체를 구현하려는 욕심을 버려라
    • 다 하는 것이 아닌 중간 단계부터 테스트를 시작하려는 생각을 해야된다.
    • 어떤 input이 있을 때 어떤 output이 나오고 등등…
  2. 일단 구현하자
    • 일단은 동작할 수 있는 것에 집중
    • getter고 뭐고 일단 쓰자.
  3. 리펙토링을 생각해보자
    1. 작성한 메서드가 어느 객체에 맞는 건지 끊임없이 고민하자
    2. 값이 유일한지 생성자에서 검증
    3. 원시값 및 문자열 포장
    4. 일급컬렉션
    5. 정적 팩토리 메서드
    6. 캐싱
  4. 점진적인 리펙토링
    1. 메소드에 인자가 추가되면?
    2. 메소드를 사용하는 모든 곳에서 컴파일 에러가 발생함
    3. 해결해도 다른 곳에서 테스트케이스 컴파일 에러가 발생
    4. 다시 디버깅
    5. 즉, 점진적인 리펙토링을 하자
    6. 자료구조를 어떤 것을 쓸 것인지에 대한 고민도 필요하다
    7. 상속보다 조합(ArrayList를 상속할 경우, 많은 퍼블릭 메소드를 가지기 때문에 메소드 호출에서 실수할 수 있다. 조합으로 하면 필요한 메소드만 있게 된다)
  • 점진적인 리펙토링을 하자
    • 기존의 테스트 케이스가 깨지지 않는 상태로 리펙토링하자
      • 예를 들어, match() 라는 메소드에 인자가 추가되면 프로덕션 코드에 match2() 라는 메소드를 임시로 구현한다
      • 테스트 코드에 match() 메소드였던 부분을 match2() 로 바꿔본다
      • 테스트 코드가 통과되면 프로덕션 코드를 바꾼다
    • 컴파일 에러 발생을 최소화하면서 리펙토링하자
  • 보통 리펙토링하다가 다른 급한 업무로 가는 일이 발생한다. 그러면 리펙토링을 포기하거나 다시 원 상태로 되돌리거나 하게 된다. 그래서 규모가 큰 리펙토링을 잘 안하게 된다. -> 그러지 말자
  • 메소드에서 이상적인 인자 개수는 0개이다. 적을수록 좋다. 4개 이상은 사용하면 안된다.
    • 클래스로 묶어서 인자 개수를 줄일 수 있다. 당첨 번호같은 경우, 당첨번호와 보너스번호까지 같이 생성돼서 돌아다니는 것이 좋으니 묶을 수 있다.