이어서...
오늘 4, 5일차를 한번에 쓰는 이유는, 계속된 고민과 고뇌와 수정으로 4일차에는 어떠한 한 기능을 완성했다- 고 말할 수 없었기 때문이다.
3주차 공통 피드백 중 객체를 객체스럽게 사용한다
라는 피드백에 정말 많은 찔림을 받았는데, 3주차 까지는 상태를 가지는 객체(Car
, Lotto
등)가 스스로 일하기보다는 그저 값을 저장하는 저장소의 느낌이 강했다. 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다
라는 피드백도 많이 찔렸는데, 도메인 객체가 하는 역할이 별로 없으니 컨트롤러와 서비스 클래스들이 많은 상태를 가지게 되었다.
그래서 객체가 객체스럽게 사용되기 위해 계속, 끊임없이 고민하고 수정을 거듭한 결과, 오늘 5일차에 한번에 기능들을 다 완성하게 되었다. 개선 vs 일단 돌아가게만 하자
를 적절히 맞추어 어느 정도 객체스럽게 구현하면서도, 여전히 클래스 분리 등의 리팩토링 할 것들은 남아있다.
커밋
나는 커밋 로그가 불필요하게 많아지는 걸 원하지 않는다. 그래서 정말 한 기능을 수행할 수 없다면 커밋하지 않는다.
그래서인지, 커밋은 어쩔 수 없이 몰아서 하게 된다. 커밋 로그를 보면, 독립적으로 구현이 가능한 기능들은 먼저 했지만, 이후 구현한 기능들은 대부분이 다른 기능들과 연계되어야만 동작할 수 있는 기능들이기 때문에 한 번에 몰아서 하게 됐다.
| ⬆️ 리팩토링 시작 전, 기능 구현 완료 후 커밋 로그
제대로 기능 작동을 하지 못해도 커밋해야 하는지, 최소한의 동작 완성을 끝내고 커밋해야 하는지, 어떤게 맞는지는 아직 모르겠다. 솔직히 맞는게 존재하지는 않는 것 같다. 이 부분은 취향 차이라고 생각한다.
흐름 관리를 위한 서비스 클래스의 증발
객체를 객체스럽게 사용하고, 데이터를 직접 다루기 보다는 관리하는 객체에 메시지를 보내는 것에 많은 신경을 썼더니, 이전 과제와는 다르게 자연스럽게 흐름을 관리하는 서비스 클래스가 사라졌다.
| ⬆️ 3주차 로또 과제에서 흐름을 관리했던 LottoService
클래스
3주차 까지의 과제는 컨트롤러든 서비스 클래스든 흐름을 관리하기 위한 클래스가 필수적이었는데, 도메인 객체 중심으로 구현을 하다 보니 그런 클래스가 자연스럽게 사라졌다. 또한 컨트롤러도 매우 간소화됐다.
컨트롤러의 간소화
3주차 로또 과제의 컨트롤러 클래스 코드는 아래와 같다.
public class LottoController{
private final LottoServices services;
private final LottoValidators validators;
private int purchaseAmount;
private int dividedPurchaseAmount;
private HashSet<PurchasedLotto> purchasedLotto;
private List<Integer> winningNumbers;
private int bonusNumber;
private LottoResultDto lottoResultDto;
public LottoController(LottoServices services, LottoValidators validators) {
this.services = services;
this.validators = validators;
}
public void run() {
setPurchaseAmount();
printPurchasedLottoAmount();
purchaseLotto();
setWinningNumbers();
setBonusNumber();
compare();
printLottoResult();
}
private void setPurchaseAmount() {
purchaseAmount = services.lottoSettingService.selectPurchaseAmount(validators.purchaseAmountValidator, InputView::getUserInput,
OutputView::printPurchaseAmountRequestMessage, OutputView::printErrorMessage);
}
private void printPurchasedLottoAmount() {
dividePurchaseAmount();
OutputView.printPurchasedAmountResultMessage(dividedPurchaseAmount);
}
private void purchaseLotto() {
purchasedLotto = services.lottoPurchaseService.purchase(dividedPurchaseAmount);
printPurchasedLotto(purchasedLotto);
}
private void printPurchasedLotto(HashSet<PurchasedLotto> purchasedLotto) {
for (PurchasedLotto lotto : purchasedLotto) {
OutputView.printPurchasedLotto(lotto.getNumbers());
}
}
private void setWinningNumbers() {
winningNumbers = services.lottoSettingService.selectWinningNumbers(validators.winningLottoValidator, InputView::getUserInput,
OutputView::printWinningNumberRequestMessage, OutputView::printErrorMessage);
}
private void setBonusNumber() {
BonusNumberValidator bonusNumberValidator = new BonusNumberValidator(winningNumbers);
bonusNumber = services.lottoSettingService.selectBonusNumber(bonusNumberValidator, InputView::getUserInput,
OutputView::printBonusNumberRequestMessage, OutputView::printErrorMessage);
}
private void compare() {
WinningLotto winningLotto = services.lottoService.createWinningLotto(winningNumbers, bonusNumber);
WinningLottoDto winningLottoDto = winningLotto.toDto();
services.lottoService.compareNumbers(winningLotto, purchasedLotto);
lottoResultDto = services.lottoResultService.generateLottoResult(purchasedLotto, winningLottoDto, purchaseAmount);
}
private void printLottoResult() {
OutputView.printWinningStatistics(lottoResultDto.fifthPrizeCount(), lottoResultDto.fourthPrizeCount(),
lottoResultDto.thirdPrizeCount(), lottoResultDto.secondPrizeCount(),
lottoResultDto.firstPrizeCount(), lottoResultDto.profitRate());
}
private void dividePurchaseAmount() {
dividedPurchaseAmount = purchaseAmount / PURCHASE_DIVISIBLE_AMOUNT.getValue();
}
}
엄청 뭐가 많지 않은가? 가독성도 떨어지고, 흐름 파악도 어렵다. 컨트롤러가 너무 많을 일을 하는 것처럼 보이기도 하다.
반면에 객체가 객체스럽게 일을 하게 만든 이번 과제에서는, 컨트롤러가 매우 간소화됐다.
package christmas.controller;
import christmas.domain.date.VisitDate;
import christmas.domain.discount.Discount;
import christmas.domain.order.Order;
import christmas.service.DateService;
import christmas.service.DiscountService;
import christmas.service.OrderService;
import christmas.view.InputView;
import christmas.view.OutputView;
public class EventPlannerController {
//TODO: 의존성 주입 구현해야함
OrderService orderService = new OrderService();
DateService dateService = new DateService();
public void run() {
OutputView.printWelcomeMessage();
VisitDate visitDate = dateService.getDateInput(InputView::getUserInput, OutputView::printDateInputRequestMessage,
OutputView::printErrorMessage);
Order order = orderService.start(InputView::getUserInput, OutputView::printMenuQuantityInputRequestMessage,
OutputView::printErrorMessage);
DiscountService discountService = new DiscountService(visitDate.toDto(), order.toDto());
Discount discount = discountService.createDiscount();
OutputView.printResult(discount.toDto(), visitDate.toDto(), order.toDto());
}
}
구현에 필요한 로직이 단 8줄로 줄었다. 설명은 더욱 더 간단하다.
애플리케이션 시작 메시지를 출력할 것을 View에 요청하고,VisitDate
, Order
, Discount
클래스를 생성할 것을 Service
에 요청하고,
View에 데이터를 넘겨줘 결과를 출력할 것을 요청한다.
이렇게 객체가 객체스럽게 일하게 하니 훨씬 예쁜 코드가 된 것 같다. 가독성 부분에서도, 객체지향적 관점에서도!
구현한 것들
이전 회고록과는 달리 구현한 모든 것들을 담을 수는 없다. 그러기엔 이틀만에 너무 많은 클래스들을 작성하고 지우고 구현했기 때문에...
이틀만에 무려 39개의 클래스를 작성했다. 이 코드들을 하나하나 다 설명할 순 없으니, 레포지토리를 참고해 주시길...
개선할 것들
결과 메시지 출력
결과 메시지를 출력할 때, enum을 사용할지, OutputView
에 상수로 두고 사용할지, 아니면 그냥 하드코딩된 값을 format()
을 사용해 출력할지를 고민중이다.
각각의 방식의 장단점을 한번 정리해봤다.
1. Enum으로 메시지 관리
장점:
- 쉬운 관리: 메시지를 수정하기 위해 이곳 저곳 찾을 필요가 없다.
- 재사용성과 일관성: 요구사항의 추가로 메시지를 재사용 해야할 때(예 : 원하는 할인 내역만 출력) 좋다.
단점:
- 유연성 부족: 동적으로 변경될 메시지들(금액)은 결국
OutputView
에서 포맷팅 해줘야 한다. - 복잡성 증가: 그냥 딱 보면 복잡하다.
2. 상수로 메시지 관리
장점:
- 가독성: 이 출력할 내용이 무슨 내용인지 알기 위해서 다른 클래스를 볼 필요가 없다.
단점:
- 재사용 어려움: 추후 요구사항의 추가로 다른 곳에서도 해당 메시지를 사용해야 한다면, 결국 enum으로 만들어야 한다.
- 관리의 어려움: 장점과 정반대되는 단점으로, 추후 애플리케이션이 커지면 이 메시지를 어디서 관리하는지 찾기 힘들다. 근데 요즘은 IDE의 찾기 기능 덕분에 딱히 의미 없는 단점인듯하다.
3. 하드코딩된 메시지 출력
장점:
직관적: 메시지가 바로 보여서 수정도 쉽고, OutputView
갔다가 enum 갔다가 할 필요가 없다. 편하다.
단점:
재사용 불가능: 만약 다른 곳에서도 재사용이 일어난다면 어차피 또 enum으로 옮겨야 한다.
그래서 일단 내가 내린 결정은 enum 사용이긴 한데... 영 수정하기가 불편하다. 결과를 출력하는 아래 printResult()
메서드를 보자.
public static void printResult(DiscountDto discountDto, VisitDateDto visitDateDto, OrderDto orderDto) {
if (discountDto.hasAnyDiscount()) {
System.out.println(RESULT_HEADER_MESSAGE.getFormatMessage(EVENT_MONTH.getValue(), visitDateDto.date())
+ RESULT_ORDERED_MENU_MESSAGE.getMessage());
printOrderedMenu(orderDto.splitMenuQuantity());
System.out.printf(RESULT_DISCOUNT_FIRST_PARAGRAPH.getParagraph(), orderDto.totalPurchaseAmount());
//아래는 if문은 아직 enum에 구현 못함
if (discountDto.christmasCountdownDiscount() != 0) {
System.out.printf("크리스마스 디데이 할인: -%s원\n", discountDto.christmasCountdownDiscount());
}
if (discountDto.weekdayDiscount() != 0) {
System.out.printf("평일 할인: -%s원\n", discountDto.weekdayDiscount());
}
if (discountDto.specialDiscount() != 0) {
System.out.printf("특별 할인: -%s원\n", discountDto.specialDiscount());
}
if (discountDto.giveawayDiscount() != 0) {
System.out.printf("증정 이벤트: -%s원\n", discountDto.giveawayDiscount());
}
System.out.printf(RESULT_DISCOUNT_SECOND_PARAGRAPH.getParagraph(),
discountDto.totalDiscount(), orderDto.totalPurchaseAmount() - discountDto.totalDiscount(), discountDto.badge());
return;
}
System.out.println(RESULT_HEADER_MESSAGE.getFormatMessage(EVENT_MONTH.getValue(), visitDateDto.date())
+ RESULT_ORDERED_MENU_MESSAGE.getMessage());
printOrderedMenu(orderDto.splitMenuQuantity());
System.out.printf(RESULT_NO_DISCOUNT_PARAGRAPH.getParagraph(), orderDto.totalPurchaseAmount(),
orderDto.totalPurchaseAmount() - discountDto.totalDiscount(), discountDto.badge());
}
enum 클래스에서도 출력될 것을 생각하며 문단을 만들고,OutputView
에서도 매개변수를 생각하며 포맷팅 해줘야 한다. 이 복잡한 코드를 리팩토링 하는 방안을 생각해 봐야 한다.
할인 전략 패턴
public Discount createDiscount() {
WeekdayStrategy weekdayStrategy = new WeekdayStrategy();
WeekendStrategy weekendStrategy = new WeekendStrategy();
ChristmasCountdownStrategy christmasCountdownStrategy = new ChristmasCountdownStrategy();
SpecialStrategy specialStrategy = new SpecialStrategy();
GiveawayStrategy giveawayStrategy = new GiveawayStrategy();
int weekdayDiscount = weekdayStrategy.accept(context);
int weekendDiscount = weekendStrategy.accept(context);
int christmasDiscount = christmasCountdownStrategy.accept(context);
int specialDiscount = specialStrategy.accept(context);
int giveawayDiscount = giveawayStrategy.accept(context);
return new Discount(christmasDiscount, weekdayDiscount, weekendDiscount, specialDiscount, giveawayDiscount);
}
주문 메뉴에 따라 해당하는 Menu
클래스를 찾는 전략 패턴은 잘 구현했는데, 해당하는 할인을 적용하는 전략 패턴은 조금 애매했다.
일단은 돌아가게만 만들려고 저렇게 구현했는데, 할인 전략들을 List
로 만들고, 그 List
를 순회하며 할인 종류와 금액을 HashMap
에 넣는 방법으로 한번 구현해봐야 겠다.
마무리
좋다! 나름 잘 구현했다고 생각한다.
당연히 개선할 것들은 남아있지만, 깔끔한 컨트롤러를 보니 뭔가 직관적으로 잘 구현했다는 생각이 든다.
객체를 객체스럽게 사용하는 것. 이 가르침은 정말 값진 것 같다. 어느새 객체지향적으로 사고하지 못하고 있는 나를 다시 객체지향적으로 사고하려고 노력하게 만들었으니..
일급 컬렉션도, 원시값 포장도, 효율적인 로직도 중요하지만,
살아 있는 객체, 그리고 그 객체들 사이의 역할, 책임, 협력의 과정이 객체지향의 가장 본질적인 것이라는 걸 다시 한번 더 느꼈다.
새롭게 알게된 점
- 객체를 객체스럽게 사용하라는 것의 의미
- 객체를 객체스럽게 사용하는 법 (그래도 아직 더 배워야 한다)
좋았던 점
- 상대적으로 빠른 시간 안에 구현을 완료한 것 (실력의 상승)
- 컨트롤러, 서비스 레이어의 다이어트
- 1번의 이유로 리팩토링, 회고, 리드미 작성 등에 더 많은 시간을 할애할 수 있게 된 것
아쉬웠던 점
- 3주차 과제까지 도메인 객체들에게 많은 일거리를 주지 않았던 것... 미안하다...
- 3주차 과제까지 컨트롤러와 서비스 레이어가 (상대적으로) 뚱뚱했던 것... 미안하다...
'우아한테크코스' 카테고리의 다른 글
[우테코 6기 프리코스] 회고 - 6기 프리코스를 마치며 (0) | 2023.12.20 |
---|---|
[우테코 6기 프리코스] 4주차 - 6, 7일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 4주차 - 3일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 4주차 - 2일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 4주차 - 1일차 회고 (0) | 2023.12.20 |