Skip to content

Commit

Permalink
feat: 쿠폰 캐싱 기능 구현 (#255)
Browse files Browse the repository at this point in the history
* retactor : ClockHolder 메서드명 변경

* feat: ClockHolder 시간 추가

* style: ClockHolder 및 메서드명 변경

* feat : Cache 기능 추가

* docs : Reame 작성
  • Loading branch information
hongdosan authored Jan 5, 2024
1 parent 64ce1a9 commit 1a6143f
Show file tree
Hide file tree
Showing 27 changed files with 228 additions and 60 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# moabam-BE
## 🐥 MOABAM 서비스

![img.png](readme-image/img.png)

![img_1.png](readme-image/img_1.png)

<br><br>

## 👨‍👨‍👧 Backend Team 소개

| 김영명 | 김희빈 | 박세연(PO) | 신재윤 | 홍혁준(SM) |
|:------------------------------------------------------------------------------:|:------------------------------------------------------------------------------:|:------------------------------------------------------------------------------:|:------------------------------------------------------------------------------:|:------------------------------------------------------------------------------:|
| DEVELOPER | DEVELOPER | DEVELOPER | DEVELOPER | DEVELOPER |
| <img src="https://avatars.githubusercontent.com/u/83266154?v=4" width="250" /> | <img src="https://avatars.githubusercontent.com/u/72112845?v=4" width="250" /> | <img src="https://avatars.githubusercontent.com/u/54196094?v=4" width="250" /> | <img src="https://avatars.githubusercontent.com/u/87688023?v=4" width="250" /> | <img src="https://avatars.githubusercontent.com/u/31675711?v=4" width="250" /> |
| [ymkim97](https://github.com/ymkim97) | [kmebin](https://github.com/kmebin) | [parksey](https://github.com/parksey) | [DevUni](https://github.com/Shin-Jae-Yoon) | [HongDosan](https://github.com/HyuckJuneHong) |
| 방 도메인, 루틴 인증(메인) | 상품 도메인, 결제, 에러 알림, BE 팀장 | 회원 도메인, 랭킹 어드민 페이지 | 방 도메인, 루틴 인증(서브), 인프라 (AWS, CI/CD) | 쿠폰 도메인, 알림, 선착순 이벤트, 캐싱 |

<br><br>

## 공통 협업 방식

![img.png](readme-image/협업.png)

## 서비스 아키텍처

![img.png](readme-image/서비스-아키텍처.png)

## CI/CD 파이프라인

![img.png](readme-image/파이프라인.png)

## 컨벤션

![img_1.png](readme-image/컨벤션.png)

## Config 관리

![img.png](readme-image/콘피그.png)

## Test

![img.png](readme-image/테스트.png)
54 changes: 33 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@ java {
sourceCompatibility = '17'
}

compileJava {
options.compilerArgs << '-parameters'
options.encoding = 'UTF-8'
}

compileTestJava {
options.compilerArgs << '-parameters'
options.encoding = 'UTF-8'
}

ext {
snippetsDir = file('build/generated-snippets')
}

def querydslSrcDir = 'src/main/generated'

clean {
delete file(querydslSrcDir)
}

tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslSrcDir)
}
Expand Down Expand Up @@ -75,6 +87,9 @@ dependencies {
// Apache Commons Lang 3
implementation 'org.apache.commons:commons-lang3:3.13.0'

// Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Expand Down Expand Up @@ -134,28 +149,25 @@ jacocoTestReport {

afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
"**/*Application*",
"**/*Config*",
"**/*Request*",
"**/*Response*",
"**/*Exception*",
"**/*Mapper*",
"**/*ErrorMessage*",
"**/*DynamicQuery*",
"**/*BaseTimeEntity*",
"**/*HealthCheckController*",
"**/*S3Manager*",
] + Qdomains)
})
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
"**/*Application*",
"**/*Config*",
"**/*Request*",
"**/*Response*",
"**/*Exception*",
"**/*Mapper*",
"**/*ErrorMessage*",
"**/*DynamicQuery*",
"**/*BaseTimeEntity*",
"**/*HealthCheckController*",
"**/*S3Manager*",
] + Qdomains)
})
)
}
}

compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

tasks.withType(Checkstyle).configureEach {
reports {
xml.required = true
Expand All @@ -177,9 +189,9 @@ sonar {
property "sonar.host.url", "https://sonarcloud.io"
property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml'
property 'sonar.coverage.exclusions', '**/test/**, **/Q*.java, **/*Doc*.java, **/resources/** ' +
',**/*Application*.java , **/*Config*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ' +
',**/*ErrorMessage*.java, **/*Mapper*.java, **/*DynamicQuery*, **/*BaseTimeEntity*, **/*HealthCheckController* ' +
', **/*S3Manager*.java'
',**/*Application*.java , **/*Config*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ' +
',**/*ErrorMessage*.java, **/*Mapper*.java, **/*DynamicQuery*, **/*BaseTimeEntity*, **/*HealthCheckController* ' +
', **/*S3Manager*.java'
property 'sonar.java.checkstyle.reportPaths', 'build/reports/checkstyle/main.xml'
}
}
Expand Down
Binary file added readme-image/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/서비스-아키텍처.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/컨벤션.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/콘피그.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/테스트.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/파이프라인.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-image/협업.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.moabam.api.application.coupon;

import java.time.LocalDate;
import java.util.Optional;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "coupons")
public class CouponCacheService {

private final CouponRepository couponRepository;

@Cacheable(key = "#couponName + #now")
public Coupon getByNameAndStartAt(String couponName, LocalDate now) {
return couponRepository.findByNameAndStartAt(couponName, now)
.orElseThrow(() -> new NotFoundException(ErrorMessage.INVALID_COUPON_PERIOD));
}

@Cacheable(key = "#now")
public Optional<Coupon> getByStartAt(LocalDate now) {
return couponRepository.findByStartAt(now);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@
import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.domain.coupon.repository.CouponManageRepository;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletRepository;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.exception.ConflictException;
import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;
Expand All @@ -34,14 +32,14 @@ public class CouponManageService {
private final ClockHolder clockHolder;
private final NotificationService notificationService;

private final CouponRepository couponRepository;
private final CouponCacheService couponCacheService;
private final CouponManageRepository couponManageRepository;
private final CouponWalletRepository couponWalletRepository;

@Scheduled(fixedDelay = 1000)
public void issue() {
LocalDate now = clockHolder.date();
Optional<Coupon> optionalCoupon = couponRepository.findByStartAt(now);
Optional<Coupon> optionalCoupon = couponCacheService.getByStartAt(now);

if (optionalCoupon.isEmpty()) {
return;
Expand Down Expand Up @@ -70,21 +68,20 @@ public void issue() {
couponManageRepository.increase(couponName, membersId.size());
}

public void delete(String couponName) {
couponManageRepository.deleteQueue(couponName);
couponManageRepository.deleteCount(couponName);
}

public void registerQueue(String couponName, Long memberId) {
double registerTime = System.currentTimeMillis();
validateRegisterQueue(couponName, memberId);
couponManageRepository.addIfAbsentQueue(couponName, memberId, registerTime);
}

public void delete(String couponName) {
couponManageRepository.deleteQueue(couponName);
couponManageRepository.deleteCount(couponName);
}

private void validateRegisterQueue(String couponName, Long memberId) {
LocalDate now = clockHolder.date();
Coupon coupon = couponRepository.findByNameAndStartAt(couponName, now)
.orElseThrow(() -> new NotFoundException(ErrorMessage.INVALID_COUPON_PERIOD));
Coupon coupon = couponCacheService.getByNameAndStartAt(couponName, now);

if (couponManageRepository.hasValue(couponName, memberId)) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_ISSUE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void delete(Member member) {
throw new BadRequestException(NEED_TO_EXIT_ALL_ROOMS);
}

member.delete(clockHolder.times());
member.delete(clockHolder.dateTime());
memberRepository.flush();
memberRepository.delete(member);
rankingService.removeRanking(MemberMapper.toRankingInfo(member));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void sendCouponIssueResult(Long memberId, String couponName, String body)

@Scheduled(cron = "0 55 * * * *")
public void sendCertificationTime() {
int certificationTime = (clockHolder.times().getHour() + ONE_HOUR) % HOURS_IN_A_DAY;
int certificationTime = (clockHolder.dateTime().getHour() + ONE_HOUR) % HOURS_IN_A_DAY;
List<Participant> participants = participantSearchRepository.findAllByRoomCertifyTime(certificationTime);

participants.parallelStream().forEach(participant -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public CertifiedMemberInfo getCertifiedMemberInfo(Long memberId, Long roomId, Li
case NIGHT -> BugType.NIGHT;
};

validateCertifyTime(clockHolder.times(), room.getCertifyTime());
validateCertifyTime(clockHolder.dateTime(), room.getCertifyTime());
validateAlreadyCertified(memberId, roomId, today);

certifyMember(memberId, roomId, participant, member, imageUrls);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private void validateEnteredRoomCount(Long memberId, RoomType roomType) {
}

private void validateCertifyTime(Room room) {
LocalDateTime now = clockHolder.times();
LocalDateTime now = clockHolder.dateTime();
LocalTime targetTime = LocalTime.of(room.getCertifyTime(), 0);
LocalDateTime targetDateTime = LocalDateTime.of(now.toLocalDate(), targetTime);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ private double calculateCompletePercentage(int certifiedMembersCount, Room room,
return 0;
}

LocalDateTime now = clockHolder.times();
LocalDateTime now = clockHolder.dateTime();
LocalTime targetTime = LocalTime.of(room.getCertifyTime(), 0);
LocalDateTime targetDateTime = LocalDateTime.of(now.toLocalDate(), targetTime);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,4 @@ public interface CouponRepository extends JpaRepository<Coupon, Long> {
Optional<Coupon> findByStartAt(LocalDate startAt);

Optional<Coupon> findByNameAndStartAt(String couponName, LocalDate startAt);

boolean existsByNameAndStartAt(String couponName, LocalDate startAt);
}
7 changes: 6 additions & 1 deletion src/main/java/com/moabam/global/common/util/ClockHolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public interface ClockHolder {

LocalDateTime times();
LocalDateTime dateTime();

LocalDate date();

LocalTime time();

LocalTime endOfDay();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
Expand All @@ -11,12 +12,22 @@
public class SystemClockHolder implements ClockHolder {

@Override
public LocalDateTime times() {
public LocalDateTime dateTime() {
return LocalDateTime.now();
}

@Override
public LocalDate date() {
return LocalDate.now();
}

@Override
public LocalTime time() {
return LocalTime.now();
}

@Override
public LocalTime endOfDay() {
return LocalTime.of(23, 59, 59, 999_999_999);
}
}
62 changes: 62 additions & 0 deletions src/main/java/com/moabam/global/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.moabam.global.config;

import static org.springframework.data.redis.serializer.RedisSerializationContext.*;

import java.time.Duration;
import java.time.LocalTime;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.moabam.global.common.util.ClockHolder;

import lombok.RequiredArgsConstructor;

@EnableCaching
@Configuration
@RequiredArgsConstructor
public class CacheConfig {

private final ClockHolder clockHolder;

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
var strSerializePair = SerializationPair.fromSerializer(new StringRedisSerializer());
var objSerializePair = SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper()));

return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(getTtl())
.serializeKeysWith(strSerializePair)
.serializeValuesWith(objSerializePair);
}

private Duration getTtl() {
LocalTime now = clockHolder.time();
LocalTime end = clockHolder.endOfDay();

return Duration.between(now, end);
}

private ObjectMapper objectMapper() {
PolymorphicTypeValidator polymorphicTypeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build();

return JsonMapper.builder()
.polymorphicTypeValidator(polymorphicTypeValidator)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.addModule(new JavaTimeModule())
.activateDefaultTyping(polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL)
.build();
}
}
Loading

0 comments on commit 1a6143f

Please sign in to comment.