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

[4기 - 고예성] SpringBoot Part3 Weekly Mission 제출합니다. #824

Open
wants to merge 21 commits into
base: yesung/week3
Choose a base branch
from

Conversation

Dev-Yesung
Copy link
Member

@Dev-Yesung Dev-Yesung commented Jul 13, 2023

📌 과제 설명

바우처 관리 웹 애플리케이션을 구현했습니다.
thymeleaf를 이용한 구현의 경우 각각의 도메인이 컨트롤러를 가지고 있습니다.
각 컨트롤러의 이름은 WebXXX(도메인 이름)Controller 입니다.

RestWebVoucherController를 이용하여 REST API를 개발했습니다.
기본적인 기능을 이전과 동일하고 추가적으로 바우처 생성기간과 할인 타입별 조회를 추가로 구현했습니다.

👩‍💻 요구 사항과 구현 내용

🌱 스프링 바우처 미션 week 3
(기본) 바우처 서비스 관리페이지 개발하기

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

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

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

보너스

  • 바우처 지갑용 관리페이지를 만들어보세요.

✅ 피드백 반영사항

#768 에서 주셨던 피드백을 모두 반영했습니다. 피드백별 커밋링크를 코멘트 주신 곳에 걸어놓겠습니다!

✅ PR 포인트 & 궁금한 점

RestAPI는 API 설계 규칙, 기준 같은 것이 있어서 잘 설계된 API를 보면서 학습하면 될거 같은데,
뷰에 관한 URL도 그런것이 존재하나요? 예를 들어 URL에 메서드(행위)를 적으면 안된다던지..?
아니면 REST와 같은 방식으로 설계를 하는지가 궁금합니다!

테스트코드는 개인적으로 개인과제와 병행하면서 진행해보려고 합니다🥲
테스트 코드에 관해서만 추후에 피드백이 가능할까요?

Dev-Yesung added 19 commits July 3, 2023 16:29
FixedAmountVoucher와 PercentAmountVoucher의 생성 테스트와 discount메서드의 성공/실패
테스트를 작성했습니다.
2주차 미션 README를 추가했습니다.
바우처의 테이블을 정의하고 바우처 클래스에 createdAt이라는 필드값을 추가했습니다. 이 값은 DB에서 기본으로 now()로 자동생성되기 때문에 입력이 없는 경우 바우처에 생성자를 따로 정의했습니다. DB에서 바우처 정보를 읽어올 때 createdAt 정보가 필요하므로 이에 관련된 생성자를 엔티티에 추가했습니다.
JdbcVoucherRepository의 CRUD인 save,findByVoucherId, deleteByVoucherId, listAll을 구현했습니다. 그리고 db연결 정보를 deploy에 작성했습니다.
git 명령을 잘못 내려서 날아간 파일들을 다시 복구했습니다...🥲
git 명령을 잘못 내려서 날아간 파일들을 다시 복구했습니다...🥲
JdbcWalletRepository에서 특정 고객에게 바우처를 할당, 고객이 어떤 바우처를 보유하고 있는지 조회, 고객이 보유한
바우처를 제거, 특정 바우처를 보유한 고객을 조회하는 기능을 모두 구현했습니다.
JdbcCustomerRepository를 구현하였습니다. 고객을 새로 생성하거나 고객을 검색, 업데이트, 삭제할 수 있습니다.
전체적인 컨트롤러와 도메인에 관련된 컨트롤러를 나누고 도메인에 따른 패키지를 나눠서 리팩토링했습니다.
DB구조를 변경했고 레포지토리의 잘못된 기능과 책임을 적절히 분산시켰습니다.
week3 미션을 위한 타임리프 의존성 설정 및 경로 설정을 완료했습니다.
thymeleaf 템플릿을 이용한 페이지 구현과 REST API 설계 및 구현을 모두 완료했습니다.
다른 곳에서 사용하지 않음과 응집도를 떨어트리던 Query 클래스를 삭제하고 쿼리들을 Jdbc레포지토리 구현체 클래스 안으로
넣었습니다. 또한 서비스에서 stream을 이용할 수 있는 부분들을 변경하여 클래스 가독성을 개선했습니다.
기존에 ApplicationUtil에 있던 dto생성 메서드와 메시지 포멧팅 메서드를 각각의 dto에 알맞는 클래스로 넣어
리팩토링했습니다. 또한 Enum에서 필드 값인 typeId와 Command의 역할이 겹치는 것을 Command를 제거하고
typeId로 통일했습니다.
기존에 DispatcherController에게 불필요하게 위임되었던 책임을 각 도메인 별 컨트롤러에게 나눴습니다.
JdbcCustomerRepository의 deleteByCustomerId에서 바우처 -> 지갑 -> 고객 순서대로 삭제되던
비즈니스 로직을 레포지토리에서 처리하는 것이 아닌 서비스에서 처리가 되도록 만들었습니다. 이를 통해 레포지토리는 더이상 연관관계를
가진 로직이 아니라 독립된 로직으로 구현할 수 있습니다.
특정 구현체를 알 수 있는 예외 클래스 네이밍을 일반적인 예외로 변경하고 필요없는 예외 클래스는 삭제했습니다.
리다이렉트 페이지가 중복되어 리팩토링했습니다.
Comment on lines 31 to 40
@Transactional
public CustomerResponse createCustomer(String customerName) {
UUID customerId = ApplicationUtils.createUUID();
CustomerStatus customerStatus = CustomerStatus.WHITE;
UUID walletId = ApplicationUtils.createUUID();
Customer customer = new JdbcCustomer(customerId, customerName, customerStatus, walletId);
Customer createdCustomer = customerRepository.createCustomer(customer);

return CustomerResponse.of(createdCustomer);
}

