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
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);
}
}
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