-
Notifications
You must be signed in to change notification settings - Fork 170
[4기 - 고예성] SpringBoot Part2 Weekly Mission 제출합니다. #768
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
base: yesung/week2
Are you sure you want to change the base?
[4기 - 고예성] SpringBoot Part2 Weekly Mission 제출합니다. #768
Conversation
FixedAmountVoucher와 PercentAmountVoucher의 생성 테스트와 discount메서드의 성공/실패 테스트를 작성했습니다.
2주차 미션 README를 추가했습니다.
바우처의 테이블을 정의하고 바우처 클래스에 createdAt이라는 필드값을 추가했습니다. 이 값은 DB에서 기본으로 now()로 자동생성되기 때문에 입력이 없는 경우 바우처에 생성자를 따로 정의했습니다. DB에서 바우처 정보를 읽어올 때 createdAt 정보가 필요하므로 이에 관련된 생성자를 엔티티에 추가했습니다.
JdbcVoucherRepository의 CRUD인 save,findByVoucherId, deleteByVoucherId, listAll을 구현했습니다. 그리고 db연결 정보를 deploy에 작성했습니다.
git 명령을 잘못 내려서 날아간 파일들을 다시 복구했습니다...🥲
git 명령을 잘못 내려서 날아간 파일들을 다시 복구했습니다...🥲
JdbcWalletRepository에서 특정 고객에게 바우처를 할당, 고객이 어떤 바우처를 보유하고 있는지 조회, 고객이 보유한 바우처를 제거, 특정 바우처를 보유한 고객을 조회하는 기능을 모두 구현했습니다.
JdbcCustomerRepository를 구현하였습니다. 고객을 새로 생성하거나 고객을 검색, 업데이트, 삭제할 수 있습니다.
전체적인 컨트롤러와 도메인에 관련된 컨트롤러를 나누고 도메인에 따른 패키지를 나눠서 리팩토링했습니다.
DB구조를 변경했고 레포지토리의 잘못된 기능과 책임을 적절히 분산시켰습니다.
| public class FileVoucherRepositoryFailException extends RuntimeException { | ||
| public FileVoucherRepositoryFailException(String message) { | ||
| super(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.
JdbcVoucherRepositoryFailException와 마찬가지로, 이런 예외를 바깥으로 던지면 어떤 Repository 구현체를 쓰는지 알게되는거 아닌가요?
제가 팀미팅 시간에 이야기 드렸던 DIP에 대한 내용과 배치되는 것 같습니다.
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.
345dc39에 반영해봤습니다! 잘 된건지는 모르겠네요 ㅎㅎ 감사합니다
| public class VoucherQuery { | ||
| public static final String CREATE_VOUCHER | ||
| = "INSERT INTO voucher (id, type_id, amount, wallet_id) VALUES (UUID_TO_BIN(?), ?, ?, ?)"; | ||
| public static final String FIND_VOUCHER_BY_ID = "SELECT * FROM voucher WHERE id = UUID_TO_BIN(?)"; | ||
| public static final String LIST_ALL = "SELECT * FROM voucher"; | ||
| public static final String DELETE_BY_ID = "DELETE FROM voucher WHERE id = UUID_TO_BIN(?)"; | ||
|
|
||
| private VoucherQuery() { | ||
| } | ||
| } |
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.
이 코드가 JdbcVoucherRepository에서 빠져나와서 생긴 장점이 있을까요~?
현재에도 JdbcVoucherRepository에서만 사용되고, 앞으로도 JdbcVoucherRepository에서만 사용될 것 같은데 불필요하게 코드의 응집력만 떨어진건 아닐까 우려됩니다.
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.
그렇네요...! 제가 처음 이렇게 클래스를 분리했던 이유는 아무래도 JdbcVoucherRepository에 쿼리가 섞여있다보니 코드 가독성이 떨어지고 클래스가 복잡해보여서 분리했었습니다. 하지만 생각해보니, JdbcVoucherRepository 밖에 쓰지 않으니 불필요한 분리네요! 수정하겠습니다
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.
792a4bc에 반영했습니다! 감사합니다:)
| private static List<VoucherResponse> makeVoucherResponseList(Wallet wallet) { | ||
| List<VoucherResponse> voucherDtos = new ArrayList<>(); | ||
| for (Voucher voucher : wallet.getVouchers()) { | ||
| var dto = ApplicationUtils.convertToVoucherResponse(voucher); | ||
| voucherDtos.add(dto); | ||
| } | ||
|
|
||
| return voucherDtos; | ||
| } |
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.
이 메서드 Stream API 활용하면 훨씬 더 가독성 있게 만들 수 있습니다.
한번 만들어보세요.
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.
792a4bc에 적용했습니다!
| CREATE TABLE voucher_type | ||
| ( | ||
| id INTEGER PRIMARY KEY, | ||
| type VARCHAR(20) NOT NULL, | ||
| UNIQUE KEY unq_type (type) | ||
| ); | ||
|
|
||
| INSERT INTO voucher_type (id, type) | ||
| VALUES (1, 'FIXED_AMOUNT'), | ||
| (2, 'PERCENT_AMOUNT'); |
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.
테이블을 만들기 전에 이 부분에 관해 학습을 했었는데
특히 성능과 확장성을 비교하면서 장점과 단점을 적어보겠습니다.
만일 테이블에 ENUM으로 타입을 넣을 경우의 장점입니다.
- 쿼리에 값을 명확하게 넣을 수 있습니다.
- 조인이 필요 없기 때문에 조회 속도가 빠릅니다.
- 추가적인 테이블이 필요없습니다. 그래서 데이터 용량을 적게 차지합니다.
단점은 다음과 같습니다.
- 확장이 필요한 경우, 위와 같은 경우 만일 FIXED_AMOUNT와 PERCENT_AMOUNT와 연관된 데이터의 저장이 필요할 때 연관 데이터를 함께 저장할 수 없습니다.
- 만일 ENUM 데이터의 변경이 일어난다면 적용되는데 시간이 걸립니다.
위와 같이 참조테이블을 만들었을 때 장점과 단점을 정리하면 다음과 같습니다.
장점 : 연관된 값을 같이 저장할 수 있고 새로운 값을 추가하는 것이 쉽다 => 확장성이 좋다.
단점 : 쿼리에 값을 명확하게 넣을 수 없고 조인을 통해 값을 가져와야 해서 조회 속도가 느려질 수 있다.
만일 voucherType에 관해서 요구사항이 계속 변한다면 참조테이블을 만드는 것이 좋겠지만,
요구사항이 명확하게 정해져있기 때문에 조회속도를 높일 수 있는 방식으로 설계를 하는게 좋을거 같다는 생각이 듭니다...!
| public static String formatWalletSaveDto(WalletSaveDto responseDto) { | ||
| String voucherId = responseDto.getVoucherId(); | ||
| String walletId = responseDto.getWalletId(); | ||
|
|
||
| return MessageFormat.format(WALLET_SAVE_FORMAT, | ||
| voucherId, | ||
| walletId); | ||
| } | ||
|
|
||
| public static String formatWalletFindResponse(WalletResponse walletResponse) { | ||
| UUID walletId = walletResponse.getWalletId(); | ||
| List<VoucherResponse> voucherResponses = walletResponse.getVoucherResponses(); | ||
| String messages = voucherResponses.stream().map(ApplicationUtils::formatVoucherResponseDto).collect(Collectors.joining()); | ||
|
|
||
| return MessageFormat.format(WALLET_FIND_RESPONSE_FORMAT, walletId, messages); | ||
| } |
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.
WalletSaveDto와 WalletResponse를 각각 문자열로 만드는 행위는 각각의 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.
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.
262831f에 반영했습니다~
| public static CustomerResponseDto convertToCustomerResponseDto(Customer customer) { | ||
| long id = customer.getId(); | ||
| public static CustomerResponse convertToCustomerResponse(Customer customer) { | ||
| UUID id = customer.getId(); | ||
| String name = customer.getName(); | ||
| CustomerStatus status = customer.getStatus(); | ||
| UUID walletId = customer.getWalletId(); | ||
|
|
||
| return new CustomerResponseDto(id, name, status); | ||
| return new CustomerResponse(id, name, status, walletId); |
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.
이 코드도 CustomerResponse 쪽으로 들어가는게 적절해보입니다.
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.
넵 이해했습니다..! 그렇다면 위의 코드가 CustomerResponse로 들어간다면,
생성자를 private로 바꾸고 메서드 자체가 static으로 들어가는 것은 어떤거 같나요?
제 생각에는 오히려 한 가지 방법으로만 생성할 수 있기 때문에 괜찮을거 같아서요..!
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.
262831f에 반영했습니다!
| private static final String PERCENT_VOUCHER_FORMAT = """ | ||
| 종류 : {0} | ||
| 아이디 : {1} | ||
| 할인 가격 : {2}% | ||
| 생성시기 : {3} | ||
| """; | ||
| private static final String CSV_USER_FORMAT = """ | ||
| private static final String CUSTOMER_FORMAT = """ | ||
| 고객 아이디 : {0} | ||
| 고객 이름 : {1} | ||
| 고객 상태 : {2} | ||
| """; | ||
| private static final String WALLET_SAVE_FORMAT = """ | ||
| 바우처 아이디 : {0} | ||
| 지갑 아이디 : {1} | ||
| """; | ||
| private static final String WALLET_FIND_RESPONSE_FORMAT = """ | ||
| 지갑 아이디 : {0} | ||
| 바우처 정보 | ||
| ================================= | ||
| {1} | ||
| """; |
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.
ApplicationUtils에서는 이 애플리케이션 전반적으로 사용되는 유틸성 메서드와 값이 포함되어야 할 것 같은데, 특정 입출력 환경에서만 사용되는 값들이 포함되어 있는 것 같아요.
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.
262831f에 반영했습니다~
| public enum VoucherType { | ||
| FIXED_AMOUNT(1, 1), | ||
| PERCENT_AMOUNT(2, 2); | ||
| private static final Logger logger = LoggerFactory.getLogger(VoucherType.class); | ||
| private final int typeId; | ||
| private final int command; | ||
|
|
||
| VoucherType(int typeId, int command) { | ||
| this.typeId = typeId; | ||
| this.command = command; | ||
| } |
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.
typeId와 command가 서로 달라지는 일이 앞으로 생길까요~?
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.
262831f에 반영했습니다!
| private void doService() { | ||
| outputConsole.printProgramMenu(); | ||
| EntireServiceCommand command = inputConsole.readEntireServiceCommand(); | ||
|
|
||
| switch (command) { | ||
| case EXIT -> isExit = true; | ||
| case VOUCHER_SERVICE -> doVoucherService(); | ||
| case CUSTOMER_SERVICE -> doCustomerService(); | ||
| case WALLET_SERVICE -> doWalletService(); | ||
| } | ||
| } |
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.
여기서 각각의 메뉴가 선택된 후에는 각각의 메뉴에 해당하는 Controller에서 기능이 수행되도록 하는게 더 자연스럽지 않을까요?
지금 VoucherController, CustomerController, WalletController는 너무 하는일이 없어보입니다. 해당 Controller들이 해야할 일을 DispatcherController들이 다 가져와버린 것 같아요.
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을 제출하고 나서 저 스스로 이게 정말 효용성이 있나
스프링 MVC의 Dispatcher Servlet과 비교하며 생각을 했습니다.
Dispatcher Servlet은 요청을 매핑하는 역할에서
핵심적인 역할들을 많이하고 나머지 그 요청에 대한 모든 처리는
개발자가 구현한 컨트롤러들에게 맡기는 것을 생각했을 때,
확실히 이런 구조로 가져가야 여러 컨트롤러로 나누는 것에 효용성이 있다고 생각했습니다.
제가 어떤부분에서 이 생각을 놓쳤는지 생각해봤을 때 Dispatcher의 역할을 명확하게 정의하지 않아서
생긴 문제라고 생각합니다..! 반영해서 PR올리겠습니다~ 감사합니다!
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.
fafcb65에 반영했습니다 감사합니다🙇♂️
| @Service | ||
| public class JdbcCustomerService { | ||
| private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerService.class); | ||
|
|
||
| private final CustomerRepository customerRepository; | ||
|
|
||
| public JdbcCustomerService(CustomerRepository customerRepository) { | ||
| this.customerRepository = customerRepository; | ||
| } |
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.
JdbcCustomerService..? 왜 네이밍이 이렇게 되었죠 ㅎ..
DIP를 위반하는 네이밍입니다.
코드 자체로만 보면 Jdbc로 구현된 CustomerRepository가 아니더라도 사용할 수 있어보이는데 말이죠.
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.
fafcb65에 반영했습니다! 감사합니다~
|
예성님 수고하셨습니다~
-> 지금처럼 작성하면 효과를 별로 못느끼실겁니다.. 각 도메인별 컨트롤러가 하는 일이 너무 없어요. 관련 내용은 리뷰 참고해보시기 바랍니다. 지금 사실상 DispatcherController가 모든 도메인별 컨트롤러 역할을 함께 가지고 있어서 문제가 있습니다.
-> 엄밀히 이야기하면 좀 다른내용인데, 관련해서는 나중에 패키지 구조를 나누는 것에 대해서 이야기할 기회가 있을겁니다.
-> 지금 하신대로 해도 상관은 없는데, 제가 볼 때 더 적절한건 Service에서 이 행위가 이루어지도록 제어하는겁니다. Customer가 지워지면 그에 따른 Voucher, Wallet이 지워져야하는건 일종의 비즈니스 로직이라고 할 수 있지 않을까요? (외래키 제약 조건과는 무관하게요) |
| @Transactional | ||
| public CustomerResponse createCustomer(String customerName) { | ||
| return jdbcCustomerService.createCustomer(customerName); | ||
| } |
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.
transaction을 컨트롤러 레이어까지 유지시키지 않아도 될것 같아보이네요
혹시 따로 의도가 있으신가요~?
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.
안녕하세요. 에프님 맹희님 코드리뷰 주셔서 감사합니다! 에프님께서 답변해주신 부분도 코드에 반영했습니다. |
| @@ -1 +1,10 @@ | |||
| spring.config.activate.on-profile: dev | |||
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.
application-dev에 dev가 포함되어 있어서 해당 라인은 지워주셔도 괜찮을것 같네요
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.
아아 이해됐습니다. 세팅 방법을 잘못 알고 있었네요! 바로 반영하겠습니다

📌 과제 설명
전체적인 설계는 다음과 같습니다.

1차 미션에서 구성했던 어플리케이션구성을 더 세분화하였습니다.
모든 도메인을 하나의 컨트롤러로 처리하는 것보다는 전체적인 컨트롤러 하나와
도메인 별 명령을 처리하는 컨트롤러 여러 개를 두는 것이 유지보수 측면에서 좋다고 생각되어 변경하였습니다.
도메인 별 예외와 글로벌 예외를 구분시켰습니다.
저의 경우 DB 설계를 다음과 같이 했습니다.
새로운 유저가 생성되면 유저가 고유한 지갑ID를 갖도록 했습니다.
새로운 유저의 지갑 ID를 지갑 테이블에서 FK로 참조하였습니다.
Foreign key constraint에 따라 만일에 대비해 주인이 없는 지갑이 생성되는 것을 막았습니다.
바우처 테이블에 지갑ID 컬럼을 부여하고 Foreign key로 지갑 테이블을 참조하게 했습니다.
바우처가 처음생성될 때는 null값을 갖도록 만들었습니다.
서비스 중에 바우처를 지갑에 부여하는 기능이 있기 때문에 특정 지갑에 바우처가 부여되면
바우처의 지갑 id에 지갑 테이블에 있는 아이디만 부여될 수 있도록 제약조건을 걸었습니다.
👩💻 요구 사항과 구현 내용
✅ PR 포인트 & 궁금한 점
도메인 별로 컨트롤러를 나누었지만, 정작 이 방법이 확장과 유지보수 측면에서 도움이 되는건가 싶습니다.
이렇게 나눴을 때 오히려 작업해야하는 양이 많아졌음을 느꼈습니다..!
이러한 설계가 괜찮은 설계인지 피드백 부탁드립니다~
레이어 별로 예외를 나눈다는거는 서비스는 서비스 별로, 레포지토리는 레포지토리 별로 나눈다는 의미인가요?
그렇다면 저처럼 나누는 것도 그리 바람직하게 예외를 나눈건 아닌건가란 생각이 듭니다..!
레이어별 예외처리에 관해 피드백 부탁드립니다!
제가 아직 DB에 익숙하지 않아 헷갈립니다.
DB 테이블에 Foreign key constraint가 걸려있기 때문에 유저를 삭제하면
바우처 테이블에 삭제하려는 지갑에 할당된 바우처들 삭제 -> 지갑 테이블의 지갑 삭제 -> 유저 테이블에서 유저 삭제
순서대로 진행되는데,
제 경우 위의 과정을 다른 레포지토리에 의존하는 것 없이
하나의 레포지토리인 JdbcCustomerRepository에서 진행합니다.
하지만 다르게 생각해보면
바우처 테이블에 할당된 바우처들 삭제 -> VoucherRepository의 역할
지갑 테이블의 지갑 삭제 -> WalletRepository의 역할
유저 테이블에서 유저 삭제 -> CustomerRepository의 역할
이라는 생각이 드는데, 저는 결국 CustomerRepository에서 모두 처리하도록 했습니다...!
왜냐하면 각각의 레포지토리가 서로 의존하면 안된다고 생각했기 때문인데요.
이렇게 처리하는 것이 적절한지 여쭤보고 싶습니다..!
예외 및 테스트 코드 작성을 아직 못했습니다...! ㅜㅜ
3차 과제 하면서 PR에 추가로 올리도록 하겠습니다!