Skip to content

Commit

Permalink
Merge pull request #92 from solid-connection/main
Browse files Browse the repository at this point in the history
[RELEASE] 캐싱 도입 및 모니터링 환경 구축, 지원서 선택지 비필수 전환, 조회수 갱신 오류 수정, nginx max body size 조정
  • Loading branch information
leesewon00 authored Sep 7, 2024
2 parents 5a1d029 + bebc855 commit 4a0c7b5
Show file tree
Hide file tree
Showing 28 changed files with 630 additions and 33 deletions.
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

## 어떤 버그인가요

> 문제가 되는 부분에 대해 설명해주세요
## 재현 방법(선택)
버그를 재현할 수 있는 과정을 설명해주세요(필요하다면 사진을 첨부해주세요)
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## 참고할만한 자료(선택)
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ services:
ports:
- "6379:6379"

redis-exporter:
image: oliver006/redis_exporter
container_name: redis-exporter
ports:
- "9121:9121"
environment:
REDIS_ADDR: "redis:6379"
depends_on:
- redis

solid-connect-server:
build:
context: .
Expand Down
1 change: 1 addition & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ server {

ssl_certificate /etc/letsencrypt/live/api.solid-connect.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.solid-connect.net/privkey.pem;
client_max_body_size 10M;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on; # 클라이언트 보다 서버의 암호화 알고리즘을 우선하도록 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableJpaAuditing
@EnableCaching
@SpringBootApplication
public class SolidConnectionApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public ResponseEntity<ApplicationsResponse> getApplicants(
Principal principal,
@RequestParam(required = false, defaultValue = "") String region,
@RequestParam(required = false, defaultValue = "") String keyword) {
applicationQueryService.validateSiteUserCanViewApplicants(principal.getName());
ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword);
return ResponseEntity
.ok(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.solidconnection.application.dto.ApplicationsResponse;
import com.example.solidconnection.application.dto.UniversityApplicantsResponse;
import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
Expand Down Expand Up @@ -43,10 +44,10 @@ public class ApplicationQueryService {
* - 1지망, 2지망 지원자들을 조회한다.
* */
@Transactional(readOnly = true)
@ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400)
public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) {
// 유저가 다른 지원자들을 볼 수 있는지 검증
SiteUser siteUser = siteUserRepository.getByEmail(email);
validateSiteUserCanViewApplicants(siteUser);

// 국가와 키워드와 지역을 통해 대학을 필터링한다.
List<University> universities
Expand All @@ -61,8 +62,10 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin

// 학기별로 상태가 관리된다.
// 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다.
private void validateSiteUserCanViewApplicants(SiteUser siteUser) {
VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term).getVerifyStatus();
@Transactional(readOnly = true)
public void validateSiteUserCanViewApplicants(String email) {
SiteUser siteUser = siteUserRepository.getByEmail(email);
VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus();
if (verifyStatus != VerifyStatus.APPROVED) {
throw new CustomException(APPLICATION_NOT_APPROVED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.example.solidconnection.application.dto.ScoreRequest;
import com.example.solidconnection.application.dto.UniversityChoiceRequest;
import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.cache.annotation.DefaultCacheOut;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
Expand Down Expand Up @@ -40,6 +41,7 @@ public class ApplicationSubmissionService {
* - 수정을 하고 나면, 성적 승인 상태(verifyStatus)를 PENDING 상태로 변경한다.
* */
@Transactional
@DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true)
public boolean submitScore(String email, ScoreRequest scoreRequest) {
SiteUser siteUser = siteUserRepository.getByEmail(email);
Gpa gpa = scoreRequest.toGpa();
Expand Down Expand Up @@ -67,8 +69,9 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) {
* - 성적 승인 상태(verifyStatus) 는 변경하지 않는다.
* */
@Transactional
@DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true)
public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) {
validateNoDuplicateUniversityChoices(universityChoiceRequest);
validateUniversityChoices(universityChoiceRequest);

// 성적 제출한 적이 한번도 없는 경우
Application existingApplication = applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(email)
Expand All @@ -83,14 +86,16 @@ public boolean submitUniversityChoice(String email, UniversityChoiceRequest univ
})
.orElse(existingApplication); // 금학기에 이미 성적 제출한 경우 기존 객체 사용

validateUpdateLimitNotExceed(application);

UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository
.getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term);
UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository
.getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.secondChoiceUniversityId(), term);
UniversityInfoForApply thirdChoiceUniversity = universityInfoForApplyRepository
.getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.thirdChoiceUniversityId(), term);

validateUpdateLimitNotExceed(application);
UniversityInfoForApply secondChoiceUniversity = Optional.ofNullable(universityChoiceRequest.secondChoiceUniversityId())
.map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term))
.orElse(null);
UniversityInfoForApply thirdChoiceUniversity = Optional.ofNullable(universityChoiceRequest.thirdChoiceUniversityId())
.map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term))
.orElse(null);
application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname());
return true;
}
Expand All @@ -109,14 +114,21 @@ private void validateUpdateLimitNotExceed(Application application) {
}
}