Choose a reason for hiding this comment

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

JdbcCustomer를 사용하는게 제가 팀미팅 시간에 이야기 드렸던 DIP에 대한 위반 여지가 있습니다.
수정해보시죠. ㅎㅎ

Copy link
Member Author

@Dev-Yesung Dev-Yesung Jul 16, 2023

Choose a reason for hiding this comment

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

이해한게 맞는지 모르겠지만, 스스로 생각해본 것을 대답 해보겠습니다~

서비스는 도메인영역의 Customer보다 상대적으로 저수준이라
서비스가 도메인에 의존하는 것은 문제가 없다고 생각합니다!
다만, 도메인인 Customer의 네이밍에 문제가 있다고 생각합니다.
도메인에 저수준인 인프라 영역의 구체적인 기술을 사용하는 Jdbc라는 이름이 들어가게 되면
특정 구현체(기술)가 어떤 것인지 알 수 있고 고수준이 저수준에 의존한다는 의미가 되어 DIP위반이라는 생각이 들었습니다. 따라서 클래스 이름을 인프라스트럭처에 NormalCustomer라는 이름으로 변경하였습니다...!

근데 왜 객체 이름을 JdbcCustomer라고 지었는지 모르겠네요 ㅎㅎㅎㅠ Jdbc에 의존하는 객체도 아닌데... 너무 머리를 비우고 코딩했나봅니다! 감사합니다~😇

Comment on lines 69 to 74
UUID walletId = customer.getWalletId();

return new JdbcCustomer(id, name, customerStatus, walletId);
})
.orElseThrow(() -> new NoExistCustomerException("업데이트하려는 유저가 존재하지 않습니다."));
Customer updatedCustomer = customerRepository.update(responseCustomer);

Choose a reason for hiding this comment

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

Comment on lines 50 to 71
@GetMapping("/type/{typeId}")
public ResponseEntity<List<VoucherResponse>> findByType(@PathVariable Integer typeId) {
List<VoucherResponse> response = voucherService.findByType(typeId);

return ResponseEntity.ok().body(response);
}

