Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.planetrush.planetrush.planet.service;

import static com.planetrush.planetrush.planet.service.IdempotencyTTL.*;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class IdempotencyManager {

private final RedisTemplate<String, String> redisTemplate;

public boolean tryRegisterResident(Long memberId, Long planetId) {
String key = generateKey(REGISTER_RESIDENT.getPrefix(), memberId, planetId);
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(
key,
"true",
REGISTER_RESIDENT.amount,
REGISTER_RESIDENT.unit
)
);
}

public boolean tryDeleteResident(Long memberId, Long planetId) {
String key = generateKey(DELETE_RESIDENT.getPrefix(), memberId, planetId);
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(
key,
"true",
DELETE_RESIDENT.amount,
DELETE_RESIDENT.unit
)
);
}

private String generateKey(String prefix, Long memberId, Long planetId) {
return new StringBuffer()
.append(prefix)
.append(":")
.append(memberId)
.append(":")
.append(planetId)
.toString();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.planetrush.planetrush.planet.service;

import java.util.concurrent.TimeUnit;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum IdempotencyTTL {

REGISTER_RESIDENT("idempotent:register-resident", 10, TimeUnit.SECONDS),
DELETE_RESIDENT("idempotent:delete-resident", 10, TimeUnit.SECONDS),
;

final String prefix;
final int amount;
final TimeUnit unit;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package com.planetrush.planetrush.planet.service;

import static com.planetrush.planetrush.planet.service.PlanetPolicy.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -52,7 +47,8 @@
@RequiredArgsConstructor
public class PlanetServiceImpl implements PlanetService {

private final RedisTemplate redisTemplate;
private final IdempotencyManager idempotencyManager;
private final PlanetValidator planetValidator;

private final MemberRepository memberRepository;
private final PlanetRepository planetRepository;
Expand Down Expand Up @@ -306,25 +302,20 @@ private GetMainPlanetListDto convertToDto(GetMainPlanetListVo vo) {
@Transactional
@Override
public void registerResident(PlanetSubscriptionDto dto) {
if (!idempotencyManager.tryRegisterResident(dto.getMemberId(), dto.getPlanetId())) {
log.warn("[IDEMPOTENT] 행성 가입 중복 요청, member={}, planet={}", dto.getMemberId(), dto.getPlanetId());
throw new DuplicatedRegisterResidentRequestException();
}
Member member = memberRepository.findById(dto.getMemberId())
.orElseThrow(() -> new MemberNotFoundException("Member not found with ID: " + dto.getMemberId()));
Planet planet = planetRepository.findByIdForUpdate(dto.getPlanetId())
.orElseThrow(() -> new PlanetNotFoundException("Planet not found with ID: " + dto.getPlanetId()));
ValueOperations ops = redisTemplate.opsForValue();
Boolean isFirstRequest = ops.setIfAbsent(
"idempotent:register-resident:" + "member:" + member.getId() + "planet:" + planet.getId(),
"true", 10,
TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isFirstRequest)) {
log.error("[IDEMPOTENT] 행성 가입 중복 요청 발생, 회원={}, 행성={}", member.getId(), planet.getId());
throw new DuplicatedRegisterResidentRequestException();
}
validateResidentLimit(member, residentRepositoryCustom);
validateDuplicateResident(member, planet, residentRepository);
planetValidator.checkMaxResidentLimit(member);
planetValidator.checkDuplicatedRegister(member, planet);
planet.addParticipant();
residentRepository.save(Resident.isNotCreator(member, planet));
}

/**
* {@inheritDoc}
*
Expand All @@ -334,19 +325,14 @@ public void registerResident(PlanetSubscriptionDto dto) {
@Transactional
@Override
public void deleteResident(PlanetSubscriptionDto dto) {
if (!idempotencyManager.tryDeleteResident(dto.getMemberId(), dto.getPlanetId())) {
log.error("[IDEMPOTENT] 행성 탈퇴 중복 요청 발생, 회원={}, 행성={}", dto.getMemberId(), dto.getPlanetId());
throw new DuplicatedDeleteResidentRequestException();
}
Member member = memberRepository.findById(dto.getMemberId())
.orElseThrow(() -> new MemberNotFoundException("Member not found with ID: " + dto.getMemberId()));
Planet planet = planetRepository.findByIdForUpdate(dto.getPlanetId())
.orElseThrow(() -> new PlanetNotFoundException("Planet not found with ID: " + dto.getPlanetId()));
ValueOperations ops = redisTemplate.opsForValue();
Boolean isFirstRequest = ops.setIfAbsent(
"idempotent:delete-resident:" + "member:" + member.getId() + "planet:" + planet.getId(),
"true", 10,
TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isFirstRequest)) {
log.error("[IDEMPOTENT] 행성 탈퇴 중복 요청 발생, 회원={}, 행성={}", member.getId(), planet.getId());
throw new DuplicatedDeleteResidentRequestException();
}
Resident resident = residentRepository.findByMemberIdAndPlanetId(member.getId(), planet.getId())
.orElseThrow(() -> new ResidentNotFoundException(
"Resident not found member id: " + member.getId() + " and planet id: " + planet.getId()));
Expand All @@ -362,8 +348,8 @@ public void deleteResident(PlanetSubscriptionDto dto) {
public void registerPlanet(RegisterPlanetDto dto) {
Member member = memberRepository.findById(dto.getMemberId())
.orElseThrow(() -> new MemberNotFoundException("Member not found with ID: " + dto.getMemberId()));
validateStartDateWithinTwoWeeks(dto.getStartDate());
validateResidentLimit(member, residentRepositoryCustom);
planetValidator.checkStartDate(dto.getStartDate());
planetValidator.checkMaxResidentLimit(member);
Planet planet = planetRepository.save(Planet.builder()
.name(dto.getName())
.category(Category.valueOf(dto.getCategory()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.time.temporal.ChronoUnit;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.planetrush.planetrush.member.domain.Member;
import com.planetrush.planetrush.planet.domain.Planet;
Expand All @@ -13,18 +14,23 @@
import com.planetrush.planetrush.planet.repository.ResidentRepository;
import com.planetrush.planetrush.planet.repository.custom.ResidentRepositoryCustom;

import lombok.RequiredArgsConstructor;

@Component
public final class PlanetPolicy {
@RequiredArgsConstructor
public class PlanetValidator {

private final ResidentRepository residentRepository;
private final ResidentRepositoryCustom residentRepositoryCustom;
private static final int MAX_RESIDENT_LIMIT = 9;
private static final int MAX_CHALLENGE_START_OFFSET = 14;

/**
* 가입 가능한 최대 챌린지 수를 초과하는지 검사합니다.
* @param member
* @param residentRepositoryCustom
*/
public static void validateResidentLimit(Member member, ResidentRepositoryCustom residentRepositoryCustom) {
@Transactional(readOnly = true)
public void checkMaxResidentLimit(Member member) {
if (residentRepositoryCustom.getReadyAndInProgressResidents(member) >= MAX_RESIDENT_LIMIT) {
throw new ResidentOverflowException("resident count overflow");
}
Expand All @@ -34,9 +40,9 @@ public static void validateResidentLimit(Member member, ResidentRepositoryCustom
* 동일한 행성에 중복 가입을 방지합니다.
* @param member
* @param planet
* @param residentRepository
*/
public static void validateDuplicateResident(Member member, Planet planet, ResidentRepository residentRepository) {
@Transactional(readOnly = true)
public void checkDuplicatedRegister(Member member, Planet planet) {
residentRepository.findByMemberIdAndPlanetId(member.getId(), planet.getId())
.ifPresent(resident -> {
throw new ResidentAlreadyExistsException("resident already exists: " + resident.getId());
Expand All @@ -47,8 +53,8 @@ public static void validateDuplicateResident(Member member, Planet planet, Resid
* 2주 이내로 시작하는지 검사합니다.
* @param startDate
*/
public static void validateStartDateWithinTwoWeeks(LocalDate startDate) {
if(ChronoUnit.DAYS.between(LocalDate.now(), startDate) > MAX_CHALLENGE_START_OFFSET) {
public void checkStartDate(LocalDate startDate) {
if (LocalDate.now().minus(MAX_CHALLENGE_START_OFFSET, ChronoUnit.DAYS).isAfter(startDate)) {
throw new InvalidStartDateException("Start date must be within 14 days from today.");
}
}
Expand Down
Loading