겸손하기 꾸준하기 건강하기

우테코 6기 프리코스(back-end) 4주차 회고 본문

woowacourse/백엔드 6기 프리코스

우테코 6기 프리코스(back-end) 4주차 회고

seminss 2023. 11. 21. 03:15

 

 
이번 4주 차 미션에서는 3주 차 미션의 학습 목표인 클래스(객체)를 분리하는 연습을 조금 더 살펴볼게요.
클래스(객체)를 분리하는 것에 대해 조금 더 깊이 고민해 볼 수 있도록 클래스 예시와 요구 사항을 추가했습니다. 특히, 클래스의 역할과 책임을 생각해 보고 클래스 작성 시 도메인 로직에 집중하는 방향으로 구현하시고, UI는 도메인 로직과 분리하는 방향으로 생각했으면 좋겠어요. 새로운 요구 사항이기보다는 지난주 요구 사항이 조금 더 구체적으로 명시되는 것이니 3주 차 미션을 성실히 수행했다면 잘하실 수 있을 거예요.
 
미션 - 크리스마스 프로모션


3주 차 공통 피드백

  1. 함수(메서드) 라인에 대한 기준을 세운다.
  2. 발생할 수 있는 예외 상황에 대해 고민한다.
  3. 비즈니스 로직과 UI 로직을 분리한다.
  4. 연관성이 있는 상수는 static final 대신 enum을 활용한다.
  5. final 키워드를 사용해 값의 변경을 막는다.
  6. 객체의 상태 접근을 제한한다.
  7. 객체는 객체스럽게 사용한다.
  8. 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다.
  9. 성공하는 케이스뿐만 아니라 예외에 대한 케이스도 테스트한다.
  10. 테스트 코드도 코드다.
  11. 테스트를 위한 코드는 구현 코드에서 분리되어야 한다.
  12. 단위 테스트하기 어려운 코드를 단위 테스트하기
  13. private 함수를 테스트하고 싶다면 클래스(객체) 분리를 고려한다.

 


 
4주 차도 사실 기록할 시간 없이 구현하기 급급했다.
미션을 제출하고 난 지금, 커밋 메시지를 통해 기억을 상기하며,, 회고를 작성해보려고 한다.☺️
(3주차 리뷰 전에 4주차 리뷰를 먼저 하는 이유는, 까먹기 전에 하나라도 더 자세하게 기록하고 싶어서 😶‍🌫️)

 

1일 차 (Nov10, 2023)

 
우선 이번 주차 미션을 읽어보면 알겠지만, 3주 차까지의 미션과 다르게 메일 형태로 요구사항이 제공되었었다.
그래서 초반에 기능 명세서를 작성하면서 요구사항을 완전히 파악하기 위해 시간을 많이 할애했다.
기능 명세서 (한 번 들어가서 읽어주세요 ㅎㅎ)
 
3주 차 미션에서 구조적으로 대차게 실패해버렸다는 것을 깨닫고, 4주 차에서는 3주 차의 문제를 해결하고자 고민을 많이 했다.
그러면서, view의 책임에 대해서도 고민을 많이 했다. 
 
로또 미션에서는 validator를 view단에 선언해 두었음에도, service단에서 호출하던 문제가 있었는데, 이번에는 다음과 같은 식으로 InputView를 구성해, 검증된 input을 controller가 받을 수 있도록 했다. (비즈니스 로직에 대한 validate는 model 내부에, view 단에서는 빈 문자열이나 잘못된 포맷만 걸러준다.)

    public static Integer readVisitDate() {
        String userInput = read();
        VisitDateValidator.validate(userInput);
        return VisitDateParser.parseInteger(userInput);
    }

    public static List<SimpleEntry<String, Integer>> readOrder() {
        String userInput = read();
        OrderMenuValidator.validate(userInput);
        return OrderParser.parseEachMenu(userInput);
    }

 
메뉴판(Menu)을 Enum으로 생성해 줬고,

public enum Menu {
    MUSHROOM_SOUP("양송이수프", 6_000, APPETIZER),
    TAPAS("타파스", 5_500, APPETIZER),
    ...

 
Order 객체를 작성해 줬다. Order에서는 view에서 처리하지 못한 비즈니스 조건을 검증한 뒤, 객체를 생성한다.
 


2일 차 (Nov11, 2023)

3일 차 (Nov12, 2023)

 
사실 4주 차에는 3주 차까지와 다르게 구조적으로 따로 구상을 먼저 하지 않고 코드 작성을 시작하고 있었다.

public void calculateInitialOrderAmount() {
        int amount = 0;
        for (Menu menu : Menu.values()) {
            int quantity = order.getOrder().getOrDefault(menu, 0);
            amount += quantity * menu.getPrice();
        }
        initialOrderAmount = new InitialOrderAmount(amount);
    }

