본문 바로가기
가치관 쌓기/개발 돌아보기

[우아한테크코스Pro]로또 구현(테스트 주도 개발)[1/9]

by simplify-len 2021. 5. 28.

들어가기

 이번 한 주동안 우아한테크코스Pro 에서는 테스트 주도 개발로 로또를 구현하는 미션을 받았습니다. 처음에는 간단할 줄 알았던 미션도 하다보니, 생각해야 될 부분- 고민해야될 부분- 생각보다 많았습니다.

 

 특히, 코드를 작성하면서 지켜야 할 부분으로 다음과 같습니다.

 

경험할 객체지향 생활 체조 원칙

  • 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  • 규칙 2: else 예약어를 쓰지 않는다.
  • 규칙 3: 모든 원시값과 문자열을 포장한다.
  • 규칙 5: 줄여쓰지 않는다(축약 금지).
  • 규칙 8: 일급 콜렉션을 쓴다.

위 규칙은 읽을 때는 간단해 보일지라도, 코드를 작성하는 과정에서 2개의 선과악이 공존하게 만드는 규칙들이였습니다.

고민을 했던 부분

1.  이름 짓기


클래스 이름을 포함해서 패키지를 설계하는 것 또한 하나의 중요한 부분이다. 개발할 때 패키지를 Lottery 라고 명명했더니, Lottery 패키지 안쪽의 모든 클래스는 암묵적으로 이름 앞에 prefix로 붙을 것이라는 착각에 빠졌었다. 그 결과 Numbers 라는 클래스가 탄생해버렸다. 즉, 의도가 노출되지 않은 클래스를 만들어 버린것이다. 

 

아무리 패키지명이 명확하다고 해도, 클래스 이름은 또다른 의도를 표현해야 한다. 다른 개발자로 하여금 헷갈리게 할 수 있는 요소이다.

 

그 외에도 한국말로는 로또 이지만, 영어로 표현하려고 보니, Lotto, Lottery, LotteryNo, NotterNumber 비슷하지만 일관성이 깨질 수 있는 부분이였다.

 예를 들어 일급 콜렉션으로 LotteryNo 가 콜렉션을 이룬 클래스의 이름을 NotterNumber 으로 한다면? 보는 사람으로 하여금 헷갈릴 수 있을 것이다.

 

2. 설계를 어떻게 가져갈 것인가?

  이 부분에 대해서 처음에 나는 도메인만 명확하게 가져가면 설계를 고민할 필요가 있을까? 싶었다. 그러나, 역시 우리의 코드는 그렇게 호락호락하지 않았던 것같다.

 흔히, Spring을 개발하면서 XXSerivce 라는 클래스를 만든다. 이는 도메인주도설계에서 영향을 받았다고 할 수 있다. 각 역할을 분리해 해 응집도를 높이는데, 목적이 있다.

 

DDD Architecture

 위 장표를 보면, Application Layer와 Domain Layer 를 분리시켜놨다. 왜 분리시켰을까? 단순하다. Entity 라 불리는 객체는 자신이 해야될 역할과 책임 그리고 협력을 명확히 드러내기 위해서다.

 그럼 나의 코드 설계에서는 어떻게 했는가? Application Layer 또는 상태가 없는 도메인 서비스와 같은 것이 없다.

 

그냥 (난장판 파티)의 객체들끼리 지지고 볶는다...

코드 일부분

 적어놓고 보니, 아쉬웠던 부분에 포함되어야 했던 부분이 아니였을까 싶다. 그래도 분명 맨 처음 설계 했을 때 고민했던 내용이기 때문에 포함한다.

 

3. 객체가 지녀야할 책임, 역할, 협력은 어디까지 가져가야 할 것인가?

 바로 위 2번의 문제 때문인지, 아니면 요구사항을 스스로 명확하게 정의하지 못한 이유에서 비롯된 문제인지, 알 수 없지만- 이 문제는 로또 구현 뿐만 아니라, 다른 코드에서도 늘 고민하는 부분이였던 것 같다.

 

 테스트 코드를 먼저 작성하고, 이를 Pass 하는 과정에서 객체가 지녀야할 책임에 대해서 이 객체한테 줄까? 저 객체한테 줄까? 라는 고민을 많이 했다.

 

 간단하게 예를 들어보자. 정답을 확인해주는 객체 InfoStore 안쪽에 로또 티켓의 당첨을 확인할 수있는 메소드가 있다. 이 메소드는 당첨을 확인하는 것이 주요 역할이다. 그런데, 코드를 구현하는 과정에서 당첨을 확인하기 위해서 로또 번호가 몃개나 일치하는 지에 대한 비지니스 로직도 포함되었다. 

 

 이게 맞는가?

로또 번호가 몃개나 일치하는지에 대한 비지니스 로직은 InfoStore 의 역할이 아니다. 이는 LottoTicket 이 가져야할 역할이다. 그러므로, 로또 번호가 몃개나 일치하는지에 대한 비지니스 로직은 LottoTicket이 가져야 한다. 이렇듯, 무심히 코드를 작성하다보면 나도 모르게 하나의 객체에 많은 역할을 부여하고 있는 내모습을 발견하게 된다. 코드를 리팩토링 하는 과정에서 이를 분명하게 하기 위한 노력을 하는데, 이때 고민을 많이 하게 되는 것 같다.

 

