Skip to content

[4기 - 김영주] SpringBoot Part1 Weekly Mission 제출합니다. (2차 제출)#692

Merged
kylekim2123 merged 27 commits intoprgrms-be-devcourse:kylekim2123from
kylekim2123:kylekim2123
Jul 3, 2023
Merged

[4기 - 김영주] SpringBoot Part1 Weekly Mission 제출합니다. (2차 제출)#692
kylekim2123 merged 27 commits intoprgrms-be-devcourse:kylekim2123from
kylekim2123:kylekim2123

Conversation

@kylekim2123
Copy link
Member

@kylekim2123 kylekim2123 commented Jun 26, 2023

1️⃣ 요구사항 정리

1. 프로그램 시작(랜딩 화면)

사용자는 숫자를 입력해서 원하는 메뉴를 선택할 수 있다.
1. 새 할인권 생성
2. 할인권 조회
3. 프로그램 종료

2. 새 할인권 생성

사용자는 숫자를 입력해서 원하는 할인 방식을 선택할 수 있다.
할인권 생성이 완료되면 생성된 할인권의 UUID, 할인 방식, 금액/비율 내용을 보여준다.

1. 고정 할인권 생성 : 사용자는 1이상의 자연수를 입력하여, 고정 할인 금액을 설정할 수 있다.
2. 비율 할인권 생성 : 사용자는 1이상 100이하의 자연수를 입력하여, 비율 할인 퍼센트를 설정할 수 있다.

3. 할인권 조회

저장된 할인권들의 UUID, 할인 방식, 금액/비율 목록을 조회한다.
ex) 550e8400-e29b-41d4-a716-446655440000 | 비율 할인 | 40%

4. 프로그램 종료

실행 중인 프로그램을 종료한다.

2️⃣ 프로그램 구조도

image


3️⃣ 기능 구현 및 실행 화면

기능 구현

  • 메뉴 선택 화면
  • 메뉴 입력 및 예외처리
  • 프로그램 종료
  • 고정 할인권 생성
  • 비율 할인권 생성
  • 할인권 조회
  • logback 이용한 로그 파일 생성
  • 실행 가능한 jar 파일 생성

실행 화면

[할인권 프로그램 v1.0]
1. 새 할인권 생성
2. 할인권 조회
3. 프로그램 종료

입력 : 1

할인 방식을 선택하세요.
1. 고정 할인
2. 비율 할인
입력 : 1

고정 할인 금액을 입력하세요.
1이상의 자연수만 입력하세요. 단위는 원입니다.
입력 : 1000

할인권 생성이 완료되었습니다.
0437e68e-7fc1-4136-9090-e8a4c3f50ed1 | 고정 할인 | 1,000원

[할인권 프로그램 v1.0]
1. 새 할인권 생성
2. 할인권 조회
3. 프로그램 종료

입력 : 1

할인 방식을 선택하세요.
1. 고정 할인
2. 비율 할인
입력 : 2

비율 할인 퍼센트를 입력하세요.
1이상 100이하의 자연수만 입력하세요. 단위는 %입니다.
입력 : 40

할인권 생성이 완료되었습니다.
71a70469-3e03-49a6-ad53-e8ec37531dce | 비율 할인 | 40%

[할인권 프로그램 v1.0]
1. 새 할인권 생성
2. 할인권 조회
3. 프로그램 종료

입력 : 2

현재까지 생성된 할인권 목록입니다.
71a70469-3e03-49a6-ad53-e8ec37531dce | 비율 할인 | 40%
0437e68e-7fc1-4136-9090-e8a4c3f50ed1 | 고정 할인 | 1,000원

[할인권 프로그램 v1.0]
1. 새 할인권 생성
2. 할인권 조회
3. 프로그램 종료

입력 : 3

프로그램을 종료합니다.

Copy link

@hanjo8813 hanjo8813 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

영주님 안녕하세요~!
과제하느라 수고 많으셨습니다 ㅎㅎ
리뷰 확인하시고 리팩토링 후 코멘트 달아주세요~~


