-
Notifications
You must be signed in to change notification settings - Fork 27
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
Feature/nyh365 step3 #31
base: base/nyh365
Are you sure you want to change the base?
Conversation
- 다중환경에서 lost update로 인한 데이터 적합성 문제를 해결하기 위해 락을 사용했습니다. - 은행 업무에서는 데이터 무결성을 지키는게 중요하다고 생각해서 비관적 락을 사용했습니다.
- 다중 환경에서 lost update로 인한 데이터 무결성 해결을 위해 비관적 락 사용
- 이후 확장성을 위해 int에서 long으로 변경
- 이후 확장성을 위해 int에서 long으로 변경
- 송금 내역 저장을 위함 - 메세지 큐에 메세지 전송을 위함
- 이벤트 리스너가 비동기적으로 실행되도록 하기 위함
- 이벤트 리스너를 통해 수령인의 잔액 업데이트 로직 분리 - 메세지 큐를 통해 수령인의 잔액 업데이트
- 이후 인덱스 설정을 위함
- jdbc를 이용한 bulk insert 구현
- 랜덤 정산과 1/N 정산을 type을 통해 분리
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.
Step1,2,3 수고하셨습니다~
private final AccountService accountService; | ||
|
||
@PostMapping("/savings") | ||
public void createSavingsAccount(@RequestBody @Valid PostSavingsAccountReq postSavingsAccountReq) { |
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.
void로 반환하면 204가 반환될 것 같아요. create라 201이 좋아보입니다.
accountService.createSavingsAccount(postSavingsAccountReq); | ||
} | ||
@PostMapping("/main/deposit") | ||
public MainAccountInfoRes depositMainAccount(@RequestBody @Valid PostMainAccountReq postMainAccountReq) { |
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.
@ResponseStatus나 ResponseEntity를 사용하지 않은 이유가 어차피 200 반환해서 굳이 사용 안하신건가요??
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.
네 그렇긴 한데..그러다 보니 create 같은 요청에 적절한 응답 코드를 반환하는 걸 놓쳤네요. 명시적으로 반환하도록 수정하여 반영하겠습니다.
@Service | ||
@RequiredArgsConstructor | ||
public class AccountService { | ||
private static final long CHARGE_UNIT = 10_000; |
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.
10_000L
* 랜덤 정산 금액 계산 | ||
*/ | ||
public List<Long> generateRandomAmount(int count, long totalAmount) { | ||
ThreadLocalRandom random = ThreadLocalRandom.current(); |
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.
저도 ThreadLocalRandom 사용했는데 Random 처럼 보안 문제로 인해 PR이 거절됩니다
SecureRandom을 써야 될 것 같은데, 다른 방법이 있는지는 모르겠네요
.nickname(postUserReq.nickname()) | ||
.build()); | ||
|
||
Account mainAccount = new Account(user.getId()); |
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.
이벤트 리스너 사용 목적에 따라 다르긴한데, 제 생각에는 해당 기능에서 이벤트 리스너를 사용하는 이유가 책임을 분리하기 위한 것이라면 적용해도 괜찮을 것 같아요.
|
||
@Repository | ||
@RequiredArgsConstructor | ||
public class SettlementDetailRepository { |
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.
나중에 정산 인원이 많아지면 JdbcTemplate 사용해서 처리하는 것도 좋아보이네요! 배워갑니다
|
||
public SavingsAccount(long userId) { | ||
this.userId = userId; | ||
this.balance = 0L; |
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.
상수 처리해도 좋아보여요 INIT_BALANCE
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | ||
|
||
@RestControllerAdvice | ||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { |
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.
👍
@Entity | ||
@Table(name = "account") | ||
public class Account extends BaseEntity { | ||
private static final long DEFAULT_CHARGE_LIMIT = 3_000_000; |
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.
3_000_000L
public interface TransferTransactionJpaRepository extends JpaRepository<TransferTransaction, Long> { | ||
@Transactional | ||
@Modifying | ||
@Query(value = """ |
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.
현재 @Modifying을 사용하는 곳에서 1차 캐시로 인해 문제가 발생하지는 않을 것 같습니다만, 영속성 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.
고생하셨습니다. RabbitMQ, Spring Event 코드 사용 등을 통해 이벤트 처리를 효과적으로 구현하신 것 같아요. 많은 인사이트를 얻은 것 같습니다. 👍 이벤트 처리와 메시지 큐 활용 부분의 지식이 적어 그 부분에 대한 코드 리뷰는 제가 더 학습한 후에 추가하겠습니다!
long receiverMainAccount; | ||
long amount; | ||
|
||
@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.
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.
단순히 가독성 때문에 사용했습니다.
@Column(name = "main_account") | ||
private Long mainAccount; |
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.
mainAccountId
가 더 명확할 것 같아요
@Query("SELECT a FROM Account a WHERE a.id = :id") | ||
public Optional<Account> findByIdWithWriteLock(@Param("id") Long id); | ||
|
||
@Transactional |
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.
인터페이스에 적어도 동작하는군요.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
} |
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
로 선언하는 것이 좋을 것 같습니다
@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.
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.
리뷰가 너무 늦어서 죄송합니다.. ㅠㅡㅠ
다른 분들께서 꼼꼼하게 리뷰를 다 적용해주신거 같아서 다음 스텝부터는 빠르게 리뷰 달아 보겠습니다!
수고하셨습니다!
구현 요구 사항
구현