Skip to content

Commit

Permalink
#244 fix: FCM 토큰 등록 쓰레드 세이프 문제 해결
Browse files Browse the repository at this point in the history
- fcm_token 테이블에 token, memberId 컬럼에 유니크 제약 조건 설정
  • Loading branch information
yonghwankim-dev committed Feb 28, 2024
1 parent ef958d5 commit e9f75a4
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import codesquad.fineants.domain.BaseEntity;
import codesquad.fineants.domain.member.Member;
Expand All @@ -19,6 +21,9 @@

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "fcm_token", uniqueConstraints = {
@UniqueConstraint(columnNames = {"token", "member_id"})
})
@Entity
public class FcmToken extends BaseEntity {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
public enum FcmErrorCode implements ErrorCode {

BAD_REQUEST_FCM_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 FCM 토큰입니다"),
CONFLICT_FCM_TOKEN(HttpStatus.CONFLICT, "중복된 FCM 토큰입니다");
CONFLICT_FCM_TOKEN(HttpStatus.CONFLICT, "중복된 FCM 토큰입니다"),
NOT_FOUND_FCM_TOKEN(HttpStatus.NOT_FOUND, "FCM 토큰을 찾을 수 없습니다");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@ToString
public class FcmRegisterResponse {
private Long fcmTokenId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -19,6 +20,7 @@
import codesquad.fineants.spring.api.errors.errorcode.FcmErrorCode;
import codesquad.fineants.spring.api.errors.errorcode.MemberErrorCode;
import codesquad.fineants.spring.api.errors.exception.BadRequestException;
import codesquad.fineants.spring.api.errors.exception.FineAntsException;
import codesquad.fineants.spring.api.errors.exception.NotFoundResourceException;
import codesquad.fineants.spring.api.fcm.request.FcmRegisterRequest;
import codesquad.fineants.spring.api.fcm.response.FcmDeleteResponse;
Expand All @@ -44,8 +46,14 @@ public FcmRegisterResponse createToken(FcmRegisterRequest request, AuthMember au
FcmToken fcmToken = fcmRepository.findByTokenAndMemberId(request.getFcmToken(), authMember.getMemberId())
.orElseGet(() -> request.toEntity(member));
fcmToken.refreshLatestActivationTime();
FcmToken saveFcmToken = fcmRepository.save(fcmToken);
return FcmRegisterResponse.from(saveFcmToken);
try {
FcmToken saveFcmToken = fcmRepository.save(fcmToken);
FcmRegisterResponse response = FcmRegisterResponse.from(saveFcmToken);
log.info("FCM Token 저장 결과 : {}", response);
return response;
} catch (DataIntegrityViolationException e) {
throw new FineAntsException(FcmErrorCode.CONFLICT_FCM_TOKEN);
}
}

private void verifyFcmToken(String token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import static org.mockito.BDDMockito.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
Expand All @@ -25,6 +30,7 @@
import codesquad.fineants.spring.AbstractContainerBaseTest;
import codesquad.fineants.spring.api.errors.errorcode.FcmErrorCode;
import codesquad.fineants.spring.api.errors.exception.BadRequestException;
import codesquad.fineants.spring.api.errors.exception.FineAntsException;
import codesquad.fineants.spring.api.fcm.request.FcmRegisterRequest;
import codesquad.fineants.spring.api.fcm.response.FcmDeleteResponse;
import codesquad.fineants.spring.api.fcm.response.FcmRegisterResponse;
Expand Down Expand Up @@ -69,6 +75,34 @@ void registerToken() throws FirebaseMessagingException {
assertThat(response.getFcmTokenId()).isGreaterThan(0);
}

@DisplayName("한 사용자가 동일한 토큰값으로 여러번의 토큰 등록을 요청해도 db에는 한개의 member_id, token 값쌍의 데이터가 있어야 한다")
@Test
void createToken_whenMultipleCreateFcmTokenAPI_thenOneFcmToken() {
// given
Member member = memberRepository.save(createMember());
FcmRegisterRequest request = FcmRegisterRequest.builder()
.fcmToken("token")
.build();
// when
List<CompletableFuture<FcmRegisterResponse>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CompletableFuture<FcmRegisterResponse> future = CompletableFuture.supplyAsync(() ->
fcmService.createToken(request, AuthMember.from(member)));
futures.add(future);
}

// 10개의 쓰레드가 전부 완료할때까지 대기
Throwable throwable = catchThrowable(() -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));

// then
assertThat(throwable)
.isInstanceOf(CompletionException.class)
.hasMessage(new FineAntsException(FcmErrorCode.CONFLICT_FCM_TOKEN).toString());
assertThat(fcmRepository.findAllByMemberId(member.getId())).hasSize(1);
}

@DisplayName("사용자는 유효하지 않은 FCM 토큰을 등록할 수 없다")
@Test
void registerToken_whenInvalidToken_thenThrow400Error() throws FirebaseMessagingException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,20 +409,26 @@ void readMyAllPortfolio_whenDailyGainIsMinus_thenDailGainRateIsMinus() {
Portfolio portfolio = portfolioRepository.save(createPortfolio(member));
Stock stock = stockRepository.save(createStock());
PortfolioHolding portfolioHolding = portFolioHoldingRepository.save(PortfolioHolding.empty(portfolio, stock));
PurchaseHistory purchaseHistory = purchaseHistoryRepository.save(
createPurchaseHistory(portfolioHolding, 3L, 90000.0));
purchaseHistoryRepository.save(createPurchaseHistory(portfolioHolding, 3L, 90000.0));
portfolioGainHistoryRepository.save(PortfolioGainHistory.builder()
.totalGain(-120000L)
.dailyGain(-120000L)
.cash(730000L)
.currentValuation(150000L)
.portfolio(portfolio)
.build());

given(currentPriceManager.hasCurrentPrice(anyString())).willReturn(true);
given(currentPriceManager.getCurrentPrice(anyString())).willReturn(50000L);
given(currentPriceManager.getCurrentPrice(anyString())).willReturn(40000L);

// when
PortfoliosResponse response = service.readMyAllPortfolio(AuthMember.from(member));

// then
assertThat(response.getPortfolios().get(0).getDailyGain()).isEqualTo(-120000L);
assertThat(response.getPortfolios().get(0).getDailyGainRate()).isEqualTo(-44);
assertThat(response.getPortfolios().get(0).getTotalGain()).isEqualTo(-120000L);
assertThat(response.getPortfolios().get(0).getTotalGainRate()).isEqualTo(-44);
assertThat(response.getPortfolios().get(0).getDailyGain()).isEqualTo(-30000);
assertThat(response.getPortfolios().get(0).getDailyGainRate()).isEqualTo(-20);
assertThat(response.getPortfolios().get(0).getTotalGain()).isEqualTo(-150000);
assertThat(response.getPortfolios().get(0).getTotalGainRate()).isEqualTo(-55);
}

@DisplayName("회원이 포트폴리오들을 삭제한다")
Expand Down

0 comments on commit e9f75a4

Please sign in to comment.