우아한 테크코스 3~5주차 로또 리뷰
로또 미션을 진행하면서 다시 페어가 바뀌었다. 이번에는 10분씩 번갈아가면서 하는 것이 아니라 메소드 하나씩 잡고 번갈아가면서 코딩하는 방식으로 진행했다. 초반에는 쉬운 기능목록만 해서 괜찮았는데 마지막 쯤 로또의 결과를 구하는 부분에 와서는 수익률 계산과 Enum, 일급컬렉션을 어느 객체에 구현할 지와 도메인 설계가 부족했던 탓인지 멘붕이 왔다.
이게 한 번 멘붕에 빠지니까 코딩을 제대로 할 수가 없었다. TDD 또한 메소드부터 작성하려니 마음처럼 잘 안됐다. 마감 시간 전까지도 페어를 하며 구현하는 것이 힘들어서 서로 동의 하에 페어를 깨고 각자 진행하는 것으로 했다. 늦지 않게 제출하긴 했지만 마감 시간이 조금 더 길었다면 마음 편하게 페어를 했을텐데 하는 아쉬움이 남았다.
TDD에 대한 두려움을 떨쳐내자. 일단 해당 기능의 메소드부터 작성하고 구현해보자. 자신감을 갖자
우테코의 말말말
페어를 하면서
- 좋았던 점
- 적극적으로 피드백을 줬을 때
- 자신을 솔직하게 드러냈을 때
- 잘 모르는 부분은 내가 잘 이해할 수 있을 때까지 알려주고 기다려줬을 때
- 아쉬웠던 점
- 적극적이면 좋겠다
- 자신감을 갖고 개발하면 좋겠다
- 자신의 생각과 다른 것도 수용하자
- 코딩할 때 급하게 하지 말자
- 의견을 더 구체적으로 말하자
포비의 말말말
- 개발자로 자수성가하려면 스타트업에 가라
- 집에 가서 공부할 수 있는 환경을 만들어라
- 원래 페어로 할 때 힘들다. 마감기한이 있으니 코드의 퀄리티를 낮추더라도 일단 미션을 제출해라
- 서로 간의 감정상태를 솔직하게 공유하자
- 기록은 짧아도 상관없으니 무엇이든 꾸준하게 기록해라
- 책에 있는 내용을 모두 정리하려고 하는 생각은 버리고 문제를 어떻게 해결했는지 어떤 감정을 가졌는지, 자기 생각을 기록하자
내가 받았던 피드백
- 가독성 향상을 위해 숫자의 경우 천 단위마다 언더스코어(
_
)를 붙이자 - 클래스명, 메소드명, 변수명, 테스트명 등 여러 부분에서 네이밍에 신경쓰자
- 사람이 보기 쉽게 코드를 짜자
- 인자로 아무 것도 주지 않는 것보다 인자를 줌으로써 더 이해가 잘 간다면 인자를 넣도록 하자
- 선언할 때는 좀 더 넓은 범위인 인터페이스로 선언해서 호출하거나 확장하는데 편한 코드를 작성하자
- 클래스에 검증로직이나 비즈니스 로직이 없다고 해서 잘못된 건 아니다
- 객체의 역할에 따라 비즈니스 로직을 설계하자
- 모든 원시값과 문자열을 포장하자- 어떤 클래스로 포장했다면 그 클래스로 리턴하는 것도 생각해보자
- 포장한 클래스로 리턴한다면 메서드 내부를 보지 않고 반환 값만 보고 더 쉽게 판단할 수 있다.
- 또한 컬렉션의 경우
put
과 같이 상태를 변경하는 메서드를 노출시키지 않도록 한다 - 객체의 역할 분배를 하기 위한 이유도 있다
int
를String
으로 바꾸고 싶다면+ ""
가 아닌String.valueOf()
를 쓰자- 상수화에도 무작정
private
을 쓰는 게 아니라 여러 곳에서 사용된다면public
으로 써도 된다. 다만 어느 객체에서 사용할 것인지는 잘 생각해봐야 한다.- 예를 들어, 이번 로또 같은 경우
MIN_RANGE
,MAX_RANGE
의 책임은LottoNumber
가 가져가야 한다
- 예를 들어, 이번 로또 같은 경우
- 생성자가 2개 이상 늘어나면 정적 팩토리 메서드를 생각하라
- 정적 팩토리라면 생성자를 private으로 만들어라
- 생성자는 하나만 있게 하고, 정적 팩토리 메서드를 2개 만들어서 처리를 해주자
- 정적 팩토리 메서드를 사용하면 미리 저장해둔 값과 비교하는 테스트코드도 짜야한다.
assertThat(LottoNumber.from(1) == LottoNumber.from(1)).isTrue();
- 검증코드가 생성자에 있건, 정적 팩토리 메서드에 있건 상관없다
- 테스트 메서드 네이밍 컨벤션을 적용해서
테스트하려는 메서드_테스트하려는 상태_기대하는 동작
의 형식으로 쓰자
다른 크루들의 피드백
- 메소드 명으로 무엇보다 크고 작음을 나타낼 때
LessThan, LessThanEqual, GreaterThan, GreaterThanEqual
으로 짓는 것이 관례다 - 테스트코드의 변수명을 실제 값은 actual, 기대한 값은 expected 로 짓는 것이 관례다
- DTO와 generator를 도메인으로 보지 않는다
- Money를 값객체로 활용한다면 동등성을 보장하기 위해 equals, hashcode도 구현하는 게 좋다
- 정상적인 경우도 테스트하자
- 클래스의 구현 순서(보통
public
이 먼저오고 그 다음에private
이 온다)class A { // 클래스 변수 // 인스턴스 변수 // 생성자 // 정적 팩토리 메소드 // 메소드(접근별로 하는 게 아닌 기능별 순서로) // 기본 메소드 (equals, hashCode, toString) // getters and setters }
- 숫자를 비교할 때
Integer.compare()
를 이용하자 - 기능이 동일하다면 컬렉션에 메서드로 존재하는
contains
를 메서드 명 그대로 짓는 게 좋다 - 탭 혹은 스페이스 바로 일관되게 공백을 쓰는 게 좋다. 들여쓰기를 하나로 통일하자
Collectors.joining()
에 delimiter 뿐만 아니라 prefix, suffix 를 정해줄 수 있다List<LottoRank> lottoRanks
가 선언되어 있을 때lottoRanks.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
를 통해Map<LottoRank, Long>
형태로 반환해줄 수 있다- HashMap에서
getOrDefault
,putIfAbsent
를 활용하자 - 항상 null safe를 생각하자.
- null이 반환될 수 있는 곳에는
Optional
을 생각하자 - 생성자가 정상적으로 생성됐다는 것을 테스트 해줄 때는
assertThat(actual).isNotNull();
로 해주자 - 에러에 해당하는 값(ERROR)이 Enum 안에 포함되는 것은 좋은 방법이 아니다.
orElseThrow()
를 활용하자. 다만 아무것도 당첨되지 않았을 때 아무것도 없음(NONE)에 해당되는 값이 Enum 안에 포함되는 건 현 미션에 경우에는 괜찮다. - enum 필드에는 final을 붙이는 것이 관례다
- 생성자에서
List<Lotto> lottos
를 받아줄 때this.lottos = new ArrayList(lottos);
를 통해서 null safe한 로직을 짜줄 수 있다 TICKET_PRICE = 1000;
와 같은 상수명도 티켓 하나의 값이라는 의미이므로 LottoTIcket 클래스에public
으로PRICE = 1000;
이라고 선언해서LottoTicket.PRICE
을 통해서 갖고 오도록 하자
배운 내용
로또 미션의 도메인 설계
- 처음에 가장 하위노드인 LottoNumber를 도출하는 것이 어렵지만 도출할 수 있다면 TDD로 개발하기 쉽다.
- 상태를 가지는 도메인 객체를 설계하는데 집중해라. 이것만 가지고 조립을 하면 된다.
- Money 객체로 원시값으로 포장하면 돈과 관련된 모든 로직을 Money에 넣어줄 수 있다
- 클래스 다이어그램
- 처음부터 메서드를 도출하기는 쉽지 않다.
- 클래스와 상태를 도출하도록 노력하자.
- 의존관계를 어떻게 맺는지 이해하자
- LottoTicket은 LottoNumber에 의존관계를 가진다.
- 1 : N이라면 Collection이 들어가야된다는 걸 알 수 있다.
- 메서드 내부에서 의존관계를 갖는 경우, 점선으로 표기한다.
- 실선은 강하게 의존관계를 갖는 경우다. (상태값에 다른 클래스 이름이 들어있으면)
- 클래스 다이어그램을 짜고 나서 어디서부터 TDD로 할 것인지 생각하자
- LottoTicket을 생성한다면 LottoTicketFactory라고 명하자. 뭐 그 안에 create, getInstance, of 등등…
- 설계는 점진적으로 업그레이드 해나가자
점진적인 리펙토링
- 기능 목록을 작성할 때 간단하게 해도 괜찮다
- 로또 전체를 구현하려는 욕심을 버려라
- 다 하는 것이 아닌 중간 단계부터 테스트를 시작하려는 생각을 해야된다.
- 어떤 input이 있을 때 어떤 output이 나오고 등등…
- 일단 구현하자
- 일단은 동작할 수 있는 것에 집중
- getter고 뭐고 일단 쓰자.
- 리펙토링을 생각해보자
- 작성한 메서드가 어느 객체에 맞는 건지 끊임없이 고민하자
- 값이 유일한지 생성자에서 검증
- 원시값 및 문자열 포장
- 일급컬렉션
- 정적 팩토리 메서드
- 캐싱
- 점진적인 리펙토링
- 메소드에 인자가 추가되면?
- 메소드를 사용하는 모든 곳에서 컴파일 에러가 발생함
- 해결해도 다른 곳에서 테스트케이스 컴파일 에러가 발생
- 다시 디버깅
- 즉, 점진적인 리펙토링을 하자
- 자료구조를 어떤 것을 쓸 것인지에 대한 고민도 필요하다
- 상속보다 조합(ArrayList를 상속할 경우, 많은 퍼블릭 메소드를 가지기 때문에 메소드 호출에서 실수할 수 있다. 조합으로 하면 필요한 메소드만 있게 된다)
- 점진적인 리펙토링을 하자
- 기존의 테스트 케이스가 깨지지 않는 상태로 리펙토링하자
- 예를 들어,
match()
라는 메소드에 인자가 추가되면 프로덕션 코드에match2()
라는 메소드를 임시로 구현한다 - 테스트 코드에
match()
메소드였던 부분을match2()
로 바꿔본다 - 테스트 코드가 통과되면 프로덕션 코드를 바꾼다
- 예를 들어,
- 컴파일 에러 발생을 최소화하면서 리펙토링하자
- 기존의 테스트 케이스가 깨지지 않는 상태로 리펙토링하자
- 보통 리펙토링하다가 다른 급한 업무로 가는 일이 발생한다. 그러면 리펙토링을 포기하거나 다시 원 상태로 되돌리거나 하게 된다. 그래서 규모가 큰 리펙토링을 잘 안하게 된다. -> 그러지 말자
- 메소드에서 이상적인 인자 개수는 0개이다. 적을수록 좋다. 4개 이상은 사용하면 안된다.
- 클래스로 묶어서 인자 개수를 줄일 수 있다. 당첨 번호같은 경우, 당첨번호와 보너스번호까지 같이 생성돼서 돌아다니는 것이 좋으니 묶을 수 있다.