    public int getInitialOrderAmount() {
        return initialOrderAmount.amount();
    }

서비스에서 이런 식으로 처리를 하고 있었는데.. 3주차 공통 피드백에서 객체는 객체스럽게 사용한다.라는 부분이 있었고, 이대로 계속 구현을 하다 보면 4주 차 미션도 3주 차와 🐶같이 망해버릴 것 같다!라는 직감이 왔다.
 
++ 3일 차가 될 때까지, 구현하는 애플리케이션에 대한 감이 잡히지 않기도 했다. (개발에 속도가 안 붙었다.)
잠시 구현을 멈추고, 프로모션 이벤트를 객체 중심으로 개발하기 위해 어떤 모델이 필요할지, 각 모델에 어떤 책임을 줘야 할지 고민하는 시간을 갖기로 했다.
 


4일 차 (Nov13, 2023)

 
일단 2,3일 차에 Service단에 구현했던 것들을 갈아엎었다.
웬만하면 모델에서 처리하려고 노력했다.
 

Order 객체

public class Order {
    private final EnumMap<Menu, Integer> orderedMenu;

    public Order(List<SimpleEntry<String, Integer>> readOrder) {
        ...
    }

    public EnumMap<Menu, Integer> getOrderedMenu() {
        return orderedMenu;
    }

    public int getMainQuantity() {
        ...
        return mainQuantity;
    }

    public int getDessertQuantity() {
        ...
        return dessertQuantity;
    }

    public int getBaseOrderAmount() {
        ...
        return amount;
    }
...

}

 
위와 같이 객체의 인스턴스 필드를 통해 값을 가져올 수 있는 것이 있다면, 굳이 외부에서 처리하지 않고, 모델 내부에서 연산한 뒤 넘길 수 있도록 했다.
 
사실 처음에는 initialAmount 필드도 있었지만, orderedMenu 필드만 선언하면 할인 전 총 주문 금액은 이를 순회하면서 연산 후 반환 가능하기 때문에 제거했다. 이런 식으로 최소한의 인스턴스 변수만 선언하려고 했다.
 


 
 
4일 차는 이번 프로모션에서 가장 중요한, 할인 정책을 처리했다. 
할인 정책도 모델 단위로 작업하고, ChristmasPromotionService에서 조합하는 방식으로 진행했다.

public class ChristmasPromotionService {

    Order order;
    VisitDate visitDate;
    ChristmasDiscountCalculator discountCalculator = new ChristmasDiscountCalculator();

    public void setVisitDate(Integer readVisitDate) {
        visitDate = new VisitDate(readVisitDate);
    }

    public void setOrder(List<SimpleEntry<String, Integer>> readOrder) {
        order = new Order(readOrder);
    }

    public OrderSummary getOrderSummary() {
        return new OrderSummary(order);
    }

    public VisitDateSummary getVisitDateSummary() {
        return new VisitDateSummary(visitDate);
    }

    public PromotionSummary getPromotionSummary() {
        if (canReceivePromotion()) {
            DiscountedItems discountedItems = discountCalculator.calculateDiscounts(visitDate, order);
            DiscountResults discountResults = new DiscountResults(order.getBaseOrderAmount(), discountedItems);
            return new PromotionSummary(discountedItems.items(), discountResults.getTotalDiscountAmount(),
                    discountResults.getFinalPaymentAmount(), Optional.ofNullable(discountResults.getBadge()));
        }
        return new PromotionSummary(order.getBaseOrderAmount());
    }

    private boolean canReceivePromotion() {
        return order.getBaseOrderAmount() > PROMOTION_THRESHOLD.getAmount();
    }

}

 
ChristmasPromotionService를 대략적으로 살펴보면, 방문 날짜와 주문 내역을 입력받아 객체로 저장하고, 전부 ***Summary 형태의 객체로 묶어서 보내고 있다.

이벤트 플래너가 안내하는 정보를 1. 방문 날짜, 2. 주문 내역, 3. 할인 혜택 이렇게 3가지로 판단했을 때, 
service 단에서 세 번의 출력을 위한 정제된 객체를 만들어서 보내고 있는 것이다.
이 summary 객체들은 controller로 넘어가면, controller에서 view 단으로 넘겨 출력문에 사용된다.
 

Service의 getPromotionSummary 메서드

    public PromotionSummary getPromotionSummary() {
        if (canReceivePromotion()) {
            DiscountedItems discountedItems = discountCalculator.calculateDiscounts(visitDate, order);
            DiscountResults discountResults = new DiscountResults(order.getBaseOrderAmount(), discountedItems);
            return new PromotionSummary(discountedItems.items(), discountResults.getTotalDiscountAmount(),
                    discountResults.getFinalPaymentAmount(), Optional.ofNullable(discountResults.getBadge()));
        }
        return new PromotionSummary(order.getBaseOrderAmount());
    }

할인 정책에 대한 부분은 결국, 이 getPromotionSummary에서 처리된 뒤, Summary 형태로 만들어 반환한다.
 

  1. DiscountCalulator을 통해, 할인 정책 별 혜택 금액을 연산한다.
    1. DiscountCaclator은 연산을 하기 위해서 할인 정책(EventSchedular, DiscountPolicy)을 사용한다.
    2. 연산이 완료되면, DiscountedItems 객체를 반환하는데, 여기에는 할인 정책별로 얼마만큼의 할인이 적용되었는지가 List로 담겨있다.
  2. Order객체에서 할인 전 주문 금액과, 앞 전에 만든 DiscountedItems를 사용해 DiscountResult를 만든다.
    1. DiscountResult는 파라미터로 넘겨받은 값을 통해, 총 혜택 금액 / 예상 결제 금액 / 배지를 필드로 갖는다.
    2. 배지를 총 혜택 금액을 통해 정적 팩토리 메서드로 초기화된다.
  3. DiscountItems와 DiscountResult가 가진 인스턴스 변수를 활용하여 PromotionSummary로 정제된다.

 
할인 정책 부분을 좀 더 구체적으로 살펴보면, EventSchedular와 DiscountPolicy 중, 
EventSchedular는 할인이 적용되는지 판단하는 역할을 하고, DiscountPolicy는 할인 연산을 담당한다. 얼마만큼의 할인이 들어가는지 체크해서, 정책 별 할인 금액을 반환하는 역할이라고 생각하면 된다.

public class ChristmasDiscountCalculator {
    private final EventSchedular eventSchedular;
    private final DiscountPolicy discountPolicy;

    public ChristmasDiscountCalculator() {
        this.eventSchedular = new ChristmasEventSchedular();
        this.discountPolicy = new ChristmasDiscountPolicy();
    }

    public DiscountedItems calculateDiscounts(VisitDate visitDate, Order order) {
        List<DiscountAmount> discounts = new ArrayList<>();
        discounts.add(discountDDay(visitDate));
        ...
    }
    
    private DiscountAmount discountDDay(VisitDate visitDate) {
        if (eventSchedular.isDDayDiscountDay(visitDate.getDate())) {
            int amount = discountPolicy.calculateDDayDiscountPrice(visitDate.getDate().getDayOfMonth());
            return new DiscountAmount(DDAY_DISCOUNT, amount);
        }
        return new DiscountAmount(DDAY_DISCOUNT, 0);
    }
    