@GetMapping("/date/{createdAt}")
public ResponseEntity<List<VoucherResponse>> findByDate(@PathVariable String createdAt) {
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd");

try {
Date parsedDate = inputFormat.parse(createdAt);
String formattedDate = outputFormat.format(parsedDate);
List<VoucherResponse> response = voucherService.findByDate(formattedDate);

return ResponseEntity.ok().body(response);
} catch (ParseException e) {
return ResponseEntity.badRequest().build();
}
}

Choose a reason for hiding this comment

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

type, createdAt은 URL을 이렇게 나누는게 표준적인 방법은 아닙니다.
어차피 검색되는 대상은 Voucher들이죠?
어떻게 하는게 좀 더 권장되는 방법인지는 찾아보시죠~

아래는 다른분들에게 드렸던 피드백 동일하게 드립니다.

API 설계 관련해서는 표준적으로 권장되는 가이드와 여러 서비스들의 API 설계가 어떻게 되어있는지 둘러보시면 좋습니다~
Voucher 관리 서비스는 명확하게 API를 설계할 수 있는 반면 애매한 API를 가지게되는 서비스들도 있어서요.
만약 그런 API를 만들게될 경우 표준적인 설계 방법을 벗어나지 않는 선에서 예성님이 생각하는 기준에 입각하여 API를 설계해보시기 바랍니다.

Copy link
Member Author

@Dev-Yesung Dev-Yesung Jul 16, 2023

Choose a reason for hiding this comment

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

이해됐습니다!

처음에는 프론트단 URL에서 쿼리 파라미터를 사용해 조건 검색하는게 생각나서 그렇게 바꾸려했는데,
혹시 프론트단에서만 사용하는 방식인가 싶어서 제가 개인프로젝트 때문에 참고하는 API를 확인해봤더니
다음과 같이 설계되어 있는 것을 확인했습니다...!

https://api.spotify.com/v1/tracks?ids={아이디값}

멘토님께서 언급하신

어차피 검색되는 대상은 Voucher들이죠?

라고 말씀하신 이유를 이해했습니다~
바로 반영하도록 하겠습니다. 감사합니다!

Comment on lines 18 to 20
List<Voucher> findByType(Integer typeId);

List<Voucher> findByDate(String formattedDate);

Choose a reason for hiding this comment

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

이 2가지는 Repository에 노출되면 안됩니다.
두가지 모두 데이터베이스에 의존적인 값 아닌가요?

