Skip to content
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

[5기 - 김현우] Spring Boot Part3 Weekly Mission 제출합니다. #972

Open
wants to merge 73 commits into
base: ASak1104-hwkim-week3
Choose a base branch
from

Conversation

ASak1104
Copy link

@ASak1104 ASak1104 commented Nov 5, 2023

👩‍💻 요구 사항

바우처 관리 Command-line Application

(기본) 서비스 관리페이지 개발하기

  • Spring MVC를 적용해서 thymeleaf 템플릿을 설정해보세요.
  • 커맨드로 지원했던 기능을 thymeleaf를 이용 해서 관리페이지를 만들고 다음 기능을 지원가능하게 해보세요
    • 조회페이지
    • 상세페이지
    • 입력페이지
    • 삭제기능

(기본) 바우처 서비스의 API 개발하기

  • Spring MVC를 적용해서 JSON과 XML을 지원하는 REST API를 개발해보세요
    • 전체 조회기능
    • 조건별 조회기능 (바우처 생성기간 및 특정 할인타입별)
    • 바우처 추가기능
    • 바우처 삭제기능
    • 바우처 아이디로 조회 기능

🚀 애플리케이션 실행

프로젝트 초기 설정 및 애플리케이션 실행은 README.md 파일을 참고해주세요!

💡 고민했던 점

  • 새로운 요구 사항으로 인해 모델과 인터페이스의 변경이 불가피했던 점
    • 바우처 모델에 createAt 필드가 추가
    • 바우처 레포지토리에 다양한 기능 추가
    • 이러한 이유로 바우처 추상 클래스와 레포지토리 인터페이스를 사용하는 모든 클래스에 변경이 연쇄적으로 발생함
    • 각 주차 별 과제에서 요구사항이 다이나믹하게 변경되다보니 특히 3주차 과제에서 변경 사항에 유연하게 대응하지 못 한 것 같습니다..
    • 그 과정에서 정말 많은 것을 고민하고 경험할 수 있었습니다.
      • 인터페이스의 변경으로 그 구현체가 모두 해당 기능을 구현해야 한다.
      • 과연 웹에서만 사용할 기능을 위해 인테페이스를 변경할 필요가 있을까?
      • 이러한 변경이 초기 설계가 문제였던 것일까? 아니면 요구사항이 너무 급진적이었던 것일까?
      • 자세한 테스트 코드를 작성하기 위해 기능적인 구현이 늦춰지는 것이 올바른 것일까? 간단한 테스트만 작성하고 디테일한 부분을 추가하는게 좋다고 생각하지만 실제 코드를 작성하다 보면 설계나 메서드에 대한 방향성이 바뀌는 경우가 많아서 구현 후에 엣지 케이스를 테스트로 작성한는 것이 저와 잘 맞는 것 같다는 생각이 들었습니다.
      • 실무에서 이렇게 한다. 통상적으로 많이 사용된다. (ex. 보통 이 어노테이션을 사용한다. 이렇게 하면 무조건 좋다) 등의 이유로 모든 부분에서 디테일한 부분을 찾아보고 코드를 작성하는 것이 올바른 설계일까? 오버 엔지니어링은 아닐까? 일단 기능을 구현하고 개선점이 생겼을 때 수정하는 것이 좋지 않을까?
      • etc.
    • 그래서 제가 과제를 진행하면서 고민했던 내용들을 멘토님과 함께 이야기해보면 좋을 것 같다는 생각을 했습니다.
  • 유틸 클래스와 같이 static 필드로 구성된 클래스의 테스트가 어려웠던 점
    • 해당 주제는 RBF에서 다룬 내용입니다.
    • 콘솔 유틸 클래스가 static final 필드로 BufferedReader를 갖고 있기 때문에 테스트 시 각 메서드마다 System.setIn()으로 새로운 입력을 지정해줄 수가 없어서 결국 테스트를 위해 해당 클래스의 구현을 변경했습니다.
    • 테스트를 위한 기능 구현의 변경이지 않냐? 라는 질문에 그렇다! 라고 할 수 밖에 없지만 추후 콘솔 애플리케이션의 테스트를 위해선 지금 구현 방법을 수정하는 것을 선택했습니다.
    • 멘토님과 이에 대해 이야기 해보면 좋을 것 같습니다.
  • XStreamMarshall가 왜 안되는 걸까?
    • 크게 중요한 내용은 아닙니다!
    • 강의에서 해당 객체를 사용해서 application/xml 데이터를 반환하는데
    • 제가 실제로 적용해보려고 정말 많은 시간을 사용했지만 결국 실패했습니다.
    • 결국 다른 방식으로 구현했습니다.
  • SpringBootApplication은 상위 패키지를 componentScan 하는 것이 왜 안될까?
    • 해당 내용도 RBF에서 다룬 내용입니다.
    • 3주차 과제에서 사용자 입력에 따라 Console과 Web 애플리케이션을 실행하고 싶다
      • 각자 사용하는 Bean은 하위 패키지에 구현하고
      • 공통으로 사용하는 Bean을 ComponentScan으로 가져오면 되지 않을까?
    • main 메서드에서 사용자 입력을 받고 원하는 SpringBootApplication을 실행해보자
      • @SpringBootApplication 의 경우 기본적으로 자신을 포함한 하위 패키지에 대한 ComponentScan을 하기 때문에 상위 디렉토리를 basePackage를 설정해도 빈 등록이 안되는 문제
    • main 메서드를 갖고 있는 상위 클래스가 컴포넌트 스캔을 하고 하위 스프링 애플리케이션이 해당 클래스를 import 방식으로 해결
      • 이렇게 하다보니 SpringBootTest에서 Bean Definition과 같은 문제가 발생
      • 하위 스프링부트 애플리케이션에서만 사용하는 빈들을 스프링부트테스트에서 모두 로드하기 때문에 특정 레포지토리가 겹치는 것과 같은 문제
      • 각 컴포넌트에 대해 프로파일 같은 것을 지정해주면 되지만 이는 테스트를 위해 프로덕션에서 불필요한 코드가 추가되는 것 같다고 생각했습니다.
    • 멘토님과 같이 코드를 보면서 이야기해보면 좋을 것 같습니다.