    ...
}

 
DiscountAmount라는 record를 List로 선언을 해서, 할인 정책별로 적용된 할인 금액이 묶여 관리될 수 있도록 했다.


 
할인 정책을 결정할 때 고민되었던 부분은,  "증정 이벤트가 할인 정책에 포함이 되어야 할 것인가?"였다.
그래서 커밋을 확인해 보면 정말 여러 번 DiscountSettings에 포함시켰다, 제거했다가를 반복한다.
 
증정 이벤트를 할인 정책에 포함을 시킨다 하면,
- 장점:  총 혜택 금액 연산 시, Stream으로 DiscountedItems의 amount를 전부 더하면 끝이다. (매우 간단)
- 단점 : 예상 결제 금액 연산시, 할인 전 총 주문 금액 - 혜택 금액 + 증정 메뉴 금액 (증정 메뉴를 다시 더하는 번거로움)
 
증정 이벤트를 할인 정책에 포함을 시키지 않으면,
- 단점 : 총 혜택 내역 및 혜택 내역 출력 시, 증정 이벤트 적용 여부와 DiscountItems를 별도로 불러와 병합해야 함 (귀찮)
- 장점 : 예상 결제 금액 : 할인 전 총 주문 금액 - 혜택 금액 (중복 연산 x)
 
초반에는 증정 메뉴 금액을 빼고 더하는 중복 연산을 피하기 위해, 포함을 시키지 않으려고 했다.
 
결국에 포함을 시켰던 이유는, "총 혜택 금액" , "혜택 내역" 키워드에 증정 이벤트가 포함이 되기 때문이다. 
증정 이벤트도 혜택 내역에 포함을 시키는 것이 이벤트 전체를 봤을 때 더 통일성 있겠다는 생각을 하면서 합치게 되었던 것 같다.
대신에 Giveway라는 Enum 클래스를 생성해 증정품 객체를 따로 관리하는 건 할 수 있도록 했다.
 


5일 차 (Nov14, 2023)

5일 차는 출력과 에러 핸들링을 끝으로, 구현을 완료한 날이다.
 
4일 차에서 언급했던 PromotionSummary도 사실 5일 차에 만든 것이다.
그리고 에러 핸들링을 해줬다.

"[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."

라고 적혀있으니까 다시 입력을 받아야하는데..  멍청하게 그동안 파악을 못했고, Exception 날리면 종료되도록 해뒀길래, 3주차에 했던 것 처럼 try catch를 사용해서 Exception을 잡을 수 있도록 해주었다.
 
"다시 입력해 주세요" 니까 에러 핸들링의 역할이 InputView에서 담당해야 하는 로직이라 판단했고, InputView에서 핸들링을 효과적으로 할 수 있는 방법이 없을까 하다가..

public class InputView {

    public static <T> T getValidInput(Supplier<T> inputSupplier) {
        while (true) {
            try {
                return inputSupplier.get();
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }    
    ...
}

함수형 프로그래밍을 위한 인터페이스, Supplier를 알게 되었다. Suppelier은 get() 메서드만 가지는데, 입력으로 들어온 메서드가 정상적으로 실행이 되면 그 결과를 그대로 return 해주고, 아니라면 Exception을 발생시킨다.
 
위와 같이 InputView에 getValidInput을 선언해 두면, Controller에서 Input을 가져올 때 아래와 같은 방식으로 검증+핸들링된 Input을 가져오는 것이 가능해진다.

private void takeVisitDate() {
    OutputView.printIntroductionMessage();
    OutputView.printTakeDateMessage();
    service.setVisitDate(InputView.getValidInput(InputView::readVisitDate));
}

private void takeOrder() {
    OutputView.printTakeOrderMessage();
    service.setOrder(InputView.getValidInput(InputView::readOrder));
}

 
이렇게 구현하고 오! 엄청 깔끔해졌다ㅎㅎ 하면서 뿌듯해하고 있었는데, 이렇게 InputView에 핸들러를 넣으니까, 모델에서 검증했을 때 Exception이 발생하면, 해당 경우를 잡을 수가 없는 문제가 있어서, 6일 차에 수정한다...😅 
 


6일 차 (Nov15, 2023)

 
order와 visitDate는 Summary가 아니라, Formatter에서 포맷팅을 해서 출력을 해주고 있었는데, PromotionSummary에서 정제된 값을 가져왔던 것처럼, 주문 내역과 방문 날짜에 대해서도 통일성을 가져가면 좋을 것 같아 OrderSummary와 VisitDateSumarry도 생성하게 되었다.
 
그리고 6일 차에 밀린 단위 테스트를 전부 작성했다. 4주차 미션에서는 구조를 계속 변경하고 하다보니까, 어느 순간부터 (아마도 할인 정책 적용을 하면서..) 단위 테스트 작성을 제깍제깍 못하고 있었는데, 6일차에 전부 해주었다. 가장 작은 모델 단위부터 Service 계층까지, 기능 명세서에 있는 요구사항 관련해서 테스트하지 않은 부분이 있다면 전부 작성했다.
 
단위 테스트를 작성하면서, 총 혜택 금액, 배지, 최종 결제 금액만을 필드로 가지고 있는 <DiscountResult> 객체에 대해, 테스트를 작성하려고 보니, 고려해야 하는 케이스가 너무나도 많았던 문제가 있었다.

/**DiscountResult의 내부 메서드*/
private Badge initializeBadge() {
    for (Badge badge : Badge.values()) {
        if (totalDiscountAmount > badge.getThreshold()) {
            return badge;
        }
    }
    return null;
}

<DicountResult>에서 혜택 금액을 배지에 넘겨주기만 하면, 배지가 스스로 결정을 내릴 수 있는 부분을, <DiscountResult> 객체에서 배지의 결정 로직을 담당하고 있었기 때문이었다. 

/**Badge가 금액을 받아 직접 결정*/
public static Badge of(int totalDiscountAmount) {
        for (Badge badge : Badge.values()) {
            if (totalDiscountAmount >= badge.getThreshold()) {
                return badge;
            }
        }
        return null;
    }
}

 
'테스트하기가 어렵다'라고 느껴지는 것이 객체 간 책임이 적절하게 정의되지 않았다는 신호로 받아들였다. 따라서 위와 같이 수정했고, 혜택 금액에 따른 최종 결제 금액 테스트와, 배지 결정 테스트를 분리해서 작성할 수 있게 했다.
 
그리고 5일 차에서 언급했던 모델에서 검증했을 때 Exception이 발생하면, 해당 경우를 잡을 수가 없던 문제를 제출 직전에 발견해서^^ 다시 핸들링 로직을 controller단으로 빼주었다. 
 
이 때도 함수형 프로그래밍을 포기할 수 없어서 단순하게 '실행만' 하는 함수형 인터페이스, Runnable을 찾아서 적용해 줬다.

public class ChristmasPromotionController {
	
