Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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
@@ -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<String, Long> {
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<String, Long> cacheLoader,
CacheWriter<String, Long> cacheWriter,
CacheReader<String, Long> cacheReader,
ExamJpaRepository examJpaRepository
) {
super(cacheLoader, cacheWriter, cacheReader);
this.examJpaRepository = examJpaRepository;
}

private final RedisTemplate<String, Long> redisTemplate;
private final ExamJpaRepository examJpaRepository;
public Optional<Long> getMaxCapacity(String schoolName) {
String key = ExamQuotaPrefix.MAX_CAPACITY.with(schoolName);
return cacheReader.read(key);
}

public void cacheSchoolMaxCapacities() {
List<ExamJpaEntity> exams = examJpaRepository.findUpcomingExamInfo(LocalDate.now());
for (ExamJpaEntity exam : exams) {
addSchoolMaxCapacity(
exam.getSchoolName(),
exam.getCapacity()
);
}
public Optional<Long> getCurrentApplications(String schoolName) {
String key = ExamQuotaPrefix.CURRENT_APPLICATIONS.with(schoolName);
return cacheReader.read(key);
}

public void cacheSchoolCurrentApplicationCounts() {
List<SchoolExamCountProjection> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ExamQuotaEvent> handler = resolver.resolve(event);
handler.accept(event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ExamResponse> getByArea(String areaName) {
Area area = Area.from(areaName);
List<ExamJpaEntity> foundExams = examJpaRepository.findByArea(area);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExamQuotaEvent> 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<ExamQuotaEvent> resolveLoad(ExamQuotaEvent event) {
return e -> {
examQuotaCacheManager.setMaxCapacity(e.getSchoolName(), e.getValue());
examQuotaCacheManager.setCurrentApplications(e.getSchoolName(),0L);
};
}
private Consumer<ExamQuotaEvent> resolveDeleteAll(ExamQuotaEvent event){
return e -> {
examQuotaCacheManager.deleteMaxCapacity(e.getSchoolName());
examQuotaCacheManager.deleteCurrentApplications(e.getSchoolName());
};
}
private Consumer<ExamQuotaEvent> 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<ExamQuotaEvent> 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());
};
}
}

This file was deleted.

19 changes: 9 additions & 10 deletions src/main/java/life/mosu/mosuserver/infra/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@

@Configuration
public class SwaggerConfig {

private final List<Server> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package life.mosu.mosuserver.domain.admin;
package life.mosu.mosuserver.infra.persistence.jpa;

import static life.mosu.mosuserver.domain.base.BaseTimeEntity.formatDate;

import com.querydsl.core.Tuple;
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package life.mosu.mosuserver.infra.persistence.redis;

public interface CacheEvictor<K> {
void evict(K key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package life.mosu.mosuserver.infra.persistence.redis;

import java.util.Map;

public interface CacheLoader<K,V> {
void loadAll(Map<K, V> values);

void load(K key, V value);

boolean exists(K key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package life.mosu.mosuserver.infra.persistence.redis;

import java.util.Optional;

public interface CacheReader<K,V> {
Optional<V> read(K key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package life.mosu.mosuserver.infra.persistence.redis;

public interface CacheWriter<K,V> {
void writeOrUpdate(K key, V value);
void increase(K key);
void decrease(K key);
void delete(K key);
}
Loading