@ASak1104 ASak1104 changed the title [5기 - 김현우] Spring Boot Part3 Weekly Mission 제출합니다. (내용 작성 중) [5기 - 김현우] Spring Boot Part3 Weekly Mission 제출합니다. Nov 10, 2023
Comment on lines 27 to 28
case "0" -> ConsoleApplication.main(args);
case "1" -> WebApplication.main(args);
Copy link
Contributor

Choose a reason for hiding this comment

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

상수화로 enum같은걸로 해두면 클린코드 적으로 좋을거 같습니다.

Copy link
Author

@ASak1104 ASak1104 Nov 14, 2023

Choose a reason for hiding this comment

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

실행할 스프링 애플리케이션의 main 메서드를 enum에서 Consumer로 관리하는 방향으로 수정해봤습니다!

commit 7412a89

.map(UUID::toString)
.anyMatch(id -> Objects.equals(id, voucherId));
public boolean hasVoucher(UUID voucherId) {
Optional<Voucher> optionalVoucher = voucherRepository.findById(voucherId);
Copy link
Contributor

Choose a reason for hiding this comment

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

EXISTQuery랑 이거랑 어떤게 좋을지 한번 고민해보세요!

Copy link
Author

Choose a reason for hiding this comment

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

좋은 고민거리인 것 같습니다!

Comment on lines 116 to 117
Map<String, Object> customerFields = new HashMap<>() {
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Map<String, Object> customerFields = new HashMap<>() {
};
Map<String, Object> customerFields = new HashMap<>()

Copy link
Author

Choose a reason for hiding this comment

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

commit 81d6f1e

}

@GetMapping("/{id}")
public String viewCustomerById(@PathVariable("id") UUID id, Model model) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public String viewCustomerById(@PathVariable("id") UUID id, Model model) {
public String viewCustomerById(@PathVariable UUID id, Model model) {

생략 가능합니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

commit 649f7ff

import team.marco.voucher_management_system.web_app.controller.CustomerController;

@ControllerAdvice(basePackageClasses = CustomerController.class)
public class ViewAdvice {
Copy link
Contributor

Choose a reason for hiding this comment

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

500에러 같은 경우에 방어 해놔도 좋습니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

500 에러는 아래 conversation에 있는 VoucherErrorController에서 처리하는 걸 의도해서 우선 다른 부분을 반영해보겠습니다!

import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class VoucherErrorController implements ErrorController {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

public ResponseEntity<Voucher> create(CreateVoucherRequest createVoucherRequest) {
Voucher voucher = voucherService.create(createVoucherRequest);

return ResponseEntity.ok(voucher);
Copy link
Contributor

Choose a reason for hiding this comment

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

REST API
리소스 생성될때 201 status created를 내려주는데 현재는 200이 내려가네요!

Copy link
Author

Choose a reason for hiding this comment

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

api 명세 규칙을 제대로 지키질 못했네요
감사합니다!

commit 51bfeea

Comment on lines 60 to 66
if (!isDeleted) {
return ResponseEntity.noContent()
.build();
}

return ResponseEntity.ok()
.build();
Copy link
Contributor

Choose a reason for hiding this comment

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

  • 삭제가 안되었다면 에러가 아닐까요?

Copy link
Author

Choose a reason for hiding this comment

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

commit f077d7d

Comment on lines 69 to 82
@GetMapping("/createdAt")
public List<Voucher> findByCreateAt(@RequestParam("from")
@DateTimeFormat(pattern = DATE_PATTERN)
LocalDateTime from,
@RequestParam("to")
@DateTimeFormat(pattern = DATE_PATTERN)
LocalDateTime to) {
return voucherService.findByCreateAt(from, to);
}

@GetMapping("/type/{type}")
public List<Voucher> findByType(@PathVariable("type") VoucherType voucherType) {
return voucherService.findByType(voucherType);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

filering같은 경우엔 queryString으로 필터링을 하면 좋을거 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

commit f949bd9

@@ -0,0 +1,6 @@
package team.marco.voucher_management_system.web_app.dto;

public record CreateCustomerRequest(
Copy link
Contributor

Choose a reason for hiding this comment

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

springValidate를 한번 써보시면 좋겠습니다~

Copy link
Author

@ASak1104 ASak1104 Nov 14, 2023

Choose a reason for hiding this comment

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

commit f870b5b, ce79ec0

return voucherRepository.findAll();
}

public Optional<Voucher> findById(UUID id) {

Choose a reason for hiding this comment

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

repository.findById 값이 null일 때 raise 하지 않는 이유가 있을까요??

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신대로 NoSuchElementException을 발생시키고 ControllerAdvice를 통해 noContent Response를 리턴하도록 한 곳에서 관리하는 것도 좋은 방법일 것 같습니다!

제가 해당 코드에서 예외를 발생하지 않은 이유는 아래와 같이 200과 noContent에 대한 분기를 Controller에서 관리하기 위해서 였습니다.

// class VoucherController

@GetMapping("/{id}")
    public ResponseEntity<Voucher> findById(@PathVariable("id") UUID id) {
        Optional<Voucher> optionalVoucher = voucherService.findById(id);

        return optionalVoucher.map(ResponseEntity::ok)
                .orElse(ResponseEntity.noContent()
                        .build());
    }

return walletRepository.unlink(customerId, voucherId);
}

public List<Customer> findCustomersByVoucherId(String voucherId) {
public List<Customer> findCustomersByVoucherId(UUID voucherId) {
List<UUID> customerIds = walletRepository.getCustomerIds(voucherId);

return voucherCustomerFacade.getCustomers(customerIds);

Choose a reason for hiding this comment

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

voucher를 가진 customer를 가져오는 의미를 function이 가지면 더 이해가 잘될 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

기능적인 의미만을 생각해서 말씀해주신 부분을 간과한 것 같습니다.
감사합니다!

commit 70a79d9

@@ -21,16 +20,14 @@ public VoucherCustomerFacadeImpl(VoucherRepository voucherRepository, CustomerRe
}

Choose a reason for hiding this comment

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

퍼사드 패턴을 꼭 사용해야할까요?? fasade를 이용해서 복잡한 로직을 단순화된 인터페이스로 제공하는 것도 아니고 각 서비스에서의 의존성을 줄여주는 용도도 아니라는 생각이 생각이 들어서 각 서비스 내부에 들어가도 괜찮다는 생각이 듭니다.

Copy link
Author

@ASak1104 ASak1104 Nov 14, 2023

Choose a reason for hiding this comment

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

확실히 현재 상황에서 퍼사드 클래스는 불필요한 것 같습니다.

2주차 과제에서는 VoucherRepository가 저장과 전체 조회 기능만을 제공했기 때문에 퍼사드 패턴을 사용했지만
commit

join 쿼리와 limit 1을 통해 존재 여부를 판단하는 쿼리를 추가하면 해당 클래스를 제거하고 성능과 구조에 대해 개선할 수 있을 것 같습니다!!

@@ -6,9 +6,9 @@
import team.marco.voucher_management_system.model.Voucher;

public interface VoucherCustomerFacade {

Choose a reason for hiding this comment

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

어떤 방향으로 다형성을 이용할 수 있을지 잘 모르겠어서 현재는 해당 인터페이스와 같은 추상화가 필요하지 않을 것 같습니다.

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.

None yet

3 participants