    ...
    
    private void takeVisitDate() {
        run(() -> {
            OutputView.printIntroductionMessage();
            OutputView.printTakeDateMessage();
            Integer visitDate = InputView.readVisitDate();
            service.setVisitDate(visitDate);
        });
    }

    ...
    
    private void run(Runnable inputRunnable) {
        while (true) {
            try {
                inputRunnable.run();
                break;
            } catch (IllegalArgumentException e) {
                OutputView.printMessage(e.getMessage());
            }
        }
    }
}

 
이번 미션에서 함수형 인터페이스를 처음 써봤는데, 아직 잘 알지는 못하지만 코드 가독성이 엄청 향상되는 느낌을 받아서, 적재적소에 적용할 수 있도록 좀 더 알아보면 좋을 것 같았다.

 


 1주 차 미션을 시작할 때까지만 해도, "객체 지향적으로 짜는 것"에 초점을 맞추어 한 객체가 수행해야 할 역할에 대해 큰 고민을 했는데, 한 주씩 지나갈 때마다, 다양한 아키텍처, 다양한 API를 사용해 보는 것에 신경을 쓰면서 본질적인 것들을 점점 놓쳐버렸다 생각한다.
 
이번 주차에는 우테코 측에서 학습 목표를 통해 명확한 방향성을 제시해 주셨기 때문에, 객체를 객체답게 사용함으로써, 하나의 객체가 본인의 책임을 다할 수 있도록 하고자 노력했다. 객체가 가지는 필드를 최대한으로 활용해서, 객체 내부에서 로직을 처리하고, service, controller는 이들을 이어주는 중간 다리로써의 역할만 하도록 했다.
 
객체가 책임을 다하도록 하니, 그토록 감이 잡히지 않았던 model-view-controller-service 구조에 어느 정도 근접할 수 있었다고 생각한다.
 
공통 피드백 내용을 코드에 녹이기 위해 반복적인 리팩토링을 하다 보니, 아쉬웠던 점도 있었다.
자꾸 구조를 변경하면서, 커밋 메시지가 이전 미션들에 비해 깔끔하게 정의되지 못했단 점이었다.
객체를 쪼개고 합치고, 디렉터리 구조를 변경하는 행위를 반복하면서, 테스트를 작성 시기를 계속 놓치기도 했다.
 
그렇지만 객체지향적으로 구현하고자 거쳤던 많은 고민 덕분에, 마지막 날 단위 테스트를 짜려고 했을 때 수월했다고 생각한다.
 
테스트 코드를 작성하는데 크게 고민할 필요 없이, ‘어떤 input 이 들어갔을 때, 어떤 output 이 나와야 한다.’라는 단순한 로직만으로 테스트 코드를 작성할 수 있어서 굉장히 뿌듯했다! ㅎㅎ
 
3주 차 미션을 끝내면서 단위 테스트를 효율적으로 작성하는 것을 목표로 삼았었는데, 4주 차 미션에서 이를 실천했다. 메서드나 리스트로 여러 파라미터를 넘김으로써 동일한 테스트를 다양한 케이스로 테스트하는 작업을 자동화하였고, 보다 정밀한 테스트를 구현했다.
 
한 달 동안 많이 성장하긴 했지만, 4주 차 미션을 마치는 지금까지도 부족하게 느껴지는 부분이 많다.
4주차 미션 내용인, 우테코 식당에서 주최하는 많은 이벤트를 위해 재사용하기 위해서 진행해야 할 추상화도 많다..
 
우선 가장 먼저 생각나는 부분은 model/policy/calender에 있는 클래스들이다. 이 부분이 깔끔하지 않다고 생각된다. 특히 날짜를 관리하는 부분을 더 확장성 있게 설계할 수 있지 않을까 싶다.


 
한 달간 몰입했던 우테코가 끝나니, 뭘 해야 할지 살짝 막막하다.. 사실 3주차 리뷰도 써야함
 
그래도 막학기니까 일단 학교 수업 열심히 들어야 하고 ㅎㅎ 자바로 코테 매일 한 문제 풀기 다시 시작하려고 한다!! ㅎㅎ
그리고 한 달 동안 정신없이 구현했던 4가지 미션의 코드들을, 한 달간 받는 피드백을 바탕으로 리팩토링 할 예정이다!
MVC도 야매로 말고, 강의 들으면서 제대로 공부해 볼 거다ㅎㅎ
 
사실 힘든 것보다 너무 재밌고 행복한 한 달이었다. 고생했다 나 자신~~ 😁

 
https://github.com/seminss/java-christmas-6-seminss

 

GitHub - seminss/java-christmas-6-seminss

Contribute to seminss/java-christmas-6-seminss development by creating an account on GitHub.

github.com

 


++ 추가 (11/29)

4주차 리뷰 받은 부분

