diff --git a/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java b/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java index 0482a922..42959234 100644 --- a/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java +++ b/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java @@ -1,79 +1,96 @@ package life.mosu.mosuserver.application.exam; import java.time.LocalDate; -import java.util.List; -import life.mosu.mosuserver.domain.exam.ExamJpaEntity; +import java.util.Optional; import life.mosu.mosuserver.domain.exam.ExamJpaRepository; -import life.mosu.mosuserver.domain.exam.projection.SchoolExamCountProjection; +import life.mosu.mosuserver.infra.persistence.redis.CacheLoader; +import life.mosu.mosuserver.infra.persistence.redis.CacheReader; +import life.mosu.mosuserver.infra.persistence.redis.CacheWriter; +import life.mosu.mosuserver.infra.persistence.redis.KeyValueCacheManager; +import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; -@Service -@RequiredArgsConstructor -public class ExamQuotaCacheManager { +@Component +@Slf4j +public class ExamQuotaCacheManager extends KeyValueCacheManager { + private final ExamJpaRepository examJpaRepository; - private static final String REDIS_KEY_SCHOOL_MAX_CAPACITY = "school:max_capacity:"; - private static final String REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS = "school:current_applications:"; + public ExamQuotaCacheManager( + CacheLoader cacheLoader, + CacheWriter cacheWriter, + CacheReader cacheReader, + ExamJpaRepository examJpaRepository + ) { + super(cacheLoader, cacheWriter, cacheReader); + this.examJpaRepository = examJpaRepository; + } - private final RedisTemplate redisTemplate; - private final ExamJpaRepository examJpaRepository; + public Optional getMaxCapacity(String schoolName) { + String key = ExamQuotaPrefix.MAX_CAPACITY.with(schoolName); + return cacheReader.read(key); + } - public void cacheSchoolMaxCapacities() { - List exams = examJpaRepository.findUpcomingExamInfo(LocalDate.now()); - for (ExamJpaEntity exam : exams) { - addSchoolMaxCapacity( - exam.getSchoolName(), - exam.getCapacity() - ); - } + public Optional getCurrentApplications(String schoolName) { + String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName); + return cacheReader.read(key); } - public void cacheSchoolCurrentApplicationCounts() { - List schoolExamCounts = examJpaRepository.countApplicationsGroupedBySchoolName(); - for (SchoolExamCountProjection projection : schoolExamCounts) { - addSchoolCurrentApplicationCount( - projection.schoolName(), - projection.applicationCount() - ); - } + public void setMaxCapacity(String schoolName, Long maxCapacity) { + String key = ExamQuotaPrefix.MAX_CAPACITY.with(schoolName); + cacheWriter.writeOrUpdate(key, maxCapacity); } - public void addSchoolMaxCapacity(String schoolName, Integer capacity) { - String key = REDIS_KEY_SCHOOL_MAX_CAPACITY + schoolName; - redisTemplate.opsForValue().set(key, Long.valueOf(capacity)); + public void setCurrentApplications(String schoolName, Long currentApplications) { + String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName); + cacheWriter.writeOrUpdate(key, currentApplications); } - public void addSchoolCurrentApplicationCount(String schoolName, Long currentCount) { - String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName; - redisTemplate.opsForValue().set(key, currentCount); + public void deleteMaxCapacity(String schoolName) { + String key = ExamQuotaPrefix.MAX_CAPACITY.with(schoolName); + cacheWriter.delete(key); } - public Long getSchoolApplicationCounts(String schoolName) { - return redisTemplate.opsForValue() - .get(REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName); + public void deleteCurrentApplications(String schoolName) { + String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName); + cacheWriter.delete(key); } - public Long getSchoolCapacities(String schoolName) { - return redisTemplate.opsForValue() - .get(REDIS_KEY_SCHOOL_MAX_CAPACITY + schoolName); + public void increaseCurrentApplications(String schoolName) { + String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName); + cacheWriter.increase(key); } - public void increaseApplicationCount(String schoolName) { - String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName; - redisTemplate.opsForValue().increment(key); + public void decreaseCurrentApplications(String schoolName) { + String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName); + cacheWriter.decrease(key); } - public void decreaseApplicationCount(String schoolName) { - String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName; - Long currentValue = redisTemplate.opsForValue().get(key); - if (currentValue != null && currentValue > 0) { - redisTemplate.opsForValue().decrement(key); - } + @Override + protected void initCache() { + examJpaRepository.findUpcomingExamInfo(LocalDate.now()) + .forEach(exam -> setMaxCapacity( + exam.getSchoolName(), + exam.getCapacity().longValue() + )); + examJpaRepository.countApplicationsGroupedBySchoolName() + .forEach(projection -> setCurrentApplications( + projection.schoolName(), + projection.applicationCount() + )); } - public void preloadSchoolData() { - cacheSchoolMaxCapacities(); - cacheSchoolCurrentApplicationCounts(); + @RequiredArgsConstructor + @Getter + private enum ExamQuotaPrefix { + MAX_CAPACITY("school:max_capacity:"), + CURRENT_APPLICATIONS("school:current_applications:"); + + private final String prefix; + + public String with(String schoolName) { + return prefix + schoolName; + } } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaService.java b/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaService.java new file mode 100644 index 00000000..d83ddbbf --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaService.java @@ -0,0 +1,19 @@ +package life.mosu.mosuserver.application.exam; + +import java.util.function.Consumer; +import life.mosu.mosuserver.application.exam.resolver.ExamQuotaEventResolver; +import life.mosu.mosuserver.presentation.exam.dto.event.ExamQuotaEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ExamQuotaService { + + private final ExamQuotaEventResolver resolver; + + public void handleExamQuotaEvent(ExamQuotaEvent event) { + Consumer handler = resolver.resolve(event); + handler.accept(event); + } +} diff --git a/src/main/java/life/mosu/mosuserver/application/exam/ExamService.java b/src/main/java/life/mosu/mosuserver/application/exam/ExamService.java index 57ee203c..d04c9f6f 100644 --- a/src/main/java/life/mosu/mosuserver/application/exam/ExamService.java +++ b/src/main/java/life/mosu/mosuserver/application/exam/ExamService.java @@ -6,7 +6,10 @@ import life.mosu.mosuserver.domain.exam.ExamJpaRepository; import life.mosu.mosuserver.presentation.exam.dto.ExamRequest; import life.mosu.mosuserver.presentation.exam.dto.ExamResponse; +import life.mosu.mosuserver.presentation.exam.dto.event.ExamQuotaEvent; +import life.mosu.mosuserver.presentation.exam.dto.event.ExamQuotaEventType; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @Service @@ -15,21 +18,18 @@ public class ExamService { private final ExamJpaRepository examJpaRepository; private final ExamQuotaCacheManager examQuotaCacheManager; + private final ApplicationEventPublisher publisher; public void register(ExamRequest request) { ExamJpaEntity exam = request.toEntity(); ExamJpaEntity savedExam = examJpaRepository.save(exam); - examQuotaCacheManager.addSchoolCurrentApplicationCount( + publisher.publishEvent(ExamQuotaEvent.ofMultipleCommand( + ExamQuotaEventType.LOAD, savedExam.getSchoolName(), - 0L - ); - examQuotaCacheManager.addSchoolMaxCapacity( - savedExam.getSchoolName(), - savedExam.getCapacity() - ); + savedExam.getCapacity().longValue() + )); } - public List getByArea(String areaName) { Area area = Area.from(areaName); List foundExams = examJpaRepository.findByArea(area); diff --git a/src/main/java/life/mosu/mosuserver/application/exam/resolver/ExamQuotaEventResolver.java b/src/main/java/life/mosu/mosuserver/application/exam/resolver/ExamQuotaEventResolver.java new file mode 100644 index 00000000..d00ff097 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/exam/resolver/ExamQuotaEventResolver.java @@ -0,0 +1,60 @@ +package life.mosu.mosuserver.application.exam.resolver; + +import java.util.function.Consumer; +import life.mosu.mosuserver.application.exam.ExamQuotaCacheManager; +import life.mosu.mosuserver.presentation.exam.dto.event.ApplicationStatus; +import life.mosu.mosuserver.presentation.exam.dto.event.ExamQuotaEvent; +import life.mosu.mosuserver.presentation.exam.dto.event.MaxCapacityStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExamQuotaEventResolver { + + private final ExamQuotaCacheManager examQuotaCacheManager; + + public Consumer resolve(ExamQuotaEvent event) { + return switch (event.getType()) { + case LOAD -> resolveLoad(event); + case DELETE_ALL -> resolveDeleteAll(event); + case CURRENT_APPLICATION -> resolveCurrentApplication(event); + case MAX_CAPACITY -> resolveMaxCapacity(event); + }; + } + + private Consumer resolveLoad(ExamQuotaEvent event) { + return e -> { + examQuotaCacheManager.setMaxCapacity(e.getSchoolName(), e.getValue()); + examQuotaCacheManager.setCurrentApplications(e.getSchoolName(),0L); + }; + } + private Consumer resolveDeleteAll(ExamQuotaEvent event){ + return e -> { + examQuotaCacheManager.deleteMaxCapacity(e.getSchoolName()); + examQuotaCacheManager.deleteCurrentApplications(e.getSchoolName()); + }; + } + private Consumer resolveCurrentApplication(ExamQuotaEvent event) { + if (!(event.getStatus() instanceof ApplicationStatus status)) { + throw new IllegalArgumentException("Invalid status for CURRENT_APPLICATION event"); + } + + return switch (status) { + case CREATE -> e -> examQuotaCacheManager.setCurrentApplications(e.getSchoolName(), e.getValue()); + case INCREASE -> e -> examQuotaCacheManager.increaseCurrentApplications(e.getSchoolName()); + case DECREASE -> e -> examQuotaCacheManager.decreaseCurrentApplications(e.getSchoolName()); + case DELETE -> e -> examQuotaCacheManager.deleteCurrentApplications(e.getSchoolName()); + }; + } + + private Consumer resolveMaxCapacity(ExamQuotaEvent event) { + if (!(event.getStatus() instanceof MaxCapacityStatus status)) { + throw new IllegalArgumentException("Invalid status for MAX_CAPACITY event"); + } + return switch (status) { + case CREATE, UPDATE -> e -> examQuotaCacheManager.setMaxCapacity(e.getSchoolName(), e.getValue()); + case DELETE -> e -> examQuotaCacheManager.deleteMaxCapacity(e.getSchoolName()); + }; + } +} diff --git a/src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java b/src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java deleted file mode 100644 index a44035b9..00000000 --- a/src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java +++ /dev/null @@ -1,19 +0,0 @@ -package life.mosu.mosuserver.global.runner; - -import life.mosu.mosuserver.application.exam.ExamQuotaCacheManager; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class ApplicationSchoolPreWarmRunner { - - private final ExamQuotaCacheManager examQuotaCacheManager; - - @EventListener(ApplicationReadyEvent.class) - public void preloadSchoolData() { - examQuotaCacheManager.preloadSchoolData(); - } -} diff --git a/src/main/java/life/mosu/mosuserver/infra/config/SwaggerConfig.java b/src/main/java/life/mosu/mosuserver/infra/config/SwaggerConfig.java index d2bdd88d..b3b82aac 100644 --- a/src/main/java/life/mosu/mosuserver/infra/config/SwaggerConfig.java +++ b/src/main/java/life/mosu/mosuserver/infra/config/SwaggerConfig.java @@ -9,21 +9,20 @@ @Configuration public class SwaggerConfig { - + private final List servers = List.of( + new Server().url("https://api.mosuedu.com/api/v1") + .description("MOSU SERVER"), + new Server().url("http://localhost:8080/api/v1") + .description("Local Development Server"), + new Server().url("http://192.168.35.174:8080/api/v1") + .description("Custom Development Server") + ); @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("MOSU API 문서") .version("1.0.0") - ).servers(List.of( - new Server().url("https://api.mosuedu.com/api/v1") - .description("MOSU SERVER"), - new Server().url("http://localhost:8080/api/v1") - .description("Local Development Server"), - - new Server().url("http://192.168.35.174:8080/api/v1") - .description("Custom Development Server") - )); + ).servers(servers); } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/ApplicationQueryRepositoryImpl.java similarity index 98% rename from src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java rename to src/main/java/life/mosu/mosuserver/infra/persistence/jpa/ApplicationQueryRepositoryImpl.java index 2beba874..b1e8af7a 100644 --- a/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/ApplicationQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package life.mosu.mosuserver.domain.admin; +package life.mosu.mosuserver.infra.persistence.jpa; import com.querydsl.core.Tuple; import com.querydsl.core.types.Predicate; @@ -11,6 +11,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import life.mosu.mosuserver.domain.admin.ApplicationQueryRepository; import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; import life.mosu.mosuserver.domain.application.QExamTicketImageJpaEntity; import life.mosu.mosuserver.domain.application.Subject; diff --git a/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/RefundQueryRepositoryImpl.java similarity index 97% rename from src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java rename to src/main/java/life/mosu/mosuserver/infra/persistence/jpa/RefundQueryRepositoryImpl.java index 032596fc..90a20149 100644 --- a/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/RefundQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package life.mosu.mosuserver.domain.admin; +package life.mosu.mosuserver.infra.persistence.jpa; import static life.mosu.mosuserver.domain.base.BaseTimeEntity.formatDate; @@ -6,6 +6,7 @@ import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; +import life.mosu.mosuserver.domain.admin.RefundQueryRepository; import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; import life.mosu.mosuserver.domain.examapplication.QExamApplicationJpaEntity; import life.mosu.mosuserver.domain.payment.PaymentMethod; diff --git a/src/main/java/life/mosu/mosuserver/domain/admin/StudentQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/StudentQueryRepositoryImpl.java similarity index 98% rename from src/main/java/life/mosu/mosuserver/domain/admin/StudentQueryRepositoryImpl.java rename to src/main/java/life/mosu/mosuserver/infra/persistence/jpa/StudentQueryRepositoryImpl.java index e77a1ed5..b81a5389 100644 --- a/src/main/java/life/mosu/mosuserver/domain/admin/StudentQueryRepositoryImpl.java +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/jpa/StudentQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package life.mosu.mosuserver.domain.admin; +package life.mosu.mosuserver.infra.persistence.jpa; import com.querydsl.core.Tuple; import com.querydsl.core.types.Predicate; @@ -7,6 +7,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; +import life.mosu.mosuserver.domain.admin.StudentQueryRepository; import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; import life.mosu.mosuserver.domain.profile.Education; import life.mosu.mosuserver.domain.profile.Gender; diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheEvictor.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheEvictor.java new file mode 100644 index 00000000..284edd0b --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheEvictor.java @@ -0,0 +1,5 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +public interface CacheEvictor { + void evict(K key); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheLoader.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheLoader.java new file mode 100644 index 00000000..e6ba2855 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheLoader.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import java.util.Map; + +public interface CacheLoader { + void loadAll(Map values); + + void load(K key, V value); + + boolean exists(K key); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheReader.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheReader.java new file mode 100644 index 00000000..c01e982a --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheReader.java @@ -0,0 +1,7 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import java.util.Optional; + +public interface CacheReader { + Optional read(K key); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheWriter.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheWriter.java new file mode 100644 index 00000000..02f8fdd9 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/CacheWriter.java @@ -0,0 +1,8 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +public interface CacheWriter { + void writeOrUpdate(K key, V value); + void increase(K key); + void decrease(K key); + void delete(K key); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheEvictor.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheEvictor.java new file mode 100644 index 00000000..789c0337 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheEvictor.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; + +@RequiredArgsConstructor +public class DefaultRedisCacheEvictor implements CacheEvictor { + protected final RedisTemplate redisTemplate; + + @Override + public void evict(String key) { + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheLoader.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheLoader.java new file mode 100644 index 00000000..3c47e9a1 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheLoader.java @@ -0,0 +1,29 @@ + package life.mosu.mosuserver.infra.persistence.redis; + + import java.util.Map; + import lombok.RequiredArgsConstructor; + import org.springframework.data.redis.core.RedisTemplate; + import org.springframework.stereotype.Component; + + @Component + @RequiredArgsConstructor + public class DefaultRedisCacheLoader implements CacheLoader { + protected final RedisTemplate redisTemplate; + + @Override + public void loadAll(Map values) { + values.forEach(this::load); + } + + @Override + public void load(String key, T value) { + + redisTemplate.opsForValue().set(key, value); + } + + @Override + public boolean exists(String key) { + Boolean result = redisTemplate.hasKey(key); + return result != null && result; + } + } diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheReader.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheReader.java new file mode 100644 index 00000000..812f5ab7 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheReader.java @@ -0,0 +1,18 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DefaultRedisCacheReader implements CacheReader { + protected final RedisTemplate redisTemplate; + + + @Override + public Optional read(String key) { + return Optional.ofNullable(redisTemplate.opsForValue().get(key)); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheWriter.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheWriter.java new file mode 100644 index 00000000..3640678d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/DefaultRedisCacheWriter.java @@ -0,0 +1,31 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DefaultRedisCacheWriter implements CacheWriter { + protected final RedisTemplate redisTemplate; + + @Override + public void writeOrUpdate(String key, T value) { + redisTemplate.opsForValue().set(key, value); + } + + @Override + public void increase(String key) { + redisTemplate.opsForValue().increment(key, 1); + } + + @Override + public void decrease(String key) { + redisTemplate.opsForValue().decrement(key, 1); + } + + @Override + public void delete(String key) { + redisTemplate.delete(key); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/KeyValueCacheManager.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/KeyValueCacheManager.java new file mode 100644 index 00000000..c9616f2c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/KeyValueCacheManager.java @@ -0,0 +1,17 @@ +package life.mosu.mosuserver.infra.persistence.redis; + +import jakarta.annotation.PostConstruct; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class KeyValueCacheManager { + protected final CacheLoader cacheLoader; + protected final CacheWriter cacheWriter; + protected final CacheReader cacheReader; + @PostConstruct + private void init(){ + initCache(); + } + protected abstract void initCache(); +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/ExamQuotaEventListener.java b/src/main/java/life/mosu/mosuserver/presentation/exam/ExamQuotaEventListener.java new file mode 100644 index 00000000..d39b8f2b --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/ExamQuotaEventListener.java @@ -0,0 +1,18 @@ +package life.mosu.mosuserver.presentation.exam; + +import life.mosu.mosuserver.application.exam.ExamQuotaService; +import life.mosu.mosuserver.global.annotation.ReactiveEventListener; +import life.mosu.mosuserver.presentation.exam.dto.event.ExamQuotaEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExamQuotaEventListener { + private final ExamQuotaService examQuotaService; + + @ReactiveEventListener + public void handleExamQuotaEvent(ExamQuotaEvent event) { + examQuotaService.handleExamQuotaEvent(event); + } +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ApplicationStatus.java b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ApplicationStatus.java new file mode 100644 index 00000000..bbb2d161 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ApplicationStatus.java @@ -0,0 +1,5 @@ +package life.mosu.mosuserver.presentation.exam.dto.event; + +public enum ApplicationStatus implements ExamQuotaStatus { + INCREASE, CREATE, DECREASE, DELETE +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEvent.java b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEvent.java new file mode 100644 index 00000000..eaf9f504 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEvent.java @@ -0,0 +1,23 @@ +package life.mosu.mosuserver.presentation.exam.dto.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(staticName = "ofSingleCommand") +public class ExamQuotaEvent { + private final ExamQuotaEventType type; + private final String schoolName; + private final Long value; + private final Enum status; + + public ExamQuotaEvent(ExamQuotaEventType type, String schoolName, Long value) { + this.type = type; + this.schoolName = schoolName; + this.value = value; + this.status = null; + } + public static ExamQuotaEvent ofMultipleCommand(ExamQuotaEventType type, String schoolName, Long value) { + return new ExamQuotaEvent(type, schoolName, value); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEventType.java b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEventType.java new file mode 100644 index 00000000..c4f9644d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaEventType.java @@ -0,0 +1,22 @@ +package life.mosu.mosuserver.presentation.exam.dto.event; + +import java.util.Set; + +public enum ExamQuotaEventType { + // Single Commands + CURRENT_APPLICATION(Set.of(ApplicationStatus.INCREASE, ApplicationStatus.CREATE, ApplicationStatus.DELETE)), + MAX_CAPACITY(Set.of(MaxCapacityStatus.CREATE, MaxCapacityStatus.UPDATE, MaxCapacityStatus.DELETE)), + + // Multiple Commands + LOAD(Set.of()), + DELETE_ALL(Set.of()); + private final Set validStatuses; + + ExamQuotaEventType(Set statuses) { + this.validStatuses = statuses; + } + + public boolean isValidStatus(ExamQuotaStatus status) { + return validStatuses.contains(status); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaStatus.java b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaStatus.java new file mode 100644 index 00000000..b73f8e58 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/ExamQuotaStatus.java @@ -0,0 +1,5 @@ +package life.mosu.mosuserver.presentation.exam.dto.event; + +public interface ExamQuotaStatus { + +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/MaxCapacityStatus.java b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/MaxCapacityStatus.java new file mode 100644 index 00000000..c006e65d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/exam/dto/event/MaxCapacityStatus.java @@ -0,0 +1,5 @@ +package life.mosu.mosuserver.presentation.exam.dto.event; + +public enum MaxCapacityStatus implements ExamQuotaStatus { + CREATE, UPDATE, DELETE +}