대망의 4주차 미션이 끝이 나면서, 프리코스를 마무리하게 되었다. 🤗🤗🤗
마지막 주차 미션은 다른 주차와 다르게 난이도가 많이 높았고, 비공개 저장소에서 구현하는 시스템 하에 미션을 구현하게 되었다. 보통 미션을 구현하기 위해서 하루 이틀을 설계하는 것에 사용하고, 나머지 시간을 구현하고 리팩토링하는 것에 시간을 썼다. 그러나 이번 미션에서는 요구 사항이 복잡하고 객체의 역할과 책임을 분리하는 것이 어려워서, 설계를 하는 것에만 거의 3일 넘게 썼다. 그래서 실제 구현한 기간은 3일 남짓 정도 밖에 안되었지만, 오래 고민하고 설계한 덕분에 구현 과정에서는 설계 자체에 대해서는 머리를 싸매지 않고 순조롭게 개발할 수 있었다.
미션을 구현할 때마다 늘 새로운 부분을 배우고 적용하는 경험을 하였다. 특히 이번 미션에서는 코드의 재사용성을 높이기 위해서 객체지향의 4대 특징 중 다형성, 상속에 집중하여 개발하였다. 개인적으로 객체지향 프로그래밍을 공부하며 배운 이론들을 프로그램을 설계하는 것에 적극적으로 활용해 본 경험이 부족하여 여기서 난항을 많이 겪었다.
머리 속에서 구현한 코드들이 실제로 동작하는 지 경험한 적이 없기 때문에, 코드를 일단 써보고 이게 되나? 하면서 실험적인 태도로 코드를 작성하였던 것 같다. 그러다 보니 주먹구구식으로 코드를 작성하고, 코드가 깔끔하지 못하면 다시 리팩토링하는 과정을 반복하며 구현을 완성하였다.
또한, 나는 자바 언어를 제대로 배운 적이 없고, 개발도 모두 독학으로 공부하였다. 그렇다보니 코드를 혼자 설계해보고 특히 자바 언어로 내가 생각한 설계를 코드로 옮기는 것도 무척 어려웠다. 그렇지만 프리코스를 수행하면서 4주차 미션과 같이 난이도 높은 미션을 꽤 괜찮은 설계(?) 로 구현할 수 있었던 것은 단언컨데 이전 미션 PR에 대해 받은 코드 리뷰들 덕분이다.
나는 첫 주차에 미션을 제출할 때만 해도 이보다 더 잘할 수 없다고 생각하면서 구현을 완료하였다. 그렇지만 처음으로 코드 리뷰 문화를 경험하고 다른 사람들의 코드를 보니 어떻게 이렇게 깔끔하게 코드를 작성하지? 여기서 어떻게 이렇게 접근할 생각을 할 수 있지? 라며 놀라는 경우가 많았다. 역시 아직 갈길은 멀다, 라고 생각하며 내가 부족했던 부분, 다음 미션에서 공부해볼 부분을 기록하고 내 PR에 달린 소중한 코드 리뷰들에 대해서도 적극 반영하려고 노력했다. 이를 매주 반복하다보니, 나도 놀랄 정도로 코드의 퀄리티가 많이 좋아졌음을 느끼게 되었다.
물론 아직도 많이 부족하지만, 프리코스 기간동안 이 정도로 몰두하면서 성장을 이뤄냈음에 스스로 자신감이 생기고 앞으로의 개발 공부에 대한 열의도 가지게 되었다.
주저리주저리는 이쯤 하고, 이번 미션에 대한 고민 과정을 풀어보겠다.
📝 구현 기능 목록
고객이 식당에 방문할 날짜를 입력하는 기능
-
안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.
메시지를 출력한다. -
12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
메시지를 출력한다. - 고객이 식당에 방문할 날짜를 입력한다.
- 뷰 레이어의 유효성 검증
- 빈 입력이 아님을 검증한다.
- 숫자로만 입력되었음을 검증한다.
- 문자열 입력을 숫자로 변환하여 반환한다.
- 뷰 ➡ 컨트롤러 레이어로 입력값 전달
- 날짜를 저장하는 객체를 생성한다.
- 도메인 레이어(Date)의 유효성 검증
- 1일부터 31일까지의 날짜 입력인지 검증한다.
- 뷰 레이어의 유효성 검증
고객이 주문할 메뉴와 메뉴의 개수를 입력하는 기능
-
주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
메시지를 출력한다. - 고객이 주문할 메뉴를 입력한다.
- 뷰 레이어의 유효성 검증
- 빈 입력이 아님을 검증한다.
- 적절한 쉼표를 이용해 구분되었음을 검증한다.
- 메뉴와 개수가 적절한 구분자를 이용해 구분되었음을 검증한다.
- 주문할 메뉴의 개수 입력이 숫자임을 검증한다.
- 뷰 ➡ 컨트롤러 레이어의 입력값 전달
- 쉼표를 기준으로 입력 문자열을 나눈다.
- 주문할 메뉴(String)와 메뉴의 개수(int)로 나눈다. (OrderRequest)
- List를 반환한다. (OrdersRequest)
- 도메인 레이어(Orders)의 유효성 검증
- 중복된 메뉴가 입력되지 않았는지 검증한다.
- 음료만 주문하지 않았는 지 검증한다.
- 총 1개 이상 20개 이하의 메뉴인지 검증한다.
- 도메인 레이어(Order)의 유효성 검증
- 메뉴에 있는 음식이 입력되었음을 검증한다.
- 도메인 레이어(Count)의 유효성 검증
- 메뉴의 개수가 1 이상임을 검증한다.
- 뷰 레이어의 유효성 검증
주문 메뉴를 출력하는 기능
- 도메인 ➡ 컨트롤러 레이어의 입력값 전달
- 도메인 레이어(Order)의 주문 메뉴의 이름과 메뉴의 개수의 래퍼 클래스 (OrderResponse)를 생성한다.
- 모든 주문 정보를 저장한 객체 (OrdersResponse)를 반환한다.
-
12월 3일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!
메시지를 출력한다. - 주문한 메뉴와 개수를 출력한다.
-
<주문 메뉴>
를 출력한다. -
바비큐립 1개
와 같이 주문한 메뉴와 개수를 개행으로 구분하여 출력한다.
-
할인 전 총 주문 금액을 출력하는 기능
- 할인 전 총 주문 금액을 계산한다.
- 도메인 레이어 (Order)에서 메뉴 당 주문 금액을 반환한다.
- 반환된 결괏값을 모두 더한다. (Orders)
- 결괏값을 컨트롤러에 전달한다.
- 할인 전 총 주문 금액을 출력한다.
-
<할인 전 총 주문 금액>
을 출력한다. - 컨트롤러에서 총 주문 금액을 전달 받아 출력한다.
-
12월 이벤트를 적용하는 기능
- 크리스마스 디데이 할인 이벤트를 적용한다.
- 입력한 날짜가 이벤트 기간에 속하는 지 확인한다.
- 날짜를 기준으로 할인 금액을 계산한다.
- 할인 금액을 반환한다.
- 평일 할인 이벤트를 적용한다.
- 입력한 날짜가 일요일 ~ 목요일에 해당하는 지 확인한다.
- 디저트 메뉴를 조회하고, 메뉴 당 할인 금액을 계산한다.
- 디저트 메뉴의 가격이 할인 금액보다 작다면, 디저트 메뉴 가격만큼 할인한다.
- 총 할인 금액을 반환한다.
- 주말 할인 이벤트를 적용한다.
- 입력한 날짜가 금요일, 토요일에 해당하는 지 확인한다.
- 메인 메뉴를 조회하고, 메뉴 당 할인 금액을 계산한다.
- 메인 메뉴의 가격이 할인 금액보다 작다면, 메인 메뉴 가격만큼 할인한다.
- 총 할인 금액을 반환한다.
- 특별 할인 이벤트를 적용한다.
- 입력한 날짜가 특별 할인 이벤트 적용일에 해당하는 지 확인한다.
- 총 할인 금액을 반환한다.
- 증정 이벤트를 적용한다.
- 할인 전 총 주문 금액이 기준 금액 이상인지 확인한다.
- 증정 상품과 개수를 반환한다.
이벤트를 적용한 결과를 생성하는 기능
- 이벤트를 적용한 결과를 반환한다.
- 총 주문 금액이 10,000원 이상인 경우 이벤트를 적용한다.
- 이벤트 종류와 혜택 내역을 저장한다.
- 할인 이벤트와 증정 이벤트 결과를 저장하여 반환한다.
이벤트 결과를 출력하는 기능
- 증정 메뉴를 출력한다.
- 증정 메뉴가 있다면 증정 메뉴의 이름과 개수를 출력한다.
- 증정 메뉴가 없다면
없음
을 출력한다.
- 혜택 내역을 출력한다.
- 이벤트와 혜택 금액을 뷰 레이어로 전달한다.
- 이벤트와 혜택 금액을 출력한다.
- 총 헤택 금액을 출력한다.
- 할인 금액의 합계와 증정 메뉴의 가격을 더한다.
- 총 혜택 금액을 출력한다.
- 할인 후 예상 결제 금액을 출력한다.
- 할인 전 총 주문 금액에서 할인 금액을 뺀 결제 금액을 뷰 레이어로 전달한다.
- 할인 후 예상 결제 금액을 출력한다.
배지를 부여하는 기능
- 부여할 배지를 반환하는 기능
- 총 혜택 금액을 계산한다.
- 혜택 금액에 따라 적절한 배지를 반환한다.
- 배지를 출력하는 기능
- 부여할 배지가 있다면 배지의 이름을 출력한다.
- 부여할 배지가 없다면
없음
을 출력한다.
🛠 시스템 흐름도
사용자에게 방문 날짜와 주문 내역을 입력 받고 도메인을 생성하는 기능
이벤트를 적용하고 이벤트 결과를 콘솔에 출력하는 기능
🧐 새롭게 고민한 내용들 (feat. 3주차 미션 코드 리뷰)
상속 객체에 대한 제네릭의 사용
요구사항에서 이벤트의 결과로 사용자에게 제공될 수 있는 하나의 혜택은 두 가지 형태이다.
첫 번째로 총 금액에서 할인해주는 할인 혜택, 증정품을 제공하는 증정 혜택이다. 할인 혜택은 할인 금액에 대한 정보만을 가질 수 있고, 증정 혜택은 증정품의 종류와 개수를 가지고 있다. 요구사항을 객체지향적으로 구조화하여 이렇게 두 가지 객체를 만들고, 이 두 개의 클래스에 대한 상위 클래스로 혜택 객체를 생성하였다. 이에 따라 아래와 같은 구조가 완성이 되었다.
이렇게 구현하면, 모든 형태의 혜택들은 Benefit 클래스를 상속하는 일관성을 가지는 클래스로 구현될 수 있다.
그럼 이벤트의 결과로 혜택을 증정할 수 있는 기능은 어떻게 구현할 수 있을까? 이벤트가 어떤 혜택을 제공하던 최종 반환되는 객체가 일관되도록 Benefit 클래스를 활용해보자.
예를 들어, 크리스마스 디데이 이벤트를 적용하는 함수를 살펴보자.
내가 설계한 것을 설명하자면, 크리스마스 이벤트를 적용할 수 있는 조건이 충족된 경우 결과 객체를 반환하고 그렇지 않은 경우 빈 객체를 반환한다. 이러한 로직 상 Optional을 사용하게 되었고 실질적으로 반환하는 객체는 PromotionResult이다. 할인 이벤트이든, 증정 이벤트 든 같은 객체를 반환하기 위해 PromotionResult를 공통으로 반환하도록 설정하였고, 클래스는 아래와 같이 작성하였다.
이 부분에서 <T extends Benefit> 부분이 상속된 객체에 대한 제네릭을 사용하는 부분이다.
T라는 타입을 제네릭으로 지정하여 T 자료형으로 선언된 benefit에는 Benefit 클래스를 상속하는 Gift, Promotion만이 올 수 있어 확장에는 열려 있고 개방에는 닫혀있는 개방 폐쇄 원칙 (OCP)을 준수한 코드를 작성하였다.
인터페이스를 통한 다형성 사용
미션을 구현하면서 했던 고민 중에 하나는 여러 가지 이벤트를 어떻게 하나의 함수에서 한번에 적용시킬 지에 대한 고민이다. 먼저 떠오른 생각은 단일 함수를 가지는 함수형 인터페이스를 구현하고, 이벤트마다 적용을 시키는 설계였다. 모든 이벤트들이 공통적으로 아래와 같은 로직을 가지고 있었기 때문이다.
입력 : 고객이 방문할 날짜, 주문 내역
중간 과정 : 이벤트 적용 조건에 부합하는 지 검사
출력 : 고객에게 제공할 혜택
이에 따라 설계한 인터페이스는 다음과 같다.
public interface PromotionService<T> {
public abstract T apply(Date date, Orders orders);
}
자바에서 제공하는 함수형 인터페이스인 Function을 사용하는 방법도 있었지만, 현재 설계와 같은 경우 두 가지의 입력 조건이 있었기 때문에 인터페이스를 직접 구현하였다.
그리고 이를 구현한 5가지 이벤트에 대해 메서드를 오버라이딩하여 요구사항에 맞게 방문할 날짜와 주문 내역을 통해 조건을 검사하여 PromotionResult를 반환하는 로직을 설계하였다.
이어진 고민은 모든 구현 클래스들을 하나의 함수에서 모두 호출하는 것이다. 여기서 다형성의 개념을 적용해 인터페이스 리스트를 생성하여 모든 구현 클래스를 수동으로 등록한다.
private List<PromotionService<?>> register() {
return Arrays.asList(
new ChristmasPromotion(),
new WeekdayPromotion(),
new WeekendPromotion(),
new SpecialPromotion(),
new GiftPromotion()
);
}
이렇게 만들어진 리스트 순회하며 인터페이스의 메서드를 아래와 같이 호출한다.
private List<PromotionResult> generatePromotionResults(
Date date, Orders orders,
List<PromotionService<?>> promotionServices
) {
return promotionServices.stream()
.map(promotionService -> generatePromotionResult(
date,
orders,
promotionService
))
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
}
private Optional<PromotionResult> generatePromotionResult(
Date date, Orders orders,
PromotionService<?> promotionService
) {
return (Optional<PromotionResult>) promotionService.apply(date, orders);
}
이렇게 만들어진 결과 객체의 리스트 List<PromotionResult>를 반환하게 된다.
이렇게 모든 구현체들을 인터페이스에 일일이 담아 호출하게 되면 다형성의 원리가 적용되어 실제 구현 클래스의 apply 메서드가 호출되고, 각 이벤트에 알맞는 객체가 반환될 수 있다. 새로운 이벤트가 기획될 때마다 일일이 리스트에 등록해야한다는 단점이 있었지만, 더 나은 방법이 떠오르지 않아서 내가 생각하는 최선의 방식대로 구현하였다.
재귀 호출에 의한 스택오버플로우 방지
지난 미션에서 잘못된 입력이 발생했을 때 다시 입력을 받기 위해 try - catch 문을 작성하고 재귀호출을 하는 것으로 구현하였다. 이러한 구현 방식이 스택 오버플로우를 발생시킬 수 있는 원인이라고 지적을 받았고, while문을 사용하여 구현하기로 다짐하였다.
그러나 문제는 중복되는 while문의 사용으로 인한 가독성이 떨어진다는 점이었다.
https://dev-dongmoo.tistory.com/14
그래서 while문을 사용하더라도 가독성을 높이기 위해서 함수형 인터페이스 중 하나인 Supplier를 사용해, 동작 자체를 인자로 전달하는 방식으로 구현하였다.
public void run() {
Date date = retry(() -> {
return Date.from(inputView.requestDate());
});
Orders orders = retry(() -> {
return Orders.from(retry(inputView::requestOrders));
});
response(date, orders, promotionHandler.process(date, orders));
}
private static <T> T retry(Supplier<T> supplier) {
while (true) {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
ConsoleWriter.printlnMessage(e.getMessage());
}
}
}
그 결과로 이렇게 정적 메서드를 하나 생성한 다음 예외가 반환될 수 있는 메서드를 호출하여 성공할 시 객체가 반환되면서 while이 종료될 수 있도록 코드를 작성하였다.
소감
열심히 설계하고 열심히 코드를 작성하였지만 이게 최선이었나 라는 질문에 대해 확신을 가질 수 없었다. 항상 나보다 더 수려한 코드를 작성한 사람들은 있었고, 내가 맞다고 생각했던 부분도 관점에 따라서 틀릴 수 있었기 때문이다.
그럼에도 4주차 프리코스를 마무리하면서 후회 없이 나의 모든 열정을 쏟아부었다는 것 하나는 자신 있게 말할 수 있다. 나는 학과에서 자바 언어를 한 번도 배운적이 없고, 백엔드 개발 분야를 오로지 독학하였기에 이렇게 객체지향 원칙에 대해, 자바를 효과적으로 사용하는 방법에 대해 고민해보지 못하였다. 그러나 이번 프리코스 기간동안 스스로 찾아보고, 다른 사람의 미션 PR을 코드 리뷰하면서 새로운 기술을 접해보고 나도 써보려 하면서 많이 성장할 수 있었던 것 같다.
프리코스는 끝났지만, 더 나은 개발자가 될 수 있는 날을 기약하면서 다음 단계를 계속 이어가려 한다.
미션 구현 코드 PR
https://github.com/Mingyum-Kim/java-christmas-6-Mingyum-Kim/pull/1
'우아한테크코스 > 레벨0' 카테고리의 다른 글
[회고] 우아한 테크코스 최종 코딩테스트 대비 - 다리 건너기 (0) | 2023.12.13 |
---|---|
[회고] 우아한테크코스 1차 심사 합격 + 앞으로의 계획 (0) | 2023.12.13 |
[회고] 우아한 테크코스 프리코스 1주차 회고 - 숫자 야구 게임 (5) | 2023.11.27 |
[회고] 우아한테크코스 프리코스 3주차 회고 - 로또 게임 (0) | 2023.11.11 |
[회고] 우아한테크코스 프리코스 2주차 회고 - 자동차 경주 게임 (1) | 2023.11.01 |