public abstract class Voucher {
    private final VoucherType type;
...
    private final LocalDateTime createdAt;
...

Voucher에는 VoucherType와 LocalDateTime로 들어가 있는데... ㅎㅎ
Repository 구현체에서 변환해줘야하는 값으로 보입니다. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

처음에는 어떤 의미로 피드백 주셨던 건지 잘 이해가 안갔습니다...! 그러다가 DIP의 관점을 가지고 왜 DB에 저장되는 값인데 노출이 되면 안될까 생각을 해봤는데, 결국 저차원 모듈인 DB가 가진 값을 그보다 고차원 모듈인 서비스가 가지게 된다면 고차원 모듈이 저차원 모듈에 의존하여 변경에 취약해진다고 판단했습니다! 수정하겠습니다 감사합니다~

Comment on lines 64 to 72
@Override
public List<Voucher> findByType(Integer typeId) {
return null;
}

@Override
public List<Voucher> findByDate(String formattedDate) {
return null;
}

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.

넵 구현해봐야겠습니다~🫠 감사합니다!

Comment on lines 45 to 59
public List<VoucherResponse> findByType(Integer typeId) {
List<Voucher> response = voucherRepository.findByType(typeId);

return response.stream()
.map(VoucherResponse::of)
.toList();
}

public List<VoucherResponse> findByDate(String formattedDate) {
List<Voucher> response = voucherRepository.findByDate(formattedDate);

return response.stream()
.map(VoucherResponse::of)
.toList();
}

Choose a reason for hiding this comment

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

아예 이렇게 서비스의 파라미터부터 이렇게 받고 있었군요 ㅎ...
아마 컨트롤러도 이렇게 되어있겠죠?
이러면 클라이언트 ~ 레포지토리까지 서로 암묵적인 의존성이 존재하는겁니다.
레포지토리가 type을 마음대로 바꿀 수도 없고, 날짜 포맷조차도 클라이언트와 동시에 바뀌어야할겁니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

위쪽에 주신 피드백과 함께 이해했습니다! 감사합니다~

@lleellee0
Copy link

@Dev-Yesung

예성님 수고하셨습니다~
리뷰 드린 내용들 확인해보시고 수정해보세요. ㅎㅎ

RestAPI는 API 설계 규칙, 기준 같은 것이 있어서 잘 설계된 API를 보면서 학습하면 될거 같은데,
뷰에 관한 URL도 그런것이 존재하나요? 예를 들어 URL에 메서드(행위)를 적으면 안된다던지..?
아니면 REST와 같은 방식으로 설계를 하는지가 궁금합니다!

-> 뷰도 그렇게 설계할 수 있지만, 보통 쉽지 않습니다. 애초에 Rest API가 자원을 기준으로 자원에 가해지는 행위를 기준으로 URL과 Method를 나누는건데요, 여러분들이 만드신 뷰는 뷰 자체가 자원이 아니라 자원을 조회하고 제어하기 위한 도구인거죠. 그래서 Rest API로 만드실 필요는 없습니다.

테스트코드는 개인적으로 개인과제와 병행하면서 진행해보려고 합니다🥲
테스트 코드에 관해서만 추후에 피드백이 가능할까요?

-> 네~ 나중에 피드백 가능합니다.

DIP를 위반하던 JdbcCustomer를 NormalCustomer로 변경했습니다.
바우처 검색에 관한 API를 리팩토링 했습니다.
쿼리파라미터를 이용하여 바우처 타입과 바우처 날짜별 검색을 하도록
변경했고
쿼리파라미터에 값이 없으면 모든 바우처가 조회되도록 변셩했습니다.
또한 기존 DB값에 의존하여 DIP를 위반하던 구현을
변경하였습니다.
}

@Override
public List<Voucher> findByDate(String date) {
Copy link

@maenguin maenguin Jul 19, 2023

Choose a reason for hiding this comment

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

여기는 애초에 LocalDateTime을 넘겨주는게 괜찮아 보입니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

저번주에 정신이 없어서 이제야 확인했네요!! 넵 반영하겠습니다 ㅎㅎ

Comment on lines +47 to +50
h2:
console:
enabled: true
path: /h2-console

Choose a reason for hiding this comment

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

다른분들에게도 남긴 코멘트인데요

일부 설정값들은 스프링부트를 사용하면 AutoConfiguration에 의해 자동으로 설정되기 때문에
사실상 지워도 같은 동작을 하게 되는데요(아닐수도? ㅎㅎ)

어쨌든 저는 이렇게 명시적으로 적어주는걸 선호하는 편 입니다.(시스템에 문제가 있을법한 경우에 한해서만요)

그 이유는 스프링부트의 AutoConfiguration에 의해 내가 의도하지 않은 기능을 가진 빈과 내가 원하지 않은 설정이 자동으로 적용될 수 있기 때문입니다.
이렇게 되면 애플리케이션이 내가 전혀 기대하지 않은 동작을 하게되죠
스프링부트는 편하지만 편한만큼 나의 인지영역을 벗어난 코드가 숨어들기 때문에 위험합니다.

그렇기 때문에 AutoConfiguration을 지원하는 의존성을 추가했을때 해당 의존성에 의해 어떤 빈이 추가되고 어떤 기본값을 가지는지 확인할 수 있는 능력을 기르시면 나중에 디버깅하실때 정말 도움이 많이 될것이라 자부합니다.
시간이 되실때 스프링부트의 AutoConfiguration의 동작 원리를 공부해보시면 좋을것 같아요!

Copy link
Member Author

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.

3 participants