-
Notifications
You must be signed in to change notification settings - Fork 170
[4기 - 김영주] H2 데이터베이스 연동 및 할인권 UPDATE, DELETE 기능 구현 #769
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기 - 김영주] H2 데이터베이스 연동 및 할인권 UPDATE, DELETE 기능 구현 #769
Conversation
- dev : Map을 이용한 repository - local : H2 database를 이용한 repository
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 void executeMenu(Menu selectedMenu) { | ||
| switch (selectedMenu) { | ||
| case CREATE -> createVoucher(); | ||
| case LIST -> listAllVouchers(); |
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.
CRUD에 맞춰서 LIST -> READ로 네이밍 변경하는건 어떤가요 ㅎㅎ
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.
반영: 8d05131
- 기존 LIST에서 READ로 네이밍을 변경하였습니다.
- 그에 따라, listAllVoucher() 메서드 명도 readAllVoucher()로 함께 변경하였습니다.
| VoucherResponseDto findResponse = voucherController.findVoucherById(id); | ||
|
|
||
| String discountAmount = viewManager.readVoucherDiscountAmountToUpdate(findResponse); | ||
| VoucherUpdateRequestDto request = new VoucherUpdateRequestDto(findResponse.getId(), findResponse.getType(), discountAmount); | ||
| VoucherResponseDto response = voucherController.update(request); |
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.
view 쪽이 프론트엔드라고 생각하면, 업데이트를 위해서 조회 + 수정 요청을 두번 보내는 상황이 됩니다.
(id값이 유효한지 체크하는 로직때문에)
따라서 조회 + 수정하는 로직은 service or repository 레이어에서 한번에 수행하는게 더 좋을것 같아요~
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.
해당 사항은 재원님과 의견을 나눠보고 싶어서, 슬랙 DM으로 드렸습니다. 감사합니다.
| @FunctionalInterface | ||
| public interface TriFunction<T, U, V, R> { | ||
|
|
||
| R apply(T t, U u, V v); | ||
| } |
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.
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.
반영: a006a6d
- 기본 라이브러리에서 TriFunction을 제공하지 않아서 직접 만들었었는데, 외부 라이브러리에 있는 줄 몰랐습니다. 정말 감사합니다!
- Apache Commons Lang 3.12.0 버전을 Gradle로 의존성 추가를 하고 바로 대체했습니다.
- 이 기회에 해당 라이브러리가 기본 Java API 기능 이외의 확장 기능을 많이 제공한다는 점을 알게되어서, 추후에 요긴하게 사용할 수 있을 것 같습니다.
| public VoucherResponseDto(Voucher voucher) { | ||
| this.id = voucher.getId(); | ||
| this.type = voucher.getType(); | ||
| this.discountAmount = voucher.getDiscountAmount(); | ||
| } |
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.
vocher 엔티티를 파라미터로 받는 생성자를 만들기보다, 필요한 필드값의 파라미터를 가진 생성자를 만든 후 해당 생성자를 사용하는 정적 팩토리 메소드를 만드는게 확장성에 더 좋을 것 같습니다.
이 경우 생성자도 재사용이 가능하니까요.
더 많은 이유는 이펙티브 자바 내용을 참고해보시면 좋을 것 같습니다.
- 정리해놓은 블로그 : https://nankisu.tistory.com/87
그리고 롬복 @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.
반영: 2d17e60
- 기존
- DTO 객체에서 voucher 객체를 직접 생성자의 매개변수로 받아서, 새로운 DTO 객체를 생성하고 있었습니다.
- 필드를 초기화하는 생성자를 만들지 않아, 추후 관련 생성자를 작성해야할 필요가 있다면 생성자가 계속 추가되어 코드가 지저분해질 여지가 있었습니다.
- 변경
- 클래스의 필드를 초기화하는 생성자 하나만 작성했습니다. (롬복의 RequiredArgsConstructor 이용)
- from() 이라는 정적 팩토리 메서드를 추가하여, voucher 객체를 매개변수로 받아 해당 메서드 안에서 new를 이용해 생성자를 호출하도록 변경했습니다.
추가적으로 빌더 패턴 말씀해주셔서 관련 내용 학습했습니다. 감사합니다.
하지만 빌더 패턴은 생성자의 매개변수가 4개 이상의 많은 개수가 들어올 때 사용하는 것이 좋다고 해서
여기서는 사용하지 않았습니다.
롬복의 Builder 어노테이션도 결국 바이트코드로 컴파일 하는 과정에서는 빌더 클래스를 다 만들기 때문에
단순히 정적 팩토리 메서드를 이용하는 것이 현재 상황에는 더 적합하다고 판단했습니다.
그래도 스스로 연습은 꼭 해보겠습니다!
| public VouchersResponseDto(List<Voucher> vouchers) { | ||
| this.vouchers = vouchers.stream() | ||
| .map(VoucherResponseDto::new) | ||
| .collect(Collectors.toList()); |
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.
java17을 사용한다면 추가된 문법을 사용해보아요
| .collect(Collectors.toList()); | |
| .toList(); |
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.
반영: 8d05131
- java 16부터 collect()를 사용하지 않고, toList()만 사용해도 리스트를 반환할 수 있다는 점을 알게되어 너무 좋았습니다.
- 관련해서 아래와 같은 내용 학습했습니다.
collect(Collectors.toList())는 mutable한 리스트가 반환되므로 추후 수정이 일어날 가능성이 있습니다.- 그래서
toUnmodfiableList()를 통해 immutable한 리스트를 반환할 수 있도록 했는데 - java 16부터는 이를
toList()를 통해서 기본적으로 immutable한 리스트로 반환하도록 기능이 추가되었습니다. toUnmodfiableList()와toList()가 완전히 같지는 않고,toUnmodfiableList()의 경우 내부 원소로 null이 들어오게 되면 NPE가 발생하지만,toList()는 null 원소를 허용한다는 차이는 있습니다.
| } | ||
|
|
||
| @Override | ||
| public int delete(String id) { |
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.
deleteById 네이밍이 더 좋을것 같아요
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.
반영: 8d05131
- 기존 delete에서 deleteById로 네이밍 변경했습니다.
- 아무래도 ID 이외에 다른 조건으로 삭제를 해야하는 상황이 있게되면 구별되어서 좋을 것 같습니다.
| --- | ||
|
|
||
| spring: | ||
| config: | ||
| activate: | ||
| on-profile: dev | ||
|
|
||
| --- | ||
|
|
||
| spring: | ||
| config: | ||
| activate: | ||
| on-profile: local | ||
| h2: | ||
| console: | ||
| enabled: true | ||
| path: /h2-console | ||
| datasource: | ||
| driver-class-name: org.h2.Driver | ||
| url: jdbc:h2:mem:local | ||
| username: sa | ||
| password: No newline at end of file |
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.
보통 profile은 local -> develop -> production 을 많이 사용하는데요.
해당 순서에 맞춰 선언해주시면 더 좋을것 같아요~!
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.
반영: 354246d
- 스스로 생각했을 때는 dev가 가장 낮은 단계라고 생각했는데 착각이었네요!
- 관련해서 기존 local은 dev로, dev는 local로 서로 네이밍을 스위칭 했습니다. 감사합니다.
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.
실제 서비스에서는 운영서버(prod) 배포 이전에 개발서버(dev)를 거쳐 테스트를 진행해서 이런 프로필별 환경 분리가 나오게 됩니다.
local은 당연 각 개발자별 로컬환경을 뜻하구요.
추후 프로젝트에서 협업을 진행하며 사용하게 될 git flow 전략을 먼저 읽어보시는걸 추천드려요~~
https://techblog.woowahan.com/2553/ (매우 유명한 글)
- 조회 메뉴 이름을 LIST에서 READ로 변경 - delete 메서드를 deleteById로 변경하여 ID에 의해 할인권이 삭제됨을 명시 - Java 16부터 추가된 toList() 기능을 이용해 collect 대체
- 매개변수가 1개일 때는 from으로 짓는 것이 권장되기 때문
- MemoryVoucherRepository로 테스트 하는 단계를 dev에서 local로 변경 - JdbcVoucherRepository로 테스트 하는 단계를 local에서 dev로 변경
mentor-tyler
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.
수고하셨습니다~
| } | ||
|
|
||
| public void showAllVouchers(List<Voucher> vouchers) { | ||
| public void showAllVouchers(VouchersResponseDto response) { |
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.
| public void showAllVouchers(VouchersResponseDto response) { | |
| public void showAllVouchers(List<Voucher> vouchers) { |
기존대로 사용하는 파라미터를 받아 처리하는건 어떨까요?
이경우의 단점은
- 테스트 코드 작성이 복잡해진다.
- 사용하는 파라미터를 감싸는 오브젝트를 넘길 경우 사용하는 쪽에서 어떤 필드를 사용하는지 메소드 안까지 들여다봐야한다.
- 메서드의 시그니처를 볼때 명시적이지 못하다.
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.
반영: 4fc2b92
- 기존
public void showAllVouchers(VouchersResponseDto response)
- 변경
public void showAllVouchers(List<VoucherResponseDto> response)- Voucher는 엔티티라서, DTO 객체인 VoucherResponseDto를 담은 리스트를 매개변수로 받도록 변경했습니다.
- 말씀하신대로 메서드 시그니처를 볼 때 바우처의 리스트가 들어온다는 더 명시적인 의미가 된 것 같습니다. 감사합니다!
| Menu createMenu = Menu.from("1"); | ||
| Menu listMenu = Menu.from("2"); | ||
| Menu updateMenu = Menu.from("3"); | ||
| Menu deleteMenu = Menu.from("4"); | ||
| Menu quitMenu = Menu.from("5"); |
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.
Parameterizedtest를 사용해보시면 깔끔하게 정리가 될것 같아요.
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.
반영: e675be2
@CsvSource를 이용하여 더 깔끔하게 테스트 코드를 변경했습니다. 감사합니다!
| public void deleteById(String id) { | ||
| int deletionCounts = voucherRepository.deleteById(id); | ||
|
|
||
| if (deletionCounts == ZERO) { |
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.
이것보다 deletionCounts == 0 이라는게 무엇을 의미하는지에 대한 inline method를 작성하는 편이 더 좋아보여요,
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(deletionCounts) {
return deletionCounts == ZERO;
}그런데 말씀해주신 inline method 관련해서 찾아보니 (https://refactoring.guru/ko/inline-method)
표현식 자체의 의미가 명확하면 별도의 메서드로 분리하지 말라는 내용이 나와서 약간의 혼동이 있는 것 같습니다.
혹시 멘토님께서 의미하신 것이 전자인지 후자인지, 아니면 아예 다른 의미인지 알 수 있을까요?
감사합니다.
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.
어쩌면 취향이 차이일지도 모른다는 생각도 들긴 하는데요 ㅎ
리팩토링에서 고민해봐야 할 부분은, 처음보는 사람도 코드를 "읽어 내려가는데" 위화감이 없어야 한다 라고 생각이 들어요.
public void deleteById(String id) {
int deletionCounts = voucherRepository.deleteById(id);
if (deletionCounts == ZERO) {
throw new NotFoundVoucherException(id);
}
}vs
public void deleteById(String id) {
int deletionCounts = voucherRepository.deleteById(id);
if (isEmptyDeleteResult(deletionCounts)) {
throw new NotFoundVoucherException(id);
}
}
private boolean isEmptyDeleteResult(deletionCounts) {
return deletionCounts == 0;
}위 코드의 차이일것 같은데, 아래 코드의 경우 메소드 이름에서 의미를 말하고 있으니 deletionCounts == 0이라는 코드도 안봐도 되는 장점이 있을것 같아서 말씀드린 부분입니다~
공유주신 링크에서 이야기 하고자 하는건 아마 코드만 봐도 알 내용을 구지 메소드로 추출하는데 의미가 없다를 이야기 하는것 같고,
제가 이야기 했던건, 0이라는 숫자가 매직넘버인가? 그럼 상수로 빼야지? 하는 생각도 좋지만 그런 리팩토링 관점이라면 inlineMethod로 추출하고 함수명으로 의미를 부여 한다면 0이라는 상수도 빼지 않아도 되고 코드를 읽어 내려가는데 더 직관적이지 않을까 했던거에요~
어쩌면 if (deletionCounts == ZERO) { vs if (isEmptyDeleteResult(deletionCounts)) { 의 차이로 볼수 있지만 자세히 살펴보면 상수로 처리하는 것보다 메소드로 추출하는게 더 좋을수도 있다를 리뷰 드리려 했던것 같네요 ㅎ
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.
반영: 9bf6ed8
- 사실 여태까지 "매직넘버라면 의미부여 차원에서 상수로 빼야한다." 라는 느낌으로 약간은 기계적으로 코딩하고 있었던 것 같습니다.
- 뭔가 수학 문제 풀듯이 법칙이나 공식이 있는 건 아니였는데, 제가 확실히 시야가 멀리 뻗어나가질 못했네요!
리팩토링 관점이라면 inlineMethod로 추출하고 함수명으로 의미를 부여 한다면 0이라는 상수도 빼지 않아도 되고 코드를 읽어 내려가는데 더 직관적이지 않을까라는 말씀이 되게 신선하게 다가오는 것 같습니다. 코딩도 정말 글쓰는 것과 똑같다는 생각도 이번에 강하게 들었네요! 감사합니다. 🙏
| private final TriFunction<UUID, VoucherType, String, Voucher> voucherGenerator; | ||
|
|
||
| VoucherType(String number, String name, String message, Function<String, Voucher> voucherGenerator) { | ||
| VoucherType(String number, String name, String message, String unit, |
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.
- 메서드의 파라미터가 많아지는 경우엔 가독성을 해칠수도 있으니 별도의 DTO 성격의 오브젝트로 감싸서 넘겨도 좋을것 같아요.
- 이 경우엔 생성자 보단 빌더패턴을 써보는것도 좋을것 같구요.
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.
반영: 6223d09
- 기존
- VoucherType의 생성자에 매개변수가 많아서 가독성이 떨어졌었습니다.
- 변경
- VoucherTypeInfo라는 별도의 빌더 클래스 역할을 하는 파일을 만들었습니다.
- 롬복의
@Builder애너테이션을 이용하였고, 현 상황에서는 모든 필드에 값을 주입하기 때문에 생성자가 아닌 클래스에 애너테이션을 붙였습니다. - VoucherType에서 Enum 객체를 초기화 할 때, 빌더 패턴을 통해 값을 받으니 어떤 값이 어떤 의미인지 더 명확해져서 좋았습니다. 감사합니다.
| @FunctionalInterface | ||
| public interface TriFunction<T, U, V, R> { | ||
|
|
||
| R apply(T t, U u, V v); | ||
| } |
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.
- VoucherType의 생성자 파라미터가 너무 많아서 가독성 저해 - VoucherTypeInfo라는 빌더 클래스를 만들어서 생성자 파라미터로 주입
할인권(Voucher) 관리 애플리케이션 2주차
1️⃣ 프로젝트 설명
id: UUID 기반의 할인권 고유 번호type: 할인 방식(고정 금액 or 퍼센트 비율)discountAmount: 할인 금액 또는 비율고정 금액 할인(Fix Discount)원입니다.퍼센트 비율 할인(Percent Discount)%입니다.생성할 수 있습니다. (1주차에서 완성)조회할 수 있습니다. (1주차에서 완성)수정할 수 있습니다.삭제할 수 있습니다.현재 PR은 할인권의 수정과 삭제 기능에 대한 PR입니다.
2️⃣ 기능 구현 사항
3️⃣ 프로그램 구조도
4️⃣ 출력 화면