💻문제점
후원을 하면 "{nickname}님이 {points}포인트 후원!" 그리고 후원 메시지를 띄우려고 했다. 어떻게 알림을 띄울까 하다가 어차피 이미 RSocket을 사용하고 있으니, 아프리카TV처럼 채팅창에 후원 내역을 띄우면 될 것 같았다.
ChatController
@MessageMapping("donation")
Mono<DonationDto> donation(DonationDto donationDto){
return chatService.donation(donationDto);
}
ChatService
public Mono<DonationDto> donation(DonationDto donationDto) {
this.sendDonation(donationDto);
return Mono.just(donationDto);
}
public void sendDonation(DonationDto donationDto) {
Flux.fromIterable(participants.get(donationDto.getChattingAddress()))
.doOnNext(ea -> {
ea.route("")
.data(donationDto)
.send()
.subscribe();
})
.subscribe();
}
포인트를 사용하는 로직은 따로 API를 만들어두고, 후원 내역을 채팅창에 띄우는 로직을 ChatService에 작성했는데, 생각해보니, 포인트 사용 로직에서 비관적 락을 통해 데이터 무결성을 보장해주었기 때문에 동시에 요청이 발생하면 다른 요청은 락을 얻기 전까지 대기하게 된다. 하지만 채팅창에는 후원 정보가 뜨게 될 것이다.
아직 후원 처리가 안 됐는데 알림이 가는건 이상할 것 같고... 이것도 동시성 문제가 있지 않을까? 싶어서 후원하는 API를 없애고 RSocket 에 요청을 보낼 때 함께 처리하도록 했다.
🔍해결
ChatService
public Mono<DonationDto> donation(DonationDto donationDto) {
return usePoint(donationDto);
}
public Mono<DonationDto> usePoint(DonationDto donationDto) {
return memberRepository.findByNickname(donationDto.getStreamer())
.switchIfEmpty(Mono.error(() -> new NoSuchElementException("존재하지 않는 사용자입니다.")))
.flatMap(streamer -> pointRepository.findByNickname(donationDto.getNickname())
.switchIfEmpty(Mono.error(() -> new NoSuchElementException("존재하지 않는 사용자입니다.")))
.flatMap(memberPoints -> {
if (memberPoints.getPoints() - donationDto.getPoints() < 0) {
return Mono.error(() -> new NoSuchElementException("포인트가 부족합니다."));
}
return databaseClient.sql("SELECT * FROM points WHERE user_id = :userId FOR UPDATE")
.bind("userId", streamer.getUserId())
.fetch()
.one()
.flatMap(row -> {
Points updatedStreamerPoints = new Points(
(Long) row.get("id"),
(String) row.get("user_id"),
(int) row.get("points")
);
updatedStreamerPoints.addPoints(donationDto.getPoints());
return pointRepository.save(memberPoints.usePoints(donationDto.getPoints()))
.flatMap(savedMemberPoints -> {
return pointRepository.save(updatedStreamerPoints)
.flatMap(res -> sendDonation(donationDto))
.thenReturn(donationDto);
});
});
}));
}
public Mono<Void> sendDonation(DonationDto donationDto) {
return Flux.fromIterable(participants.get(donationDto.getChattingAddress()))
.flatMap(ea -> ea.route("")
.data(donationDto)
.send())
.then();
}
기존 후원 API에서는 userId를 받아 pointRepository.findByUserId를 수행했었는데 위 로직을 보면 다음과 같이 바뀌어져 있다.
//변경 전
.flatMap(streamer -> pointRepository.findByUserId(member.getUserId())
//변경 후
.flatMap(streamer -> pointRepository.findByNickname(donationDto.getNickname())
처음에는 프론트에서 nickname이 아니라 userId를 받도록 해야하나 했지만, 후원 정보를 출력할 때 닉네임을 사용하기 때문에 nickname은 받아야 했다. 오히려 userId가 findByUserId하는 데에만 사용되기 때문에 필요성을 따지면 userId가 불필요 했다.
하지만 points 테이블에는 nickname 컬럼은 없고, userId 컬럼만 있다.
그렇다면 memberRepository에서 findByUserId로 Member 객체를 얻어, pointsRepository에서 findByUserId(member.getNickname) 를 수행해야할까? 그렇게 할 수도 있지만 너무 복잡하고 가독성이 좋을 것 같지는 않았다.
그래서 다음과 같이 userId로 points와 member 테이블을 join하여 Points 객체를 얻도록 했다.
public interface PointRepository extends ReactiveCrudRepository<Points, Long> {
Mono<Points> findByUserId(String userId);
@Query("SELECT p.* FROM points p JOIN member m ON p.user_id = m.user_id WHERE m.nickname = :nickname")
Mono<Points> findByNickname(String nickname);
}
프론트에서 츄르를 후원해보자.
내가 보낸 후원도, 다른 사람이 보낸 후원도 잘 뜨는걸 확인할 수 있다.
'TIL' 카테고리의 다른 글
[커뮤 프로젝트] 서버 아키텍처 변경을 통한 성능 개선 (0) | 2023.07.02 |
---|---|
[TIL - 20230628] 후원 비관적 락 -> Redis 분산 락 변경 (동시성) (0) | 2023.06.29 |
[TIL - 20230622] K6 성능 테스트 시나리오 작성 (0) | 2023.06.23 |
[TIL - 20230621] Webflux R2DBC 후원 동시성 문제 해결 (0) | 2023.06.22 |
[TIL - 20230620] Webflux CORS (0) | 2023.06.21 |