private void validateNoDuplicateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) {
// 입력값 유효성 검증
private void validateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) {
Set<Long> uniqueUniversityIds = new HashSet<>();

uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId());
uniqueUniversityIds.add(universityChoiceRequest.secondChoiceUniversityId());
uniqueUniversityIds.add(universityChoiceRequest.thirdChoiceUniversityId());
if (universityChoiceRequest.secondChoiceUniversityId() != null) {
addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.secondChoiceUniversityId());
}
if (universityChoiceRequest.thirdChoiceUniversityId() != null) {
addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.thirdChoiceUniversityId());
}
}

if (uniqueUniversityIds.size() < 3) {
private void addUniversityChoice(Set<Long> uniqueUniversityIds, Long universityId) {
boolean notAdded = !uniqueUniversityIds.add(universityId);
if (notAdded) {
throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.solidconnection.cache;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

@Component
@RequiredArgsConstructor
@Slf4j
public class CacheUpdateListener implements MessageListener {

private final CompletableFutureManager futureManager;
@Override
public void onMessage(Message message, byte[] pattern) {
String messageBody = new String(message.getBody(), StandardCharsets.UTF_8).replaceAll("^\"|\"$", "");
futureManager.completeFuture(messageBody);
}
}
56 changes: 56 additions & 0 deletions src/main/java/com/example/solidconnection/cache/CachingAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.solidconnection.cache;

import com.example.solidconnection.cache.annotation.DefaultCacheOut;
import com.example.solidconnection.cache.annotation.DefaultCaching;
import com.example.solidconnection.cache.manager.CacheManager;
import com.example.solidconnection.util.RedisUtils;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Aspect
@Component
@RequiredArgsConstructor
public class CachingAspect {
private final ApplicationContext applicationContext;
private final RedisUtils redisUtils;

@Around("@annotation(defaultCaching)")
public Object cache(ProceedingJoinPoint joinPoint, DefaultCaching defaultCaching) throws Throwable {

CacheManager cacheManager = (CacheManager) applicationContext.getBean(defaultCaching.cacheManager());
String key = redisUtils.generateCacheKey(defaultCaching.key(), joinPoint.getArgs());
Long ttl = defaultCaching.ttlSec();

// 1. 캐시에 있으면 반환
Object cachedValue = cacheManager.get(key);
if (cachedValue != null) {
return cachedValue;
}
// 2. 캐시에 없으면 캐싱 후 반환
Object result = joinPoint.proceed();
cacheManager.put(key, result, ttl);
return result;
}

@Around("@annotation(defaultCacheOut)")
public Object cacheEvict(ProceedingJoinPoint joinPoint, DefaultCacheOut defaultCacheOut) throws Throwable {

CacheManager cacheManager = (CacheManager) applicationContext.getBean(defaultCacheOut.cacheManager());

for (String key : defaultCacheOut.key()) {
String cacheKey = redisUtils.generateCacheKey(key, joinPoint.getArgs());
boolean usingPrefix = defaultCacheOut.prefix();

if (usingPrefix) {
cacheManager.evictUsingPrefix(cacheKey);
}else{
cacheManager.evict(cacheKey);
}
}
return joinPoint.proceed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.solidconnection.cache;

import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

@Component
public class CompletableFutureManager {
private final Map<String, CompletableFuture<Void>> waitingRequests = new ConcurrentHashMap<>();

public CompletableFuture<Void> getOrCreateFuture(String key) {
return waitingRequests.computeIfAbsent(key, k -> new CompletableFuture<>());
}

public void completeFuture(String key) {
CompletableFuture<Void> future = waitingRequests.remove(key);
if (future != null) {
future.complete(null);
}
}
}
Loading

0 comments on commit 4a0c7b5

Please sign in to comment.