[Enum 객체가 너무 무거운지에 대한 고민이 있습니다.]

질문을 못보고 리뷰를 먼저 달았는데, 영주님께서 먼저 고민을 하셨었군요!
이 내용은 리뷰 참고해주시면 감사하겠습니다~

Service 객체가 너무 아무것도 안하는 것 같고 (...)

현재는 요구사항이 단순해서 그렇습니다.
service 레이어는 비즈니스 요구사항에 맞춰 데이터를 가공하는 역할을 하기 때문에 현재로도 충분합니다!


@Controller
@RequiredArgsConstructor
public class VoucherController implements CommandLineRunner {
Copy link

@hanjo8813 hanjo8813 Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View와 Controller 로직이 함께 있는게 조금 어색합니다.
웹 환경에서의 MVC 패턴이라고 생각하면 View는 화면, 즉 html이 되겠죠?
그리고 컨트롤러는 해당 화면에 필요한 model을 던져주는 역할만 합니다.

바우처 앱에서는 html이 콘솔을 띄워주는 코드라고 생각해보면 됩니다.
CommandLineRunner를 상속한 ConsoleView(?) 클래스를 만들어서 분리하면 될 것 같네요.

(대공사가 예상되네요)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 기존
    • 컨트롤러 내에서 뷰 로직을 호출하여 객체의 책임 분리가 명확하게 되지 못했습니다.
  • 변경
    • 컨트롤러에서는 뷰로 전달되는 데이터만 반환할 수 있도록 간단한 구조로 변경하였습니다.
    • 컨트롤러가 아닌 CommandLineApplication이라는 별도의 클래스를 만들어 CommandLineRunner를 구현하도록 했습니다. 그래서 run() 메서드가 자동적으로 실행되도록 했습니다.
    • 그러면 또 CommandLineApplication에서 뷰 로직을 호출하는 코드가 많아질 것 같아서, 하나의 레이어를 더 두었습니다.
    • ViewManager라는 클래스는 일종의 파사드 클래스로써, inputView와 outputView 구현체의 메서드들을 조합하여 입출력에 필요한 기능을 가지고 있습니다. CommandLineApplication은 입출력 기능을 사용할 때 구현체 메서드를 직접 호출하지 않고 ViewManager의 메서드를 호출하게 됩니다.

사진도 같이 첨부하겠습니다.
image


public static Menu getMenu(String menuNumber) {
return Arrays.stream(Menu.values())
.filter(menu -> menuNumber.equals(menu.getNumber()))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

equals 순서에 주의해주세요.
여기서 파라미터인 menuNumber가 null이라면 NPE가 발생합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: a08dcee

  • stream을 통해 Enum 객체를 찾는 방식에서, Map을 이용한 방식으로 변경하면서 자연스럽게 삭제되었습니다.
  • 하지만, null이 발생할 수 있는 문자열 객체에 대해서는 메서드 호출을 할 때 주의해야 한다는 점을 상기할 수 있었습니다. 감사합니다.

Comment on lines 16 to 18
} catch (Exception e) {
LOGGER.error(e.getMessage());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외를 한번에 처리하시는 부분 좋습니다 👍
다만 예외를 catch하는 부분을 while문 쪽으로 이동시키면 어플리케이션을 종료시키지 않고 예외처리를 할 수 있을것 같습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 약간 다른 생각이긴 한데요 ㅎ
이렇게 최상위 Exception 으로 잡는 경우 각 예외마다 처리하는 방식이 다를때 처리가 애매해질것 같긴 하더라고요.
그래서 일반적으로 spring에서는 ExceptionHandler 와 ControllerAdvice 로 활용하곤 하죠.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 98dae5e

  • 기존
    • 가장 처음 실행되는 VoucherappApplication에 try~catch를 두어 예외처리를 한번에 했습니다.
    • 예외가 발생하면 프로그램이 종료되었습니다.
  • 변경
    • CommandLineRunner를 구현하는 별도의 클래스의 run() 메서드 내에 있는 while 문에 try~catch를 두었습니다.
    • 예외가 발생하면 프로그램이 종료되지 않고 재시작을 할 수 있도록 변경했습니다.
    • 다만 아직은 각 예외마다 처리하는 방식을 다르게 두지 않아서, 현재 try~catch는 예외 처리라기 보다는 단순히 예외에 대한 로그 작성 및 출력의 역할인 것 같습니다. 추후에 예외마다 처리 방식을 다르게 두어야 할 때가 온다면 멘토님께서 말씀하신 부분에 대해 공부해서 적용해보도록 하겠습니다. 감사합니다!

Comment on lines 26 to 31
public static Menu getMenu(String menuNumber) {
return Arrays.stream(Menu.values())
.filter(menu -> menuNumber.equals(menu.getNumber()))
.findFirst()
.orElseThrow(() -> new MenuInputException(NOT_EXIST_MENU_MESSAGE));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

창현님 리뷰에도 있어서 복붙합니다.

이 코드의 경우엔 enum 숫자만큼 반복문을 돌아야합니다. O(n)
따라서 values를 map 캐싱(?)한 필드를 추가해 사용하는 방법이 있습니다.
참고

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: a08dcee

  • 기존
    • 매 메서드 호출마다 values()를 호출하여 Enum 객체 배열의 사본을 계속 생성하는 형태였습니다. 비효율적입니다.
  • 변경
    • 수정 불가능한 unmodifiableMap으로 Enum 객체의 맵을 만들어서, 시간복잡도 O(1) 내로 객체 조회가 가능하도록 변경했습니다.
    • 이 과정에서 stream API의 collect를 사용하여 key는 number(메뉴 혹은 할인권 타입의 번호)로, value는 Enum 객체로 하는 맵을 생성했습니다.

execute(menu);
}

outputView.showQuitMessage();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if문 안으로 들어가도 괜찮을 것 같습니다.

if (menu.isQuit()) {
    outputView.showQuitMessage();
    return;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 뷰와 컨트롤러를 분리하는 과정에서 switch 문으로 메뉴에 따라 분기를 하게 되어서, 자연스럽게 해당 코드는 삭제되었습니다.
  • switch 문 내에 현재 메뉴가 QUIT에 해당하는 경우, while문을 더이상 반복하지 않도록 boolean 값을 false로 바꾸는 방향으로 변경했습니다.

@@ -0,0 +1,8 @@
package com.devcourse.voucherapp.exception;

public class MenuInputException extends RuntimeException {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 예외를 직접 만드셨군요! 좋습니다 👍

커스텀 예외를 만든김에 필드값까지 추가로 활용해보는건 어떨까요?
예외처리할 때 어떤 입력값 때문에 예외가 발생했는지 등의 '원인'을 포함시키는게 좋습니다.

해당 원인을 담는 필드를 추가하고, 예외처리하는곳에서 꺼내서 로깅하도록 시도해보세요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 4cbac60

  • 기존
    • 커스텀 예외 내부에 원인과 관련된 별다른 로직이 없었습니다.
    • 예외 메시지는 커스텀 예외 내부가 아닌, 예외를 throw하는 외부에서 상수를 통해 매개변수로 넘겨줬습니다.
  • 변경
    • 외부에서 throw를 하여 예외를 발생시킬 때, 예외의 원인인 "사용자의 입력값"을 매개변수로 넘기도록 변경했습니다.
    • 예외 메시지는 커스텀 예외 내부에 상수 필드로 선언하고, 예외 원인과 함께 출력되도록 하였습니다.

@SpringBootApplication
public class VoucherappApplication {

private static final Logger LOGGER = LoggerFactory.getLogger(VoucherappApplication.class);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

롬복 어노테이션 중 @Slf4j를 사용하면 이렇게 생성하지 않아도 됩니다~
클래스 위에 달면 돼요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 98dae5e

  • 저는 생성자와 게터, 세터 부분에 대해서만 알고 있었는데, 이런 편리한 기능도 있었군요! 너무 감사합니다.

Comment on lines 65 to 73
if (voucherType.isFix()) {
outputView.showFixDiscountPriceInputMessage();

return;
}

if (voucherType.isPercent()) {
outputView.showPercentDiscountRateInputMessage();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 경우 early return을 사용하는 것보다 오히려 else를 사용하는 게 더 자연스러운 것 같아요.
그보다 switch 표현식을 사용하는 게 더 좋을것 같습니다.
enum 클래스의 is~ 메소드도 제거할 수 있구요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 기존 if문에서 switch 문으로 변경했습니다. 아직 다른 메뉴가 없음에도 불구하고, 다른 메뉴가 생길 수 있다는 점을 고려하여 불필요한 코드를 삽입한 것 같습니다. 감사합니다.

Comment on lines 33 to 43
public boolean isCreate() {
return this == Menu.CREATE;
}

public boolean isList() {
return this == Menu.LIST;
}

public boolean isQuit() {
return this == Menu.QUIT;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무지성 getter를 사용하지 않고 의미있는 메소드를 생성하신 점 좋네요~ 👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메소드 생성 측면에서는 좋긴 하지만 이 부분은 코드만 봐도 충분히 이해가 될 정도일것 같아서 바로 처리해도 무방할것 같아요.

Menu menu = getMenu();
if(menu.isCreate()){
  // ...
}
Menu menu = getMenu();
if(menu == Menu.CREATE){ // 또는 switch
  // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 기존 if문에서 switch 문으로 변경하면서, isXXX() 형태의 메서드들을 모두 삭제하였습니다.

Comment on lines +9 to +10
@Component
public class ConsoleOutputView implements OutputView {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

콘솔 관련 클래스를 Bean으로 등록하신 이유가 궁금합니다.

컨트롤러 리뷰에 언급했듯, 우선 구조적 분리가 필요할 것 같구요,
이후 콘솔의 책임은 유틸성 작업이 대부분이라서 static하게 관리해도 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 내용 관련해서는 서브멘토님과 의견을 나눠보고 싶습니다!

일단 저는 콘솔 입출력 클래스를 static이 아닌, interface로 만든 후 구현체를 구현하고 있습니다. 이는 콘솔 입출력이 System.in / out 이 아닌 text.io와 같은 외부 라이브러리나 파일 입출력 방식을 이용할 수도 있을 것 같아서 확장성을 고려했기 때문에 그렇습니다.

그래서 다형성을 통해 외부로부터 입출력 클래스에 대한 의존성을 주입받기 위해서 Bean으로 등록했었습니다.
이후 컨트롤러와 뷰를 구조적으로 분리한 경우에도 이 논리는 동일하게 적용되었습니다.

run() 메서드가 실행되는 CommandLineApplication에서 입출력 기능을 사용할 때, inputView와 outputView의 구현체의 메서드를 직접 호출하는 형태가 아니라 ViewManager와 같은 파사드 클래스를 거쳐서 입출력 기능을 사용하도록 되어 있습니다.
따라서 ViewManager에는 inputView, outputView의 구현체가 생성자를 통해서 주입되도록 했기 때문에, 스프링 컨테이너에 빈으로 등록해서 사용하게 되었습니다.

하지만 현재 프로젝트에서는 사실 System in, out 말고 다른 입출력은 사용하지 않을 것 같아서, 말씀해주신대로 static으로 하면 파일도 줄어들도 더 깔끔할 것 같다는 생각이 듭니다. 서브멘토님의 생각은 어떠신가요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 구조 괜찮은 것 같습니다 👍 ㅎㅎ
확장성을 고려한 설계라는 점에서 충분히 설득력이 있는것 같습니다!

Comment on lines 16 to 18
} catch (Exception e) {
LOGGER.error(e.getMessage());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 약간 다른 생각이긴 한데요 ㅎ
이렇게 최상위 Exception 으로 잡는 경우 각 예외마다 처리하는 방식이 다를때 처리가 애매해질것 같긴 하더라고요.
그래서 일반적으로 spring에서는 ExceptionHandler 와 ControllerAdvice 로 활용하곤 하죠.

outputView.showQuitMessage();
}

private void execute(Menu menu) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메소드의 이름은 최대한 가독성 있게! 풀어서 작성하는게 좋을것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 사용자의 메뉴 입력에 따라 각각의 메뉴를 실행하는 역할의 메서드라서, "메뉴를 실행한다." 라는 의미로 executeMenu(Menu selectedMenu) 라는 이름으로 변경해봤습니다.
  • 그런데 매개변수에 이미 selectedMenu 라는 이름이 되어 있어서, 의미가 중복되는 것은 아닌지 고민이 되는 것 같습니다.

Comment on lines 33 to 43
public boolean isCreate() {
return this == Menu.CREATE;
}

public boolean isList() {
return this == Menu.LIST;
}

public boolean isQuit() {
return this == Menu.QUIT;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메소드 생성 측면에서는 좋긴 하지만 이 부분은 코드만 봐도 충분히 이해가 될 정도일것 같아서 바로 처리해도 무방할것 같아요.

Menu menu = getMenu();
if(menu.isCreate()){
  // ...
}
Menu menu = getMenu();
if(menu == Menu.CREATE){ // 또는 switch
  // ...
}

import java.util.UUID;
import lombok.Getter;

public enum VoucherType {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 동의합니다. 최대한 나누는게 중요해 보여요.


class MenuTest {

@DisplayName("존재하는 메뉴를 입력했을 때, 해당 메뉴 객체가 반환되는지 테스트")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트의 네이밍은 상황,실행,기대값의 조합으로 가급적 문장형이 되는게 좋습니다.

Suggested change
@DisplayName("존재하는 메뉴를 입력했을 때, 해당 메뉴 객체가 반환되는지 테스트")
@DisplayName("존재하는 메뉴를 입력했을 때, 해당 메뉴 객체가 반환된다.")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 01a8199

  • 말씀해주신대로 문장형으로 작성되도록 변경하였습니다. 감사합니다!

- CommandLineRunner를 컨트롤러에서 구현하지 않고, 별도의 클래스를 만들어서 구현
- Input, Output을 하나의 ConsoleView에 포함시켜 입출력 책임만 가지는 클래스를 별도로 구현
- 기존 Enum 클래스가 너무 많은 책임을 가지므로 각 객체로 책임을 이관
- 인스턴스 변수의 할당과 예외처리를 생성자에서 진행하도록 변경
- Enum의 values() 메서드는 매 호출마다 사본을 반환하기 때문에 비효율적
- Map을 통해 미리 메모리에 상수로 두고 O(1) 시간 복잡도로 조회하는 방식으로 개선
- 예외 메시지 출력을 기존 로거를 이용한 방식에서 단순 콘솔 출력으로 변경
- ERROR 로그 레벨에 대해서만 파일에 로그 기록
- 서비스에서 list 대신 findAllVouchers로 메서드명 변경
- 테스트코드에서 상황, 실행, 기대값에 맞게 문장형으로 변경
- 입출력 클래스명을 책임에 적합하게 변경
@kylekim2123 kylekim2123 changed the title [4기 - 김영주] SpringBoot Part1 Weekly Mission 제출합니다. (1차 제출) [4기 - 김영주] SpringBoot Part1 Weekly Mission 제출합니다. (2차 제출) Jun 28, 2023
Copy link
Member Author

@kylekim2123 kylekim2123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링부트 1주차 미션에 대한 1차 피드백을 반영하여 2차 제출을 하였습니다.
멘토님께서 말씀하신대로 일단 이번 주 과제까지는 현재 PR에 연속적으로 올리도록 하겠습니다.
각 리뷰마다 모두 반영 사항과 의견에 대해 코멘트를 달았습니다. 시간 나실 때 천천히 확인 부탁드립니다.
항상 정말 감사드립니다.

import java.util.UUID;
import lombok.Getter;

public enum VoucherType {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 6a76ecb

  • 기존
    • VoucherType이라는 하나의 Enum 클래스 안에서 객체 조회, 생성, 예외처리의 여러 책임을 한꺼번에 가지고 있었습니다.
  • 변경
    • Enum 클래스에서 객체 생성과 예외처리를 하지 않고, 각각의 엔티티의 생성자에서 할 수 있도록 변경하였습니다.

@@ -0,0 +1,8 @@
package com.devcourse.voucherapp.exception;

public class MenuInputException extends RuntimeException {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 4cbac60

  • 기존
    • 커스텀 예외 내부에 원인과 관련된 별다른 로직이 없었습니다.
    • 예외 메시지는 커스텀 예외 내부가 아닌, 예외를 throw하는 외부에서 상수를 통해 매개변수로 넘겨줬습니다.
  • 변경
    • 외부에서 throw를 하여 예외를 발생시킬 때, 예외의 원인인 "사용자의 입력값"을 매개변수로 넘기도록 변경했습니다.
    • 예외 메시지는 커스텀 예외 내부에 상수 필드로 선언하고, 예외 원인과 함께 출력되도록 하였습니다.

Comment on lines +9 to +10
@Component
public class ConsoleOutputView implements OutputView {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 내용 관련해서는 서브멘토님과 의견을 나눠보고 싶습니다!

일단 저는 콘솔 입출력 클래스를 static이 아닌, interface로 만든 후 구현체를 구현하고 있습니다. 이는 콘솔 입출력이 System.in / out 이 아닌 text.io와 같은 외부 라이브러리나 파일 입출력 방식을 이용할 수도 있을 것 같아서 확장성을 고려했기 때문에 그렇습니다.

그래서 다형성을 통해 외부로부터 입출력 클래스에 대한 의존성을 주입받기 위해서 Bean으로 등록했었습니다.
이후 컨트롤러와 뷰를 구조적으로 분리한 경우에도 이 논리는 동일하게 적용되었습니다.

run() 메서드가 실행되는 CommandLineApplication에서 입출력 기능을 사용할 때, inputView와 outputView의 구현체의 메서드를 직접 호출하는 형태가 아니라 ViewManager와 같은 파사드 클래스를 거쳐서 입출력 기능을 사용하도록 되어 있습니다.
따라서 ViewManager에는 inputView, outputView의 구현체가 생성자를 통해서 주입되도록 했기 때문에, 스프링 컨테이너에 빈으로 등록해서 사용하게 되었습니다.

하지만 현재 프로젝트에서는 사실 System in, out 말고 다른 입출력은 사용하지 않을 것 같아서, 말씀해주신대로 static으로 하면 파일도 줄어들도 더 깔끔할 것 같다는 생각이 듭니다. 서브멘토님의 생각은 어떠신가요?


class MenuTest {

@DisplayName("존재하는 메뉴를 입력했을 때, 해당 메뉴 객체가 반환되는지 테스트")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 01a8199

  • 말씀해주신대로 문장형으로 작성되도록 변경하였습니다. 감사합니다!

Comment on lines 22 to 24
public Collection<Voucher> list() {
return voucherRepository.findAllVouchers();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 01a8199

  • 말씀해주신대로 list() -> findAllVouchers() 로 변경하였습니다. 감사합니다!

outputView.showQuitMessage();
}

private void execute(Menu menu) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 사용자의 메뉴 입력에 따라 각각의 메뉴를 실행하는 역할의 메서드라서, "메뉴를 실행한다." 라는 의미로 executeMenu(Menu selectedMenu) 라는 이름으로 변경해봤습니다.
  • 그런데 매개변수에 이미 selectedMenu 라는 이름이 되어 있어서, 의미가 중복되는 것은 아닌지 고민이 되는 것 같습니다.

execute(menu);
}

outputView.showQuitMessage();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 뷰와 컨트롤러를 분리하는 과정에서 switch 문으로 메뉴에 따라 분기를 하게 되어서, 자연스럽게 해당 코드는 삭제되었습니다.
  • switch 문 내에 현재 메뉴가 QUIT에 해당하는 경우, while문을 더이상 반복하지 않도록 boolean 값을 false로 바꾸는 방향으로 변경했습니다.


@Controller
@RequiredArgsConstructor
public class VoucherController implements CommandLineRunner {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: fa6cab8

  • 기존
    • 컨트롤러 내에서 뷰 로직을 호출하여 객체의 책임 분리가 명확하게 되지 못했습니다.
  • 변경
    • 컨트롤러에서는 뷰로 전달되는 데이터만 반환할 수 있도록 간단한 구조로 변경하였습니다.
    • 컨트롤러가 아닌 CommandLineApplication이라는 별도의 클래스를 만들어 CommandLineRunner를 구현하도록 했습니다. 그래서 run() 메서드가 자동적으로 실행되도록 했습니다.
    • 그러면 또 CommandLineApplication에서 뷰 로직을 호출하는 코드가 많아질 것 같아서, 하나의 레이어를 더 두었습니다.
    • ViewManager라는 클래스는 일종의 파사드 클래스로써, inputView와 outputView 구현체의 메서드들을 조합하여 입출력에 필요한 기능을 가지고 있습니다. CommandLineApplication은 입출력 기능을 사용할 때 구현체 메서드를 직접 호출하지 않고 ViewManager의 메서드를 호출하게 됩니다.

사진도 같이 첨부하겠습니다.
image

Comment on lines 16 to 18
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 98dae5e

  • 기존
    • 가장 처음 실행되는 VoucherappApplication에 try~catch를 두어 예외처리를 한번에 했습니다.
    • 예외가 발생하면 프로그램이 종료되었습니다.
  • 변경
    • CommandLineRunner를 구현하는 별도의 클래스의 run() 메서드 내에 있는 while 문에 try~catch를 두었습니다.
    • 예외가 발생하면 프로그램이 종료되지 않고 재시작을 할 수 있도록 변경했습니다.
    • 다만 아직은 각 예외마다 처리하는 방식을 다르게 두지 않아서, 현재 try~catch는 예외 처리라기 보다는 단순히 예외에 대한 로그 작성 및 출력의 역할인 것 같습니다. 추후에 예외마다 처리 방식을 다르게 두어야 할 때가 온다면 멘토님께서 말씀하신 부분에 대해 공부해서 적용해보도록 하겠습니다. 감사합니다!

@SpringBootApplication
public class VoucherappApplication {

private static final Logger LOGGER = LoggerFactory.getLogger(VoucherappApplication.class);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 98dae5e

  • 저는 생성자와 게터, 세터 부분에 대해서만 알고 있었는데, 이런 편리한 기능도 있었군요! 너무 감사합니다.

Copy link

@hanjo8813 hanjo8813 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 훑어보니 제가 더 리뷰할 부분은 없는것 같습니다. (너무 깔끔한데요;;)
시간 남으시면 추가 요구사항까지 빠르게 구현해보시고, 이번주 안으로 리뷰 끝내봐요~~

- 할인권 생성 순서대로 저장하기 위해 LinkedHashMap으로 변경
- 전체 할인권 목록의 수정 방지를 위해 복사본으로 반환
@kylekim2123
Copy link
Member Author

@hanjo8813

반영: cad1d0f

다른 팀원들의 리뷰를 참고하여, 추가 변경 사항을 반영하였습니다.

  • 할인권 목록의 순서 보장을 위해 HashMap에서 LinkedHashMap으로 저장소 타입을 변경하였습니다.
  • findAllVouchers()가 반환하는 할인권 목록의 수정 방지를 위해 List.copyOf()를 통해 원본이 아닌 복사본을 반환하도록 변경하였습니다.

그리고 1주차 미션의 심화 기능에 대해서는, 2주차 미션에 반영해서 추후에 PR 전송하도록 하겠습니다!
File IO를 이용한 블랙리스트 대신, DB를 통해서 Customer와 블랙리스트를 관리하고 yaml 프로퍼티 이용해서 프로필도 구분 해보겠습니다.
감사합니다. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants