-
Notifications
You must be signed in to change notification settings - Fork 170
[4기 - 김영주] SpringBoot Part2 Customer CRUD 기능 추가 #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ot-basic into kylekim2123-w2_2
- 실제 Home, Voucher, Customer 메뉴를 각각의 웹페이지라고 가정하고 View를 분리 - Home 메뉴는 HomeView에서 입출력 담당 - Voucher 메뉴는 VoucherView에서 입출력 담당 - Customer 메뉴는 CustomerView에서 입출력 담당
hanjo8813
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
영주님 수고하셨습니다~!
| case UPDATE -> updateVoucher(); | ||
| case DELETE -> deleteVoucher(); | ||
| case QUIT -> quitApplication(); | ||
| case HOME -> isRunning = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 VoucherCommand는 bean으로 등록되고 있어서 isRunning을 한번 변경하면 다시 변경이 불가능한 상태더라구요
(HOME으로 간 후 다시 바우처 메뉴로 진입이 불가능)
bean이 싱글톤으로 동작한다는걸 항상 기억해주세요~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: f5fbd50
- 각 메뉴의 run() 메서드가 실행될 때마다, isRunning을 true로 초기화 하도록 변경했습니다.
- 미처 발견하지 못한 버그였는데, 감사합니다!
| CUSTOMER("2", "고객 메뉴"), | ||
| QUIT("quit", "프로그램 종료"); | ||
|
|
||
| private static final Map<String, HomeMenu> MENUS = Collections.unmodifiableMap(Stream.of(values()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
참조형 static을 제대로 파보셨으니 screaming(upper) snake case 말고 그냥 camel case로 하는게 좋을것 같습니다~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: f2a56a9
- MENUS -> homeMenuMap 과 같은 형식으로 변경했습니다.
- 홈 메뉴 뿐만 아니라, 전반적으로 모두 변경했습니다.
| private void validateIsAlphaNumeric(String nickname) { | ||
| if (isNotAlphaNumeric(nickname)) { | ||
| throw new CustomerInputException(nickname); | ||
| } | ||
| } | ||
|
|
||
| private boolean isNotAlphaNumeric(String nickname) { | ||
| return !nickname.matches(ALPHA_NUMERIC_REGEX); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
두 메소드를 합쳐도 될 것 같아요~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: f2a56a9
- 불필요한 메서드 삭제 후 하나로 합쳤습니다.
- Customer 클래스 뿐만 아니라 Voucher 클래스에 대해서도 동일하게 합쳤습니다.
| private static final Map<String, CustomerType> CUSTOMER_TYPES = Collections.unmodifiableMap(Stream.of(values()) | ||
| .collect(Collectors.toMap(CustomerType::getNumber, Function.identity()))); | ||
|
|
||
| private final String number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 메뉴들에서는 메뉴 숫자에 대한 변수명을 option으로 사용중이니 통일하는게 더 좋을것 같아요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: f2a56a9
- number -> option 으로 변수명 모두 변경했습니다.
| FIX(VoucherTypeInfo.builder() | ||
| .number("1") | ||
| .name("고정 할인") | ||
| .message("\n고정 할인 금액을 입력하세요. (1이상의 자연수, 단위: 원)") | ||
| .condition("1이상의 자연수") | ||
| .unit("원") | ||
| .voucherGenerator(FixDiscountVoucher::new) | ||
| .build() | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이걸 전에 보지 못했는데 builder를 사용하신 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 부분은 멘토님의 직전 PR 리뷰에 대한 반영사항이었습니다.
- VoucherType의 객체 생성 과정에서 생성자의 매개변수가 너무 많아, 이를 별도의 클래스인 VoucherTypeInfo로 만들어 하나로 묶었습니다.
- VoucherTypeInfo를 생성할 때에도 생성자 매개변수가 4개 이상이므로, 여기에 빌더 패턴을 사용하는 것이 효과적이라 생각했습니다.
| public class CustomerInputException extends RuntimeException { | ||
|
|
||
| private static final String INVALID_CUSTOMER_INPUT_MESSAGE = "입력하신 닉네임이 조건(공백이 없는 소문자 알파벳과 숫자 조합)에 맞지 않습니다. 다시 입력해주세요."; | ||
|
|
||
| public CustomerInputException(String nickname) { | ||
| super(format("{0} | 현재 입력 : {1}", INVALID_CUSTOMER_INPUT_MESSAGE, nickname)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재는 각 상황에 해당하는 exception을 새로 만들어주고 있는데..
사실상 이렇게 하면 클래스를 무한생성해야 합니다.
따라서 보통 예외 클래스를 특정 기준으로 상속해서 분리하고, 예외 상황에 대한 enum을 따로 관리하게 됩니다.
- RuntimeException
- BuisnessException
- (요 아래부턴 일단 나중에..)
- Customer~Exception
- Voucher~Exception
- BuisnessException
public enum BusinessRule {
INVALID_VALUE(400, "값 잘못됨~");
private int httpStatusCode; // 다음 과제가 web환경이니 넣었습니다
private String message;
(생략)
}public class BusinessException extends RuntimeException {
private String message;
private int httpStatusCode;
// 가변인자에 대해 알아보세요~!
public BusinessException(BusinessRule rule, Object... args) {
this.message = rule.getMessage();
this.httpStatusCode = rule.getHttpStatusCode();
// 메시지 포맷은 대충 썼습니다.
String resultMessage = message + " | " + args.toString();
super(resultMessage);
}
}public ... 무언가를_검증하는_메소드(int value1, int value2) {
(검증중...)
throw new BusinessException(BusinessRule.INVALID_VALUE, value1, value2);
}이 패턴은 Spring mvc에서 @ControllerAdvice 사용시 자주 쓰는 패턴인데,
이번 리뷰에서 한번 훑어보시고 어떻게 쓰면 좋을지 생각해보시면 좋을것 같습니다!
적용은 web으로 전환하는 미션에서 해주셔도 괜찮습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한마디 거들자면,
예외를 만든다든건 다르게 처리하기 위한 하나의 그룹을 묶는 단위로도 볼수 있는데요.
바퀴를 다시 만들지 말라 라는 이야기가 있듯이 너무 row level의 예외를 만들다 보면 재원멘토가 이야기 한대로 예외가 무한정 많아지죠.
적당히 비슷하게 사용될만한 예외를 만들어 사용하던지, 자바에서 제공하는 예외를 사용하는것도 방법이 될것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: c765c70

- 기존에 무수히 만들었던 커스텀 예외 클래스를 모두 삭제했습니다.
- 대신, 최상위 예외 클래스로
BusinessException을 만들고, 각 메뉴에서 발생할 수 있는 예외들을 자손으로 두었습니다. (사진) - 그리고 각 예외 세부사항에 대해서는 일일히 클래스로 만들지 않고,
BusinessRule이라는 Enum 클래스 내에 정의하여 관리하도록 변경했습니다. - 아직 Web을 적용하지 않아서 status code는 따로 두지 않고, message만 두었습니다.
| customerRepository.findCustomerByNickname(nickname) | ||
| .ifPresent(customer -> { | ||
| throw new ExistedCustomerException(nickname); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
더블체크 좋습니다~~
| private boolean isEmptyDeleteResult(int deleteCounts) { | ||
| return deleteCounts == 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| String menuOption = customerView.readUserInput(); | ||
| CustomerMenu selectedMenu = CustomerMenu.from(menuOption); | ||
| executeMenu(selectedMenu); | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예외는 가급적 하위 레벨의 예외부터 잡는게 좋을것 같아요. 그래야 "예상"한 처리가 될것 같아서요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: c765c70
- 각 메뉴에서 Exception을 catch하던 기존 코드에서, 각 메뉴에서 발생할 수 있는 예외에 대해서만 catch하도록 변경했습니다.
- VoucherCommand에서는 VoucherException에 대한 처리만 수행
- CustomerCommand에서는 CustomerException에 대한 처리만 수행
- HomeCommand에서는 HomeException에 대한 처리만 수행
- 단, HomeCommand에서는 미처 제가 예상하지 못했던 예외가 발생할 여지가 있으므로, 추가적인 catch문을 통해 Exception에 대해서도 처리하도록 하였습니다.
try { String menuOption = homeView.readUserInput(); HomeMenu selectedMenu = HomeMenu.from(menuOption); executeMenu(selectedMenu); } catch (HomeException e) { log.error("홈 메뉴에서 예외 발생 - {} | '{}' | 사용자 입력 : {}", e.getRule(), e.getMessage(), e.getCauseInput(), e); homeView.showExceptionMessage(format("{0} | 현재 입력 : {1}", e.getMessage(), e.getCauseInput())); } catch (Exception e) { log.error("원인 불명의 예외 발생 : '{}'", e.getMessage(), e); homeView.showExceptionMessage("알 수 없는 에러가 발생하였습니다."); quitApplication(); }
| executeMenu(selectedMenu); | ||
| } catch (Exception e) { | ||
| String message = e.getMessage(); | ||
| log.error(message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
습관적으로 예외를 로그로 남길땐 stack trace까지 남기는게 좋을것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: c765c70
- 아래와 같이 스택 트레이스까지 남기도록 수정했습니다. 감사합니다!
08:16:36.366 [main] ERROR c.d.v.command.VoucherCommand -- 할인권 메뉴에서 예외 발생 - VOUCHER_DISCOUNT_AMOUNT_INVALID | '입력하신 수치가 해당 할인권 방식의 조건에 맞지 않습니다.' | 사용자 입력 : -1
com.devcourse.voucherapp.exception.VoucherException: 입력하신 수치가 해당 할인권 방식의 조건에 맞지 않습니다.
at com.devcourse.voucherapp.entity.voucher.FixDiscountVoucher.getValidPrice(FixDiscountVoucher.java:25)
at com.devcourse.voucherapp.entity.voucher.FixDiscountVoucher.<init>(FixDiscountVoucher.java:20)
at com.devcourse.voucherapp.entity.voucher.VoucherType.makeVoucher(VoucherType.java:61)
at com.devcourse.voucherapp.service.VoucherService.create(VoucherService.java:24)
at com.devcourse.voucherapp.controller.VoucherController.create(VoucherController.java:18)
at com.devcourse.voucherapp.command.VoucherCommand.createVoucher(VoucherCommand.java:62)
at com.devcourse.voucherapp.command.VoucherCommand.executeMenu(VoucherCommand.java:47)
at com.devcourse.voucherapp.command.VoucherCommand.run(VoucherCommand.java:36)
at com.devcourse.voucherapp.command.HomeCommand.executeMenu(HomeCommand.java:45)
at com.devcourse.voucherapp.command.HomeCommand.run(HomeCommand.java:31)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:755)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:319)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
at com.devcourse.voucherapp.VoucherappApplication.main(VoucherappApplication.java:10)
| public class CustomerInputException extends RuntimeException { | ||
|
|
||
| private static final String INVALID_CUSTOMER_INPUT_MESSAGE = "입력하신 닉네임이 조건(공백이 없는 소문자 알파벳과 숫자 조합)에 맞지 않습니다. 다시 입력해주세요."; | ||
|
|
||
| public CustomerInputException(String nickname) { | ||
| super(format("{0} | 현재 입력 : {1}", INVALID_CUSTOMER_INPUT_MESSAGE, nickname)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한마디 거들자면,
예외를 만든다든건 다르게 처리하기 위한 하나의 그룹을 묶는 단위로도 볼수 있는데요.
바퀴를 다시 만들지 말라 라는 이야기가 있듯이 너무 row level의 예외를 만들다 보면 재원멘토가 이야기 한대로 예외가 무한정 많아지죠.
적당히 비슷하게 사용될만한 예외를 만들어 사용하던지, 자바에서 제공하는 예외를 사용하는것도 방법이 될것 같습니다.
| public enum ExceptionRule { | ||
| MENU_INVALID("잘못된 메뉴를 선택하셨습니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
항상 한칸 띄워주기!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영: be8f200
- 디테일 감사합니다! 다른 Enum 클래스도 전부 적용했습니다. Google style guide가 Enum은 자동으로 안 띄어 주네요 ㅠ
할인권(Voucher) 관리 애플리케이션 2주차
1️⃣ 프로젝트 설명
할인권(Voucher) 메뉴
생성: 새 고정 금액 할인권 또는 퍼센트 비율 할인권 생성조회: 전체 할인권 목록 조회수정: 할인권의 고정 금액 또는 퍼센트 비율 수치 수정삭제: 특정 할인권 삭제id: UUID 기반의 할인권 고유 번호type: 할인 방식(고정 금액 or 퍼센트 비율)discountAmount: 할인 금액 또는 비율고정 금액 할인(Fix Discount)퍼센트 비율 할인(Percent Discount)고객(Customer) 메뉴
생성: 새 일반 고객 생성조회: 전체 고객 목록 조회, 블랙리스트 고객 목록 조회수정: 고객 타입 수정 (일반 <-> 블랙리스트)삭제: 특정 고객 삭제id: UUID 기반의 고객 고유 번호type: 고객 타입(일반 or 블랙리스트)nickname: 고객 닉네임(중복 x)일반 고객블랙리스트 고객2️⃣ 프로젝트 구조도
3️⃣ 예외처리 사항
홈(Home)
MenuInputException할인권(Voucher)
MenuInputExceptionVoucherTypeInputExceptionDiscountAmountExceptionNotFoundVoucherException고객(Customer)
MenuInputExceptionCustomerInputExceptionExistedCustomerExceptionCustomerTypeInputExceptionNotFoundCustomerException4️⃣ 기능 구현
이전 PR까지의 기능 구현
현재 PR 기능 구현
이후 계획