본문 바로가기

etc/우테코 6기 프리코스

4주차 프리코스 미션(크리스마스 이벤트)

 

기능 요구 사항 & 프로그램 요구 사항


https://github.com/JunTaeINC/java-chrismas-6-JunTaeINC

 

GitHub - JunTaeINC/java-chrismas-6-JunTaeINC

Contribute to JunTaeINC/java-chrismas-6-JunTaeINC development by creating an account on GitHub.

github.com

 

 

 

미션 진행에 학습한점 


이번주 미션진행의 목표는 인터페이스 활용, stream의 학습 및 적용, getter의 최소화, 원시 값의 미사용, 클래스 분리, 그리고 작은 단위 테스트의 구현이다.

 

1. interface의 적극활용

이번 미션에서는 언급은 안되었지만 interface사용을 요구하는것 같았다.

나는 금액, 할인정책, 할인 유&무, 메뉴의 인터페이스를 만들어서 활용했다.

 

1.1 DiscountTable

기능요구사항에서 총 주문금액이 일정 수준 미만일 경우에는 할인 이벤트가 적용되지 않게 구현을 해야 했다. 
하지만 난 이미 기능을 구현을 다 해놓고 나중에 리펙토링 과정에서 하나씩 수정하는 게 편해 그렇게 진행을 하였다. 
우선 초기에는 NoDiscountDiscount(인터페이스)가 없이 Discount만 있었다. 어떻게 구현을 해야 할지 고민을 하다가 null로 처리를 하는 것과 예외를 던지는 방법, 인터페이스를 구현하는 방법이 있어서 요구사항에 맞는 인터페이스를 도입했다.

package christmas.domain.event.discount;

public interface Discountable {

   int getAmount();
}

 

package christmas.domain.event.discount;

import christmas.domain.amount.DiscountAmount;

public class Discount implements Discountable {

   private final DiscountAmount discountAmount;

   public Discount(int discountAmount) {
      this.discountAmount = new DiscountAmount(discountAmount);
   }

   @Override
   public int getAmount() {
      return discountAmount.getAmount();
   }
}

 

package christmas.domain.event.discount;

public class NoDiscount implements Discountable {

   @Override
   public int getAmount() {
      return 0;
   }
}

 

package christmas.domain.event.discount;

import static christmas.config.message.ResultMessage.BENEFIT_FORMAT;
import static christmas.config.message.ResultMessage.NEW_LINE;
import static christmas.config.message.ResultMessage.NONE;
import static christmas.domain.constant.DiscountConstant.MINIMUM_ORDER_AMOUNT;
import static christmas.domain.constant.DiscountConstant.ZERO;

import christmas.domain.VisitDate;
import christmas.domain.order.Order;
import christmas.util.NumberFormatter;
import java.util.stream.Collectors;

public class DiscountService {

   private final DiscountConfig discountConfig;

   public DiscountService() {
      this.discountConfig = new DiscountConfig();
   }

   public Discountable getTotalDiscountAmount(Order order, VisitDate visitDate) {
      if (order.getTotalOrderAmount() < MINIMUM_ORDER_AMOUNT) {
         return new NoDiscount();
      }

      int totalDiscountAmount = discountConfig.getDiscountPolicies().stream()
         .filter(policy -> policy.isApplicable(order, visitDate))
         .mapToInt(policy -> policy.getDiscountAmount(order, visitDate))
         .sum();

      return new Discount(totalDiscountAmount);
   }
}

 

위와 같이 만약 기준금액 미만일 경우 NoDiscount객체를 생성하고, 할인이 적용이 가능하면 Discount객체를 생성하도록 구현했다.

 

 

1.2 DiscountPolicy

할인정책으로는 평일할인, 주말할인, 크리스마스 디데이 할인, 특별할인이 있었다.

처음에는 하나의 집합으로 이 할인들을 묶어서 관리해줄 방법을 찾다가 인터페이스를 활용해서 간단하게 구현을 하게 됐다.

 

package christmas.domain.event.discount.list;

import christmas.domain.VisitDate;
import christmas.domain.order.Order;

public interface DiscountPolicy {

   boolean isApplicable(Order order, VisitDate visitDate);

   int getDiscountAmount(Order order, VisitDate visitDate);

   String getEventName();
}

각 할인들은 할인이 적용가능한지 boolean타입을 반환하는 메서드와, 할인 금액을 반환하는 메서드, 이벤트의 이름을 반환하는 코드를 구성했다.

 

<혜택 내역>
크리스마스 디데이 할인: -1,200원
평일 할인: -4,046원
특별 할인: -1,000원
증정 이벤트: -25,000원

미션 요구사항에 이렇게 출력을 요구하여 설정했다.

만약 추후에 할인 정책이 추가될경우 DiscountConfig에 추가하고 DiscountPolicy를 구현하는 구현체를 만들어주면 된다.

package christmas.domain.event.discount;

import christmas.domain.event.discount.list.ChristmasDdayDiscount;
import christmas.domain.event.discount.list.DiscountPolicy;
import christmas.domain.event.discount.list.SpecialDiscount;
import christmas.domain.event.discount.list.WeekdayDiscount;
import christmas.domain.event.discount.list.WeekendDiscount;
import java.util.Arrays;
import java.util.List;

public class DiscountConfig {

   private final List<DiscountPolicy> discountPolicies;

   public DiscountConfig() {
      DiscountPolicy christmasDdayDiscount = new ChristmasDdayDiscount();
      DiscountPolicy specialDiscount = new SpecialDiscount();
      DiscountPolicy weekdayDiscount = new WeekdayDiscount();
      DiscountPolicy weekendDiscount = new WeekendDiscount();

      discountPolicies = Arrays.asList(christmasDdayDiscount, specialDiscount, weekdayDiscount, weekendDiscount);
   }

   public List<DiscountPolicy> getDiscountPolicies() {
      return discountPolicies;
   }
}

 

package christmas.domain.event.discount;

import static christmas.config.message.ResultMessage.BENEFIT_FORMAT;
import static christmas.config.message.ResultMessage.NEW_LINE;
import static christmas.config.message.ResultMessage.NONE;
import static christmas.domain.constant.DiscountConstant.MINIMUM_ORDER_AMOUNT;
import static christmas.domain.constant.DiscountConstant.ZERO;

import christmas.domain.VisitDate;
import christmas.domain.order.Order;
import christmas.util.NumberFormatter;
import java.util.stream.Collectors;

public class DiscountService {

   private final DiscountConfig discountConfig;

   public DiscountService() {
      this.discountConfig = new DiscountConfig();
   }

   public String getDiscountDetail(Order order, VisitDate visitDate) {
      if (getTotalDiscountAmount(order, visitDate).getAmount() == ZERO) {
         return NONE.getMessage();
      }

      return discountConfig.getDiscountPolicies().stream()
         .filter(policy -> policy.isApplicable(order, visitDate))
         .map(policy -> String.format(BENEFIT_FORMAT.getMessage(), policy.getEventName(), NumberFormatter.getNumberFormat(policy.getDiscountAmount(order, visitDate))))
         .collect(Collectors.joining(NEW_LINE.getMessage()));
   }
}

 

이를 통해서 할인정보를 가져와서 각 할인이 적용이 되는지 검증을 하고 각 할인내역을 정해진 문자열로 출력을 하도록 구현했다. 
이렇게 인터페이스를 활용을 하여 하나의 인터페이스로 여러 개의 클래스를 관리하는 데에 편리함을 느꼈다. 

하지만 문제가 있다. 지금 isApplicable(Order order, VisitDate visitDate), getDiscountAmount(Order order, VisitDate visitDate)의 매개변수를 보면 어느 클래스에서는 사용하지 않는 매개변수가 있다. 
지금 isApplicable에서는 평일, 주말 할인은 Order, VisitDate를 모두 사용하지만 / 특별, 크리스마스 디데이 할인은 VisitDate만 사용한다. 

추후 리펙토링 과정에서 해결할 수 있는 방법을 찾아봐야겠다. 간단하게 찾아본 바로는 하나의 인터페이스를 더 구현해서 여러 개를 상속을 받는 방법도 있는데, 이게 최선의 방법일지 모르겠다. 내가 원하는 건 할인정책을 통해 각각의 할인 정보를 관리하는 것인데 추후 리펙토링 과정에서 수정을 해야겠다.

 

 

 

2. 설계

이번 미션에는 한눈에 확인이 가능한 설계도면?을 작성하고 진행을 했다. 
코드리뷰를 통해서 몇몇 사람들이 이렇게 눈으로 보기 좋게 설계를 하니 가독성도 좋고 어느 정도 실행흐름을 가늠할 수 있었다.

 

2.1 실행흐름

1. 방문날짜를 입력을 받고 VisitDate 객체를 생성한다. 객체를 만드는 과정에서 검증을 해서 추후 재입력기능에 활용할 예정이다.

2. 주문을 입력받는다. Order 객체를 생성한다. 이 또한 객체를 만드는 과정에서 검증을 한다.

3. 입력이 끝나면 이제 주문과 방문날짜를 통해 각종 이벤트를 통해서 객체를 만든다. (평일할인, 주말할인, 증정품, 뱃지 등등)

4. Ouput에서 출력에서 출력을 할 경우 객체에게 메시지를 요청하면 그에 원하는 답을 주도록 구현을 한다.

 

 

2.2 설계

솔직히 말하면 이렇게 설계를 하면서 작업을 해본 적이 없어서 익숙하지 않을뿐더러, 첫술에 배부를 수 없듯이 많은 수정이 필요했다. 
아마도 대충 큰 흐름은 이런 식으로 가지고 가고 싶었던 모양이다.

1차 도메인 설계

 

