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

Feature/nyh365 step3 #31

Open
wants to merge 54 commits into
base: base/nyh365
Choose a base branch
from
Open

Feature/nyh365 step3 #31

wants to merge 54 commits into from

Conversation

nyh365
Copy link

@nyh365 nyh365 commented Feb 11, 2025

구현 요구 사항

  • 1/n 정산: 정확히 1/n로 정산을 진행합니다.
  • 랜덤 정산: 전체 금액을 벗어나지 않아야 하며, 랜덤으로 금액을 나눠 정산해야 합니다.

구현

  • 1/n 정산
    • 금액을 1/n 으로 나눈뒤 나머지 금액만큼 1원씩 앞에서부터 부여하도록 구현했습니다. 이후 랜덤으로 섞은 뒤 사용자에게 배분합니다.
  • 랜덤 정산
    • 금액 내에서 무작위로 나누어 배분하도록 구현했습니다.
  • 정산 내역을 저장
    • 정산 상세 내역을 저장하는데 여러 개의 insert 요청이 있을 것이라 생각해 bulk insert로 구현했습니다.

nyh365 added 30 commits January 27, 2025 16:57
- 다중환경에서 lost update로 인한 데이터 적합성 문제를 해결하기 위해 락을 사용했습니다.
- 은행 업무에서는 데이터 무결성을 지키는게 중요하다고 생각해서 비관적 락을 사용했습니다.
- 다중 환경에서 lost update로 인한 데이터 무결성 해결을 위해 비관적 락 사용
- 이후 확장성을 위해 int에서 long으로 변경
- 이후 확장성을 위해 int에서 long으로 변경
- 송금 내역 저장을 위함
- 메세지 큐에 메세지 전송을 위함
- 이벤트 리스너가 비동기적으로 실행되도록 하기 위함
- 이벤트 리스너를 통해 수령인의 잔액 업데이트 로직 분리
- 메세지 큐를 통해 수령인의 잔액 업데이트
@nyh365 nyh365 self-assigned this Feb 11, 2025
Copy link

@opixxx opixxx left a 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) {
Copy link

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) {
Copy link

Choose a reason for hiding this comment

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

@ResponseStatus나 ResponseEntity를 사용하지 않은 이유가 어차피 200 반환해서 굳이 사용 안하신건가요??

Copy link
Author

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;
Copy link

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();
Copy link

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());
Copy link

Choose a reason for hiding this comment

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

다른 곳에서 이벤트 리스너를 사용하셨으니 이 부분도 이벤트 리스너 사용해서 책임 분리를 하는 것은 어떻게 생각하시나요? 개인적인 의견이 궁금해서 남깁니다

Copy link
Author

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 {
Copy link

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;
Copy link

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 {
Copy link

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;
Copy link

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 = """
Copy link

Choose a reason for hiding this comment

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

현재 @Modifying을 사용하는 곳에서 1차 캐시로 인해 문제가 발생하지는 않을 것 같습니다만, 영속성 1차 캐시 문제에 대해서 한 번 알아두시면 좋아보여요

Copy link

@ZZAMBAs ZZAMBAs left a 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
Copy link

Choose a reason for hiding this comment

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

DTO에 빌더 패턴을 적용하신 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

단순히 가독성 때문에 사용했습니다.

Comment on lines 33 to 34
@Column(name = "main_account")
private Long mainAccount;
Copy link

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
Copy link

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()));
Copy link

Choose a reason for hiding this comment

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

userRepository.findByEmail() 코드 실행 후, 그 유저가 삭제되었다면 이 코드 실행에 문제가 있을 것 같아요.

Comment on lines 139 to 156
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;
}
Copy link

Choose a reason for hiding this comment

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

내부 메서드는 private로 선언하는 것이 좋을 것 같습니다

Comment on lines +16 to +24
@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);
}
Copy link

Choose a reason for hiding this comment

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

스레드 풀을 하나로 쓰지 않고 둘로 나눈 이유가 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

아직 학습 중이라 코드상에는 반영을 못했습니다. 스레드 풀을 분리한 이유는 이벤트 처리로 인한 작업량이 많을 것 같아 스레드 풀을 분리하여 각 기능에 적합하게 구성하고자 했습니다.

Copy link
Contributor

@hellozo0 hellozo0 left a comment

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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants