식당에서 크리스마스를 맞아 프로모션을 진행하는 코드를 작성하는 미션이었다.
먼저 사용자로부터 식당 방문 예정일과, 주문할 메뉴를 입력받는다.
입력받은 날짜와 주문 내용에 따라 적절한 혜택을 적용하고 출력하는 프로그램이다.
이번 미션은 지난 미션들보다 필요한 기능과 제약조건이 많아서 생각해야 하는 부분이 많았다.
좀 더 효율적인 코드, 가독성이 좋은 코드, 아키텍처 설계를 고려한 코드, 유지보수하기 좋은 코드를 만들고 싶었다.
1. 구현할 기능 목록 작성
주어진 요구사항을 여러 번 반복해서 확인하면서 작성했다.
할인 조건, 할인 내용 등 세세한 조건들이 많아서 세부적인 부분까지 모두 작성했다.
예전에 프로젝트에서 사용자 요구사항 명세서를 적는 것과 유사하다고 생각했다.
요구사항 명세서를 작성할 때 세세한 내용을 기록해 두면 추후 코드 구현과정에서 해당 부분을 확인할 수 있어서 좋았다.
< 구현할 기능 목록 >
---
## 🧑🏻🍳 오픈 준비
- [X] 메뉴를 등록한다.
---
## 🙋🏻 주문 받기
- [X] 안내 메세지 출력
- [X] 방문 날짜 요청 메세지 출력
- [X] 방문 날짜 입력 받기
- [x] 1이상 31이하의 숫자만 입력받을 수 있다.
- [x] 유효조건에 맞지 않는 경우 에러메세지를 출력한다.
- [x] 에러 메시지 출력 후 그 부분부터 입력을 다시 받는다.
- [X] 주문 요청 메세지 출력
- [X] 주문 입력 받기
- [x] 메뉴판에 없는 메뉴는 입력할 수 없다.
- [x] 메뉴 개수는 1 이상의 숫자로만 입력할 수 있다.
- [x] 메뉴 형식이 예시와 같아야한다. (e.g. 시저샐러드-1)
- [x] 중복 메뉴는 입력할 수 없다. (e.g. 시저샐러드-1,시저샐러드-1)
- [x] 음료만 주문 시 주문할 수 없다.
- [x] 메뉴는 한번에 최대 20개까지만 주문할 수 있다.
- [x] 유효조건에 맞지 않는 경우 에러메세지를 출력한다.
- [x] 에러 메시지 출력 후 그 부분부터 입력을 다시 받는다.
---
## 💁🏻 주문 결과 출력
- [X] "예약일"에 받을 혜택 미리보기 메세지 출력
- [X] 주문한 메뉴 목록 출력
- [x] 주문 메뉴의 출력 순서는 자유롭게 할 수 있다.
- [X] 할인 전 총 주문금액을 출력
- [X] 증정 메뉴 출력
- [x] 증정 이벤트 조건에 부합하는지 확인하고 적용한다
- [x] 적용된 내역이 없는 경우 "없음"을 출력한다.
- [X] 이벤트(할인) 내역 출력
- [x] 총주문금액 1만 원 이상부터 이벤트가 적용된다.
- [x] 크리스마스 디데이 할인, 평일 할인, 주말 할인, 특별 할인 조건에 부합하는지 확인하고 적용한다.
- [x] 고객에게 적용된 이벤트 내역만 출력한다.
- [X] 적용된 내역이 없는 경우 "없음"을 출력한다.
- [X] 총혜택금액 출력
- [x] 총혜택 금액은 할인 금액의 합계 + 증정 메뉴의 가격이다.
- [X] 할인 후 예상 결제 금액 출력
- [x] 할인 전 총주문금액 - 할인 금액 가격이다.
- [X] 이벤트 배지 갯수 출력
- [x] 총혜택 금액에 따라 이벤트 배지를 부여한다.
- 5천 원 이상(별), 1만 원 이상(트리), 2만 원 이상(산타)
- [x] 적용된 내역이 없는 경우 "없음"을 출력한다.
---
### 📄 이벤트(할인) 종류 및 내용
- [X] 크리스마스 디데이 할인
- 1일 ~ 25일에 적용되며 1,000원부터 시작해 날마다 100원씩 증가함
- 총주문금액에서 해당 금액만큼 할인함
- [X] 평일 할인
- 일요일 ~ 목요일에 적용되며 디저트 메뉴 1개당 2,023원 할인
- [X] 주말 할인
- 금요일, 토요일에 적용되며 메인 메뉴 1개당 2,023원 할인
- [X] 특별 할인
- 이벤트 달력에 별이 있으면 총주문 금액에서 1,000원 할인
- [X] 증정 이벤트
- 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인(25,000원 1개 증정)
- [X] 이벤트 기간: 크리스마스 디데이 할인 제외한 다른 이벤트는 1일 ~ 31일동안 적용
---
### ❌ 에러메세지
- [X] 모든 에러 메세지는 [ERROR]로 시작한다.
- 방문 날짜가 잘못된 경우: "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."
- 주문 메뉴가 잘못된 경우: "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."
- 음료만 주문한 경우: "[ERROR] 음료만 주문할 수 없습니다. 다시 입력해 주세요."
- 메뉴를 20개 초과 주문한 경우: "[ERROR] 메뉴는 한번에 최대 20개까지만 주문할 수 있습니다. 다시 입력해 주세요."
---
2. 구조 설계
이번 과제에서 가장 고민이 많이 됐던 부분이다.
MVC 패턴을 지키면서 각 도메인들이 각자의 역할을 충실히 수행하고, SOLID 원칙을 지키고 싶었다.
- Controller
Planner: 전체 로직을 총괄하는 Controller로 Planner 클래스를 두었다. (과제에서 해당 역할을 '이벤트 플래너'라고 표현했다.) - Model
Product: 메뉴는 공통 속성인 [이름, 가격]을 필드로 갖는 Product 클래스를 상속받도록 하고 타입에 따라 Appetizer, MainDish, Dessert, Drink로 구분했다.
Order: 주문과 관련된 기능을 하며 필드로 Map <Product, Integer>를 설정해 주었다. 메뉴별 주문 개수를 확인할 수 있다.
Benefit: BenefitCondition인터페이스를 상속받은 각 클래스들이 할인 조건을 계산한다.
Benefit 클래스는 BenefitCondition []을 필드로 가지며 할인과 관련된 기능을 담당한다.
Badge: 배지 발급과 관련된 기능을 담당한다. - View
InputView: 사용자로부터 값을 입력받아 적절한 형태로 컨트롤러에 전달한다. Validator를 사용해 적절한 값인지 확인한다.
OutputView: 비즈니스로직이 적용된 결과를 사용자에게 출력한다. - Utils, Exception, Configurer: 유효성검증, 상수, 파싱, 생성자 주입 등 필요한 기능을 분리하였다.
< 프로젝트 구조 >
3. 비즈니스 로직 코드 작성
이전 프로그래밍 요구사항에 추가로 InputView, OutputView를 적용하도록 하는 조건이 추가되었다.
해당 부분은 지난 과제부터 적용하고 있던 방식이라 역할이 어디까지인지 좀 더 명확하게 구분하는 걸 목표로 했다.
세부 비즈니스 로직은 아래 링크에 기술되어 있다.
4차 과제 코드: (링크)
* 부족한 부분이 많은 코드입니다. 코드 리뷰는 언제든지 환영합니다.^^
4. 테스트 코드 작성
지난 과제에서 테스트코드 작성에서 아쉬운 부분이 많았다.
While(true){} 방식을 사용하다 보니 테스트하기 어려운 코드들이 있었고, private 메서드가 많아서 테스트에 한계가 있었다.
이번에는 Parser와 Validator를 구분하고, 도메인이 가지고 있는 필드에 대한 로직은 해당 클래스가 구현하도록해서 테스트 케이스를 좀 더 구체적으로 작성할 수 있었다.
또한 테스트 코드의 테스트 커버리지에 대한 내용을 인텔리제이를 통해 확인할 수 있었다.
5. 궁금한 점, 더 알아가야 할 것
과제를 구현하면서 스스로 의문이 생겼던 부분들이 있다. 이 부분은 좀 더 자료를 찾아보고 고민해봐야 할 것 같다.
- View의 역할은 어디까지인가?
InputView, OutputView는 입출력을 담당하고 있는 클래스이다. InputView의 경우 사용자 입력을 String으로 받아 유효성 검증 수행과 비즈니스 로직에서 필요한 형태로 파싱까지 해서 전달해주고 있다. 특히 이번 코드에서는 올바르지 않은 값이 입력된 경우 정확한 값을 입력받는 것까지 InputView에서 담당하고 있다.
유효성 검증과 파싱은 Validator와 Parser에서 담당하고 있기는 하지만, 최종 리턴값의 형태를 변경해서 주는 것이 맞는지에 대한 궁금증이 생겼다. 컨트롤러로 String값을 전달해 준 후 컨트롤러에서 파싱과 유효성 검증을 해야 하는 것일까? 그리고 잘못된 값이라면 다시 입력받도록 하는 것도 컨트롤러에서 하는 게 맞는 걸까?
우선 이번 코드를 작성하면서는 정확한 입력값을 받는 것까지가 InputView의 역할이라고 생각해서 입력과 관련된 모든 기능을 담당한다고 설정했다. SRP 관점에서 어떤 방식이 적절할지는 더 고민해봐야 할 것 같다.
마찬가지로, OutputView 역시 단순히 String값을 인자로 받아서 출력만 하는 역할을 해야 할지, 데이터를 전달받아 해당 내용에 맞는 내용을 만들어서 출력해야 할지 고민이었다. 이번에는 두 가지 방식이 모두 적용되었는데 이 부분은 한 가지 방식으로 통일이 되어야 좋을 것 같다. - 객체 간의 연관관계는 어느정도가 적절한가? 메서드 파라미터의 개수는 어느정도가 적절한가?
객체 지향 프로그래밍에서 객체간의 관계는 비즈니스 로직과 직접적인 관계가 있다. 이번에 코드를 구현하면서 엔티티 간 연관관계는 어떻게 구성해야 하는지에 대한 고민이 많이 됐다. Order는 Map <Product, Integer>를 필드로 갖게 되는데 이 방식이 맞는 것인지, 혹은 실제 주문을 하는 대상을 새로운 'Customer' 클래스로 두고 Customer - Product 사이에 Order를 객체로 만들어 Customer에게 List <Order>를 적용해야 할지 등 더 적절한 방식을 고민해야 했다. 이번 과제에서는 Customer는 한 명으로 고정되어 있어 크게 구분할 필요는 없으나, Customer - Product 관계가 N:N 관계가 된다면 Order를 중간 엔티티로 놓아야 할 것 같다.
또한 Benefit 메서드에서 혜택을 계산하려다 보니 대부분의 메서드에 date, order를 매개변수로 넣어주었는데 (할인 적용여부를 계산하려면 날짜와 주문내용에 대한 정보가 필요했다.) 이 부분이 가독성을 떨어트리는 것 같아 혹시 다른 방법은 없을지 고민해보고 있다. - 상수는 어디까지 사용해야 하는가?
프리코스 이전에 작성했던 코드들은 매직넘버나 메시지를 하드코딩하는 경우가 많았다.
이번 코스를 하며 다른 분들의 코드를 보고, 관련 자료를 찾아보며 매직넘버나 메시지를 상수처리하면 가독성이 향상되고 수정이 쉽다는 장점이 있다는 걸 느꼈다. 이번 과제에서는 상수를 따로 Util로 분리해서 작성해 보았는데 또 반대로 너무 많은 것들을 상수로 처리하게 되면 상수를 관리하는 비용이 발생할 수도 있겠다는 생각이 들었다. 이 부분은 Enum을 사용해서도 개선할 수 있다고 한다. 해당 부분에 대해서도 좀 더 알아볼 예정이다. - 반복문 사용이 적절한가? 자료 구조는 어떤 것이 적절한가?
이번 과제에서는 입력받은 메뉴가 메뉴 목록에 있는지, 여러 할인 조건 중에 일치하는 게 있는지 등 많은 부분에서 반복문이 적용됐다.
그러다 보니 눈으로 볼 때 너무 많은 for() 문이 있지 않은가 하는 생각이 들었다. (기능적으로는 정상 동작한다.)근데 해당 방식이 최적의 방식일까? 하는 궁금증이 생겼다. 지금은 데이터의 양이 많지만 만약 주문의 제약이 없어 엄청나게 많은 내용이 입력된다면? 혹은 메뉴가 무한하게 많아진다면? 등 여러 상황을 생각해 보게 됐다. 예전에 알고리즘을 배우면서 실제 프로그래밍에서 이게 사용이 될까? 하는 생각을 한 적이 있는데 이렇게 검색이나 특정 조건을 적용할 때 반영할 수도 있겠다는 생각이 들었다.
또한 자료 구조와 관련해서도 Map, Set, Tree 등을 크게 사용할 일이 없었는데 효율성이나 안정성을 생각해서 적용하는 게 좋은 상황도 있겠다고 느꼈다. 그리고 해당 구조에서 데이터를 어떤 형태로 저장하는지에 대한 이해도 필요하다는 걸 절실히 느꼈다. Map의 키 값으로 Product를 저장하다 보니 테스트 코드를 작성하면서 올바른 Product가 저장되었는지 확인하는 로직을 구현하기가 어려웠다. 결국 Product 인스턴스의 name값의 Hash를 비교해서 메뉴가 제대로 입력된 건지 확인하는 방식을 사용했다. 나중에 코드를 구현한다면 Key값은 좀 더 검색과 비교가 쉬운 타입을 사용하는 게 좋을 것 같다는 생각이 들었다. - Getter 사용은 무조건적으로 지양해야 하는가?
코드를 구현하면서 Getter를 통해 정보를 가져오고, 해당 정보를 바탕으로 비즈니스 로직을 구현하는 방식 작성하기에는 쉽고 편리하다. 프리코스 이전에는 클래스를 만들면서 자연스럽게 Getter와 Setter부터 만들고 시작한 적도 있었다.
그런데 이 부분은 이번 과정을 하면서 생각이 좀 바뀌게 되었다. 객체는 역할에 따라 자신이 처리할 로직을 가지고 있어야 한다. 하지만 만약 Getter를 무분별하게 사용한다면 해당 객체가 수행해야 할 내용을 다른 객체에서 수행하고 있게 될지도 모른다. 따라서 단순히 필드값을 가져오는 Getter는 지양하는 게 맞다는 생각이 들었다. 정말 꼭 필요한 경우를 제외하고, Getter/Setter를 다른 클래스에서 사용하는 게 맞는지 한번 더 고민해 보는 과정이 필요할 것 같다.
4주라는 시간이 생각보다도 더 빨리 지나갔다. 그리고 나에게도 많은 변화들이 생겼다.
앞으로 어떤 것들을 더 준비해야 할지, 내가 어떤 부분이 부족했는지, 부족한 건 어떻게 채워갈 수 있을지 생각해 볼 수 있는 시간이었다.
오늘로 프리코스는 종료되지만 앞으로 해야 할 것들은 더 많이 생긴 기분이다.
처음에 각 과제를 수행할 때 일주일이면 넉넉하다고 생각했었는데, 어떻게 하면 더 좋은 코드를 만들 수 있을까 고민하고 리팩터링 하다 보니 일주일이라는 시간도 짧게 느껴졌다. 일정 관리의 중요성에 대해서도 생각해 보게 됐다.
예전에는 기술 관련 글을 읽는 게 눈에 잘 들어오지 않았는데 이제는 관련 자료나 새로운 기술에 관한 글을 읽는게 좀 더 편해졌다. 그리고 새로운 내용을 보는 게 재밌게 느껴졌다. 단순히 읽는 것에서 끝나지 않고 '내 코드에 어떻게 적용할 수 있을까?' 고민하면서 보게 되니까 더 집중해서 보게 되는 것 같다.
이번 과정을 수행하면서 프로그래밍에 대한 열정이 다시 한번 샘솟았다. 어제보다 더 발전한 코드, 좀 더 체계적이고 효율적인 코드를 만들기 위해 계속해서 고민할것이다.
한달동안 프리코스를 통해 많은 것들을 느끼고 배울 수 있어서 감사하고 유익한 시간이었다.
참고자료
[AssertJ] Array, List, Map을 쉽게 테스트할 수 있는 예제
[Spring boot] 테스트코드 작성 - Junit을 이용한 Unit test(단위 테스트)
'우아한테크코스 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 백엔드 최종 합격 후기 (0) | 2023.12.27 |
---|---|
[우아한테크코스 6기] 최종 코딩테스트: 비상 근무표 작성 (0) | 2023.12.17 |
[우아한테크코스 6기] 프리코스 3차 과제: 로또(@ParameterizedTest, @CsvSource) (0) | 2023.11.08 |
[우아한테크코스 6기] 프리코스 2차 과제: 레이싱 게임(JUnit5, System.setIn, System.setOut) (0) | 2023.10.31 |
[우아한테크코스] 프리코스 1차 과제: 숫자 야구 (0) | 2023.10.23 |