아쉬웠던 부분

1. Stream API 의 사용

로또의 당첨확인 유무를 판단하는 코드에서 Stream API 사용에 아쉬운 부분이 남았다.

 

맨 처음 작성했던 코드

	public Result confirmTicket(Tickets buyerTickets) {
		validateConfirm();
		Result result = new Result();
		Ticket lastWeekTicket = this.lastWeekWinningTicket;

		buyerTickets.getValues().stream()
			.map(ticket -> {
				int matchCount = ticket.matchCount(lastWeekTicket);
				if (ticket.numbers().contains(bonus)
					&& matchCount == LotteryMatchType.FIVE_MATCH.matchCount()) {
					return LotteryMatchType.FIVE_WITH_BONUS_MATCH.matchCount();
				}
				return matchCount;
			}).filter(matchCount -> matchCount >= MATCH_THREE_NUMBER)
			.map(LotteryMatchType::fromInteger)
			.forEach(match -> result.getResultMap().addMatchType(match));
		return result;
	}

해당 메소드에 결과로, 당첨된 갯수에 따른 횟수가 담긴 Map을 반환하는 것이 목적이였다. 그러서 3번 라인에 new Result() 을 하고, 17번 라인의 result.getResultMap().addMatchType(match)); 에서 map을 통해 가공된 데이터를 add 한다.

 

사실 동작되는데는 문제가 없다. 그럼 어디에 문제가 있냐고? Stream API를 사용하는 목적은 데이터의 흐름 자체를 가공하는 것에 목적을 둔다. 그러나 마지막 forEach 를 사용하게 되면서, Stream API 를 단순히 하나의 도구로 사용된 것이 지나지 않게 되었다. 반환되는 값까지 map으로 반환되었다면 더욱 불변을 보장할 수 있는 우아한 코드가 되지 않았을까? 

코드리뷰 피드백

변경된 코드는 아래와 같다.

	public Result confirmTicket(Tickets buyerTickets) {
		return new Result(LotteryMatchTypeMap.of(buyerTickets.getValues().stream()
			.map(ticket -> lastWeekWinningTicket.getMatchTypeWith(ticket))
			.filter(matchType -> LotteryMatchType.MISS_MATCH != matchType)
			.collect(groupingBy(a -> a, summingInt(a -> 1)))));
	}

2. 불변성을 지키는 코딩

코드를 작성하는 과정에서 가장 많이 받은 피드백이였다. 나 스스로도 불변성을 지켜야 한다. 지켜야 한다라고 열심히 떠들었는데, 막상 내 코드에서는 그렇지 않았던 것 같다. 객체가 지닌 변수들에게 불변성을 지킬수 있게 해주자..!!

코드리뷰 피드백 중...

 외부에 노출된 객체의 속성에게 변경될 수 있는 요인이 주어지면, 나도 모르는 사이드 이펙트가 발생할 확률이 높다. 이를 조심하자.

 

3. 미숙한 자료구조 사용

 왜 Collection은 Set, List, Map이 있을까? 시기적절하게 자료구조를 활용하여 언어 단에서 요구사항을 보장시키게 한다면- 개발자가 고민할 내용이 적어졌을 것이다.

 

각 자료구조의 특징을 잘 알고 있다고 생각하지만, 사용할 때는 나도 모르게 List와 Map 만 사용하는 내 자신을 발견했다.

코드리뷰 피드백 중...

 

 

참고자료

https://www.youtube.com/watch?v=YdtknE_yPk4

 

제출 코드 Pull Request

 

작업 브랜치

1. 문자열 덧셈 계산기

 

[Step2] 문자열 덧셈 계산기 by LenKIM · Pull Request #1578 · next-step/java-lotto

안녕하세요. 문자열 덧셈 계산기 코드 리뷰 부탁드립니다. AddCalculatorModel 가 주요 코드입니다. 어떠한 피드백도 감사합니다😃

github.com

2. 로또 (자동)

 

[Step3] 로또(자동) by LenKIM · Pull Request #1627 · next-step/java-lotto

리뷰어님! 코드 리뷰 부탁드립니다. 앞서 피드백 해주셨던 Stream API 을 적극 적용했습니다 : ) 하루 종일 로또에 대해서 생각하다보니... 갑자기 안사던 로또를 사고 싶어지는 밤이네요 😌 이전과

github.com

3. 로또 (2등)

 

[Step4] 로또(2등) by LenKIM · Pull Request #1676 · next-step/java-lotto

안녕하세요. 김규남 리뷰어님! 벌써 3번째이네요. 보내주신 피드백 덕분에 코드를 시야가 넓어졌습니다. 이번에는 커밋의 수는 2개뿐입니다. 조금더 나누서 커밋해야되는건 아니였을까 싶습니

github.com

4. 로또 (수동)

 

[Step5] 로또(수동) by LenKIM · Pull Request #1698 · next-step/java-lotto

안녕하세요. 리뷰어님, 5단계 로또(수동) PR입니다. 수동기능이 들어가면서 Ticket 이 가져야할 책임으로서 Type을 하나 추가했습니다. 더불어, 로또번호가 int 였던 부분을 LottoNo 으로 변경했습니다.

github.com

 

댓글