  1. simple entry 보다는 dto를 만들어서 input 값을 가져오는 게 어떨까요?
  2. .ChristmasDiscountCaculator가 너무 많은 일을 하고 있다. 각 할인 정책을 별도로 분리하는 게 어떨까요?
  3. 한 클래스에는 한 책임만 있어야 한다.
    1. SRP, OCP 위반
    2. 기존 코드의 변경으로 이어지면 안된다. 수정이 필요하면 추가 하는 방식!
  4. isWeekday를 선언했으면, isWeekend는 필요 없다.
  5. 뱃지를 결정하는 로직을 Badge 내부에서 for문을 돌리면 Badge의 순서에 강하게 의존한다. enum에는 순서가 없으니, if문으로 바꾸는 게 좋을 것 같다.
  6. 생성자 주입을 하자.
  7. DiscountDDay에서 EarlyReturn을 하자.
  8. Service에서 상태값을 가지면 안된다. order visitDate는 있으면 안되고, 메서드에서 결과를 연산해서 반환만 하는 식으로. stateless 하게 해줘야 한다.
    1. 스프링은 서비스, 컨트롤러 모두 싱글톤 방식을 사용하는데, 유저가 들어올 때마다 새로 만들면 문제는 안되지만, 싱글톤을 쓰게 되면 돌려쓰면 돼서 성능적 이득이니까 싱글톤 상황을 고려하는 게 맞다.
  9. Integer를 쓰지 않아도 되는 상황에서는 Integer를 쓰지 않는게 좋다. Integer를 쓰면 해당 값이 null을 가질 수도 있다는 인식이 생긴다.
  10. Optional을 파라미터로 쓰는 것은 안티패턴이다. 함수의 반환값, 필드값으로만 써야 한다.
  11. 테스트 코드를 작성할 때 여러 Assert문이 있으면 위에 테스트가 실패했을 때 아래 테스트는 작동하지 않는다 → Assert Softly 를 활용하자.
  12. 유틸밖에 없는 클래스는 생성자를 private으로 닫아주자. 안그러면 자바에서 기본으로 생성해주는 생성자는 public이다.

리펙토링 된 코드 🖍️🖍️

https://github.com/seminss/java-christmas-6-seminss/tree/refactor

 

GitHub - seminss/java-christmas-6-seminss

Contribute to seminss/java-christmas-6-seminss development by creating an account on GitHub.

github.com