처음보다 뭐가 많이 생겼다. 이전에는 int, String 값 즉 원시값을 이용을 했었는데, 이번 미션에서는 원시값 대신 각 해당 객체를 만들어서 유지보수, 재사용성을 기대하고 구성을 해봤다. 처음에 와 다르게 점점 늘어가는 클래스를 보며 이렇게 하는 게 맞나? 너무 오버하는 거 같은데라고 생각이 들었다. 하지만 처음 내가 생각했던 거에 조금 살이 더 붙여져서 원하는 데로 잘 짜진 듯하다. 아마도..

2차 도메인 설계

 

 

인텔리제이에서 제공하는 다이어그램으로 클래스와 메서드를 확인했는데 간단한 크리스마스 프로모션 프로젝트?에 너무 과한가? 라는 생각이 문득 든다. 근데 이렇게 보니 좀 있어보이긴 한다

인텔리제이 다이어그램

 

3. stream() 을 활용

이번 미션에서는 시간적 여유가 이전 미션에 비해 많아서 몇 가지 목표를 가지고 있었다. 인터페이스 구현, getter 최대한 사용 안 하기, stream 학습 및 사용, 작은 단위 테스트 이렇게 목표를 설정해 두고 이번 미션을 진행했다. stream을 사용하면 코드가 간결해지고 라인수를 많이 줄여서 마냥 좋은 줄 알았지만, 때때로 성능적으로 반복문에 비해서 성능이 더 안 좋을 수도 있다. 간단한 연산에는 반복문을 활용하는 것이 오히려 더 성능이 좋다고 한다.

 

 

미션 진행 후기


이번 미션에서 중요한 개발 원칙과 기법에 대해 배우고 사용해 볼 수 있는 시간이었다. 이번주 미션의 목표는 인터페이스 활용, 스트림의 학습 및 적용, getter의 최소화, 원시 값의 미사용, 클래스 분리, 그리고 작은 단위 테스트 작성이다.

 

코드 리뷰를 통해서 여러 사람들의 코드를 보면서 설계도처럼 이쁘게 잘 정리를 해서 보기에도 좋았고 이해도 빠르게 됐다. 그래서 나도 이번 미션을 통해서 적용해 보기로 했다. 하지만 처음에 생각했던 것과는 달리 생각보다 더 많이 채워지면서 처음과 많이 뚱뚱해졌다. 아직 이렇게 설계를 하고 구현을 하는것이 익숙치 않아서 그런듯 하다. 다음에 기회가 있으면 설계에 시간을 더 투자해서 조금더 완성도 있게 구현하고 싶다.

 

사실, 이전에 인터페이스의 사용 시점이나 활용 방법에 대해 정확히 이해하지 못하고 있었다. 다른 사람들의 코드에서 인터페이스가 활용된 부분을 봐도, '왜 인터페이스를 사용하지? 그냥 해도 구현이 되는데?', '정말 필요한가?', '너무 투머치한 거 같은데'라는 의문이 들었다. 그래서 이번에도 인터페이스를 사용하지 않고 구현을 시작을 했다. 하지만 연관이 있는 클래스들이 눈에 보이며 각각의 클래스에 공통된 메서드가 필요하고 그들을 관리할 수 있는 관리자? 역할이 필요해서 ‘한번 적용해 볼까?’라는 생각으로 적용해 봤다.

인터페이스를 적용하니 클래스들이 같은 인터페이스를 구현함으로써 다형성을 활용할 수 있게 되고, 클래스의 교체, 혹시 모를 추가기능이 있을 확장성까지 상승되는 장점들이 있다는 걸 알게 되었다. 이번 미션을 통해, 인터페이스의 활용과 적용을 학습할 수 있는 시간이었다.

getter를 최소화하는 방법을 통해 객체의 불변성을 유지하고 원하는 값이 있을 경우 객체에 메시지를 보내는 듯한 메서드를 구현해 원하는 값을 반환함으로써 코드의 안전성을 높이는 방법을 학습하고, 이전 코드리뷰에서 원시값을 사용을 최소화하는 게 좋다는 의견을 통해 원시 값을 직접 사용하지 않고, 객체를 만듦으로써 코드의 명확성과 안정성을 높이는 방법을 적용해 보고 학습할 수 있었다.

 

4주 동안 프리코스를 통해서 기존에 자바는 어느 정도 알고 있다고 생각을 했지만 아직도 학습을 해야 할 부분이 많다는 것을 알게 되었다. 프리코스 진행 전에는 ‘왜 이걸 사용을 해야 하지?’처럼 이해를 못 하고 구현을 하게 된 경우가 있었는데 이번 기회로 왜 사용을 해야 하는지 알게 되고 기능을 사용함으로써 얻게 되는 장점을 명확히 알게 되는 시간이었다. 결말도 좋았으면 좋겠지만 역시 짧은기간에 많이 배우고 가는것 같다. 4주간 프리코스 참여하신 분들, 우테코 관계자분들 고생하셨습니다.