Conversation
- 다중환경에서 lost update로 인한 데이터 적합성 문제를 해결하기 위해 락을 사용했습니다. - 은행 업무에서는 데이터 무결성을 지키는게 중요하다고 생각해서 비관적 락을 사용했습니다.
- 다중 환경에서 lost update로 인한 데이터 무결성 해결을 위해 비관적 락 사용
- 이후 확장성을 위해 int에서 long으로 변경
- 이후 확장성을 위해 int에서 long으로 변경
- 송금 내역 저장을 위함 - 메세지 큐에 메세지 전송을 위함
- 이벤트 리스너가 비동기적으로 실행되도록 하기 위함
- 이벤트 리스너를 통해 수령인의 잔액 업데이트 로직 분리 - 메세지 큐를 통해 수령인의 잔액 업데이트
- 이후 인덱스 설정을 위함
- jdbc를 이용한 bulk insert 구현
- 랜덤 정산과 1/N 정산을 type을 통해 분리
| private final AccountService accountService; | ||
|
|
||
| @PostMapping("/savings") | ||
| public void createSavingsAccount(@RequestBody @Valid PostSavingsAccountReq postSavingsAccountReq) { |
There was a problem hiding this comment.
void로 반환하면 204가 반환될 것 같아요. create라 201이 좋아보입니다.
| accountService.createSavingsAccount(postSavingsAccountReq); | ||
| } | ||
| @PostMapping("/main/deposit") | ||
| public MainAccountInfoRes depositMainAccount(@RequestBody @Valid PostMainAccountReq postMainAccountReq) { |
There was a problem hiding this comment.
@ResponseStatus나 ResponseEntity를 사용하지 않은 이유가 어차피 200 반환해서 굳이 사용 안하신건가요??
There was a problem hiding this comment.
네 그렇긴 한데..그러다 보니 create 같은 요청에 적절한 응답 코드를 반환하는 걸 놓쳤네요. 명시적으로 반환하도록 수정하여 반영하겠습니다.
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AccountService { | ||
| private static final long CHARGE_UNIT = 10_000; |
| * 랜덤 정산 금액 계산 | ||
| */ | ||
| public List<Long> generateRandomAmount(int count, long totalAmount) { | ||
| ThreadLocalRandom random = ThreadLocalRandom.current(); |
There was a problem hiding this comment.
저도 ThreadLocalRandom 사용했는데 Random 처럼 보안 문제로 인해 PR이 거절됩니다
SecureRandom을 써야 될 것 같은데, 다른 방법이 있는지는 모르겠네요
| .nickname(postUserReq.nickname()) | ||
| .build()); | ||
|
|
||
| Account mainAccount = new Account(user.getId()); |
There was a problem hiding this comment.
다른 곳에서 이벤트 리스너를 사용하셨으니 이 부분도 이벤트 리스너 사용해서 책임 분리를 하는 것은 어떻게 생각하시나요? 개인적인 의견이 궁금해서 남깁니다
There was a problem hiding this comment.
이벤트 리스너 사용 목적에 따라 다르긴한데, 제 생각에는 해당 기능에서 이벤트 리스너를 사용하는 이유가 책임을 분리하기 위한 것이라면 적용해도 괜찮을 것 같아요.
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class SettlementDetailRepository { |
There was a problem hiding this comment.
나중에 정산 인원이 많아지면 JdbcTemplate 사용해서 처리하는 것도 좋아보이네요! 배워갑니다
|
|
||
| public SavingsAccount(long userId) { | ||
| this.userId = userId; | ||
| this.balance = 0L; |
| import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | ||
|
|
||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { |
| @Entity | ||
| @Table(name = "account") | ||
| public class Account extends BaseEntity { | ||
| private static final long DEFAULT_CHARGE_LIMIT = 3_000_000; |
| public interface TransferTransactionJpaRepository extends JpaRepository<TransferTransaction, Long> { | ||
| @Transactional | ||
| @Modifying | ||
| @Query(value = """ |
There was a problem hiding this comment.
현재 @Modifying을 사용하는 곳에서 1차 캐시로 인해 문제가 발생하지는 않을 것 같습니다만, 영속성 1차 캐시 문제에 대해서 한 번 알아두시면 좋아보여요
ZZAMBAs
left a comment
There was a problem hiding this comment.
고생하셨습니다. RabbitMQ, Spring Event 코드 사용 등을 통해 이벤트 처리를 효과적으로 구현하신 것 같아요. 많은 인사이트를 얻은 것 같습니다. 👍 이벤트 처리와 메시지 큐 활용 부분의 지식이 적어 그 부분에 대한 코드 리뷰는 제가 더 학습한 후에 추가하겠습니다!
| long receiverMainAccount; | ||
| long amount; | ||
|
|
||
| @Builder |
| @Column(name = "main_account") | ||
| private Long mainAccount; |
| @Query("SELECT a FROM Account a WHERE a.id = :id") | ||
| public Optional<Account> findByIdWithWriteLock(@Param("id") Long id); | ||
|
|
||
| @Transactional |
| User user = userRepository.findByEmail(postSavingsAccountReq.email()) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.INVALID_EMAIL)); | ||
|
|
||
| savingsAccountRepository.save(new SavingsAccount(user.getId())); |
There was a problem hiding this comment.
userRepository.findByEmail() 코드 실행 후, 그 유저가 삭제되었다면 이 코드 실행에 문제가 있을 것 같아요.
| public void charge(long amount, Account account) { | ||
| if (account.isDailyLimitExceeded(amount)) { | ||
| throw new CustomException(ErrorCode.EXCEEDED_DEPOSIT_LIMIT); | ||
| } | ||
| account.deposit(amount); | ||
| } | ||
|
|
||
| public User getUserById(long userId) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.INVALID_USER_ID)); | ||
| return user; | ||
| } | ||
|
|
||
| public Account getAccountByIdWithWriteLock(long mainAccount) { | ||
| Account account = accountRepository.findByIdWithWriteLock(mainAccount) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.INVALID_MAIN_ACCOUNT)); | ||
| return account; | ||
| } |
| @Bean(name = ASYNC_LISTENER_TASK_EXECUTOR_NAME) | ||
| public ThreadPoolTaskExecutor asyncListenerTaskExecutor() { | ||
| return getThreadPoolTaskExecutor(ASYNC_LISTENER_TASK_EXECUTOR_NAME); | ||
| } | ||
|
|
||
| @Bean(name = ASYNC_SCHEDULER_TASK_EXECUTOR_NAME) | ||
| public ThreadPoolTaskExecutor asyncSchedulerTaskExecutor() { | ||
| return getThreadPoolTaskExecutor(ASYNC_SCHEDULER_TASK_EXECUTOR_NAME); | ||
| } |
There was a problem hiding this comment.
아직 학습 중이라 코드상에는 반영을 못했습니다. 스레드 풀을 분리한 이유는 이벤트 처리로 인한 작업량이 많을 것 같아 스레드 풀을 분리하여 각 기능에 적합하게 구성하고자 했습니다.
hellozo0
left a comment
There was a problem hiding this comment.
리뷰가 너무 늦어서 죄송합니다.. ㅠㅡㅠ
다른 분들께서 꼼꼼하게 리뷰를 다 적용해주신거 같아서 다음 스텝부터는 빠르게 리뷰 달아 보겠습니다!
수고하셨습니다!
구현 요구 사항
구현