우아한테크코스 6기 백엔드 프리코스 - 크리스마스 프로모션

리팩토링

로직이 복잡해서 테스트를 통과시키는 것에 집중했더니 하드코딩한 부분과 중복이 많아졌다.

BenefitDetails

메서드에 중복된 로직이 존재한다.

BenefitDetails-redundant

BenefitDetails-after-refactoring

할인 이벤트가 추가되는 것을 견딜 수 있다.

하지만 위아래의 크리스마스 디데이, 특별 할인에 의한 혜택 금액은 그렇지 못하다. 유사한 이벤트(총혜택 금액에 대한 할인 이벤트)가 추가되면 변수를 추가하고 이런 메서드를 추가해줘야 한다.

BenefitDetails-fields

크리스마스 디데이 할인, 특별 할인을 어떻게 표현해야 유사한 할인 이벤트가 추가되어도 BenefitDetails 클래스를 수정하지 않을 수 있을까?
질문을 생각하면서 답이 자연스럽게 나왔다. 이벤트가 추가되는데 BenefitDetails가 수정된다면 이는 BenefitDetails책임이 한 개 이상이라는 뜻이다. 크리스마스 디데이 할인과 특별 할인을 클래스로 만들면 된다. 그리고 그 둘에 다형성을 적용하면 총금액에 할인이 적용되는 이벤트라는 형식으로 일관된 인터페이스를 통해 접근할 수 있다.

죽은 객체

getter 메서드는 객체의 배를 갈라 데이터를 꺼낸다. 배가 갈라진 객체는 죽은 객체이다.

feedback-1

feedback-2

3주차 미션의 공통 피드백에서도 getter를 호출하지 않고 객체에게 메세지를 던져 객체 고유의 로직을 실행하도록 권장하고 있다.

아래는 VisitDate 클래스인데 두 가지 문제점이 있다.

  • 자신의 로직을 실행하지 않고 getter로 필드를 반환하고 있다.
  • 다른 도메인(이벤트)의 정보(할인 행사 날짜)에 의존하고 있다.

VisitDate-1

VisitDate-2

살아있는 객체

VisitDate 클래스에서 getter 메서드를 제거하고 이벤트 도메인에 의존하는 코드를 제거했다.
어떤 이벤트가 진행중인지 모르기 때문에 이벤트 변경에 대해 영향을 받지 않게 되었다.
인스턴스가 표현하는 날짜 필드를 외부로 전달하지 않고 메세지를 받으면 로직을 실행한 뒤 그 결과를 반환한다.

VisitDate-r-1

VisitDate-r-2

크리스마스 디데이 할인 혜택 금액은 날짜에 대한 연산을 적용한다. getter를 사용할 수 밖에 없는걸까?
함수형 인터페이스를 사용하면 getter는 필요없다. 혜택 금액이 날짜에 대한 1차식이라면 해당 식을 전달하면 된다.

애플리케이션 구조

흰색은 논리적인 계층이고 회색 네모는 해당 계층에 해당하는 클래스이다.

architecture

  • IO 계층 - 사용자로부터 입력을 받아 서비스로 전달하거나 서비스 계층에서 전달된 정보를 출력한다.
  • controller 계층 - 애플리케이션으로의 진입점이 된다. IO에서 받은 데이터를 서비스 계층에 전달하고 서비스 계층이 생성한 결과를 IO 계층을 전달한다.
  • service 계층 - 모델 클래스들을 활용하여 서비스 로직을 실행하고 결과를 반환한다.
  • domain 계층 - 서비스를 제공하기 위해 필요한 클래스들이다.

domain 계층

서비스를 제공하기 위해 필요한 객체들이다. 도메인에 따라 크게 event와 order로 나뉜다.

event 상속도

구현해야 할 할인 이벤트는 아래와 같았다.

  • 평일, 주말 할인
  • 특별 할인
  • 크리스마스 디데이 할인

event-hierarchy

추상 클래스인 DiscountChristmasDDayDiscountSpecialDiscount의 중복을 뽑아낸 것이다.

Discount-abstract-class

인터페이스의 필요성

위의 그림에서 DiscountPerOrderDiscountPerItem 인터페이스를 정의한 이유는 다음과 같다.

  • 적용되는 양상이 같은 여러 이벤트를 하나의 컬렉션에 담아 관리할 수 있다.
  • 인터페이스를 보고 해당 이벤트가 어떻게 적용되는 지 유추할 수 있다.

BenefitDetails

할인 행사로 인해 받은 혜택을 표현하는 클래스.

BenefitDetails-1 BenefitDetails-2 BenefitDetails-3 BenefitDetails-4

이번 미션에서 가장 큰 클래스이다. 더 작은 클래스로 나눌 수 있을것 같은데 그러지 못한 점이 아쉽다.

테스트 커버리지

모든 코드는 테스트를 통과하기 위해 작성되었기 때문에 커버리지가 높을 수 있었다. 커버리지가 높았기 때문에 크고 작은 리팩토링을 마음껏 진행할 수 있었다. 테스트 코드가 없었다면 리팩토링을 한 뒤 여전히 정상적으로 작동하는지 확인하는 일은 끔찍했을 것이다. 🫠

test-coverage-1 test-coverage-2

커버리지가 낮은 클래스

ItemOrderInput 클래스에서 분기 커버리지가 낮게 나왔다. 인텔리제이가 지적한 부분은 equals 메서드였다.

low-coverage

ItemOrderInput.equals 메서드는 주문에 중복된 상품을 입력하는 경우를 잡기 위해 사용된다. 동일한 상품을 여러 번 주문한 경우에 대한 경우는 m4t12에서 테스트한다.

동일한 타입의 객체끼리는 비교했지만 서로 다른 클래스의 인스턴스와의 비교하는 테스트를 작성하지 않아 위의 코드가 테스트되지 않았다.

reference

Comments