Skip to content

Commit a5d245a

Browse files
authored
[Refactor] #698 솝탬프 캐싱 로직 개선 (#699)
2 parents ce5d4c2 + 790c543 commit a5d245a

File tree

17 files changed

+761
-414
lines changed

17 files changed

+761
-414
lines changed

.github/workflows/app-cd-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: ⚙️ MAKERS-DEV-APP-DEPLOY
22

33
on:
44
push:
5-
branches: [ dev ]
5+
# branches: [ dev ]
66

77
env:
88
SPRING_PROFILES_ACTIVE: dev

.github/workflows/deploy-lambda-dev.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch: # 수동 실행
55
push:
66
branches:
7-
- develop # develop 브랜치 푸시 시 자동 실행
7+
- dev # dev 브랜치 푸시 시 자동 실행
88

99
jobs:
1010
deploy:
@@ -45,12 +45,17 @@ jobs:
4545
run: chmod +x gradlew
4646

4747
- name: Build Lambda JAR
48-
run: ./gradlew clean lambdaJar -x test
48+
run: ./gradlew clean jar lambdaJar -x test
4949

5050
- name: Upload JAR to S3
51+
id: upload
5152
run: |
5253
# 빌드된 ZIP 파일 찾기
5354
JAR_FILE=$(ls build/distributions/*-lambda.zip | head -1)
55+
if [ -z "$JAR_FILE" ]; then
56+
echo "Error: Lambda ZIP file not found"
57+
exit 1
58+
fi
5459
5560
# 타임스탬프 생성
5661
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
@@ -60,6 +65,7 @@ jobs:
6065
aws s3 cp "$JAR_FILE" "s3://${{ env.S3_BUCKET }}/$S3_KEY"
6166
6267
echo "S3_KEY=$S3_KEY" >> $GITHUB_ENV
68+
echo "s3_key=$S3_KEY" >> $GITHUB_OUTPUT
6369
6470
- name: Install SAM CLI
6571
uses: aws-actions/setup-sam@v2

build.gradle

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,21 @@ dependencyManagement {
126126

127127
// Lambda ZIP 빌드 설정
128128
task lambdaJar(type: Zip) {
129-
dependsOn bootJar
129+
dependsOn jar, bootJar
130+
130131
archiveClassifier = 'lambda'
131132
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
132-
zip64 = true // 대용량 ZIP 파일 지원
133+
zip64 = true
134+
135+
// 최상단 클래스 (jar 방식에서는 생략해도 무방하지만 기존 구조 유지를 위해 남겨둠)
136+
into('/') {
137+
from sourceSets.main.output
138+
}
133139

134-
// lib 디렉토리 구조로 패키징
135140
into('lib') {
136141
from(jar)
137142
from(configurations.runtimeClasspath) {
138-
// Lambda에서 불필요한 파일 제외
139-
exclude "org/apache/tomcat/embed/**"
143+
exclude "tomcat-embed-*"
140144
exclude "META-INF/*.SF"
141145
exclude "META-INF/*.DSA"
142146
exclude "META-INF/*.RSA"
@@ -152,9 +156,9 @@ build.dependsOn lambdaJar
152156
// JAR 설정
153157
jar {
154158
enabled = true
155-
archiveClassifier = ''
159+
archiveClassifier = 'plain'
156160
}
157161

158162
bootJar {
159163
enabled = true
160-
}
164+
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package org.sopt.app.application.playground.dto;
22

3+
import java.util.HashSet;
34
import java.util.List;
5+
import java.util.Set;
46
import org.sopt.app.application.playground.dto.RecommendedFriendInfo.PlaygroundUserFindFilter;
57

68
public record PlaygroundUserFindCondition(
7-
List<Long> generations,
9+
Set<Long> generations,
810
List<PlaygroundUserFindFilter> filters
911
) {
12+
// 기존에 List였던 generations를 Set으로 변경하며 대응을 위해 생성자를 오버라이딩하여 사용함
13+
public PlaygroundUserFindCondition(List<Long> generationsList, List<PlaygroundUserFindFilter> filters) {
14+
this(new HashSet<>(generationsList), filters);
15+
}
1016

1117
public static PlaygroundUserFindCondition createRecommendFriendRequestByGeneration(List<Long> generations) {
12-
return new PlaygroundUserFindCondition(generations, List.of());
18+
return new PlaygroundUserFindCondition(new HashSet<>(generations), List.of());
1319
}
1420
}

src/main/java/org/sopt/app/application/rank/RankCacheService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public interface RankCacheService {
2929
CachedUserInfo getUserInfo(Long id);
3030

3131
void updateCachedUserInfo(Long id, CachedUserInfo userInfo);
32+
33+
void removeCachedUserInfo(Long userId);
34+
35+
void updateScore(Long userId, long currentUserScore);
36+
3237
}

src/main/java/org/sopt/app/application/rank/RedisRankService.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ public void updateCachedUserInfo(Long id, CachedUserInfo userInfo){
7070
.put(CacheType.SOPTAMP_PROFILE_MESSAGE.getCacheName(), id, userInfo);
7171
}
7272

73+
@Override
74+
public void removeCachedUserInfo(Long userId) {
75+
redisTemplate.opsForHash().delete(CacheType.SOPTAMP_PROFILE_MESSAGE.getCacheName(), userId);
76+
}
77+
78+
@Override
79+
public void updateScore(Long userId, long currentUserScore) {
80+
redisTemplate.opsForZSet()
81+
.add(CacheType.SOPTAMP_SCORE.getCacheName(), userId, currentUserScore);
82+
}
83+
7384
@Override
7485
public CachedUserInfo getUserInfo(Long id) {
7586
try {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.sopt.app.application.soptamp;
2+
3+
import lombok.AccessLevel;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.RequiredArgsConstructor;
7+
import org.sopt.app.common.event.Event;
8+
9+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
10+
public class SoptampEvent {
11+
12+
@Getter
13+
@RequiredArgsConstructor(staticName = "of")
14+
public static class SoptampUserScoreCacheSyncEvent extends Event {
15+
private final Long userId;
16+
}
17+
18+
@Getter
19+
@RequiredArgsConstructor(staticName = "of")
20+
public static class SoptampUserProfileCacheSyncEvent extends Event {
21+
private final Long userId;
22+
}
23+
24+
@Getter
25+
@RequiredArgsConstructor(staticName = "of")
26+
public static class SoptampUserAllCacheSyncEvent extends Event {
27+
private final Long userId;
28+
}
29+
30+
@Getter
31+
@RequiredArgsConstructor(staticName = "of")
32+
public static class SoptampUserRemoveCacheEvent extends Event {
33+
private final Long userId;
34+
}
35+
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.sopt.app.application.soptamp;
2+
3+
import static org.sopt.app.common.config.AsyncConfig.CACHE_SYNC_EXECUTOR;
4+
5+
import lombok.AccessLevel;
6+
import lombok.RequiredArgsConstructor;
7+
import org.sopt.app.application.rank.CachedUserInfo;
8+
import org.sopt.app.application.rank.RankCacheService;
9+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserAllCacheSyncEvent;
10+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserProfileCacheSyncEvent;
11+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserRemoveCacheEvent;
12+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserScoreCacheSyncEvent;
13+
import org.sopt.app.application.user.UserWithdrawEvent;
14+
import org.sopt.app.interfaces.postgres.SoptampUserRepository;
15+
import org.springframework.scheduling.annotation.Async;
16+
import org.springframework.stereotype.Component;
17+
import org.springframework.transaction.event.TransactionPhase;
18+
import org.springframework.transaction.event.TransactionalEventListener;
19+
20+
@Component
21+
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
22+
public class SoptampEventListener {
23+
24+
private final RankCacheService rankCacheService;
25+
private final SoptampUserRepository soptampUserRepository;
26+
27+
@Async(CACHE_SYNC_EXECUTOR)
28+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
29+
public void handleScoreCacheSyncEvent(final SoptampUserScoreCacheSyncEvent event) {
30+
soptampUserRepository.findByUserId(event.getUserId()).ifPresent(user ->
31+
rankCacheService.updateScore(user.getUserId(), user.getTotalPoints())
32+
);
33+
}
34+
35+
@Async(CACHE_SYNC_EXECUTOR)
36+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
37+
public void handleProfileCacheSyncEvent(final SoptampUserProfileCacheSyncEvent event) {
38+
soptampUserRepository.findByUserId(event.getUserId()).ifPresent(user ->
39+
rankCacheService.updateCachedUserInfo(user.getUserId(), CachedUserInfo.of(SoptampUserInfo.of(user)))
40+
);
41+
}
42+
43+
@Async(CACHE_SYNC_EXECUTOR)
44+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
45+
public void handleAllCacheSyncEvent(final SoptampUserAllCacheSyncEvent event) {
46+
soptampUserRepository.findByUserId(event.getUserId()).ifPresent(user -> {
47+
rankCacheService.updateScore(user.getUserId(), user.getTotalPoints());
48+
rankCacheService.updateCachedUserInfo(user.getUserId(), CachedUserInfo.of(SoptampUserInfo.of(user)));
49+
});
50+
}
51+
52+
@Async(CACHE_SYNC_EXECUTOR)
53+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
54+
public void handleRemoveCacheSyncEvent(final SoptampUserRemoveCacheEvent event) {
55+
rankCacheService.removeRank(event.getUserId());
56+
rankCacheService.removeCachedUserInfo(event.getUserId());
57+
}
58+
59+
@Async(CACHE_SYNC_EXECUTOR)
60+
@TransactionalEventListener(
61+
phase = TransactionPhase.AFTER_COMMIT,
62+
classes = UserWithdrawEvent.class
63+
)
64+
public void handleUserWithdrawCache(final UserWithdrawEvent event) {
65+
rankCacheService.removeRank(event.getUserId());
66+
rankCacheService.removeCachedUserInfo(event.getUserId());
67+
}
68+
69+
}

src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import java.util.Optional;
88
import lombok.RequiredArgsConstructor;
99
import org.sopt.app.application.platform.dto.PlatformUserInfoResponse;
10-
import org.sopt.app.application.rank.CachedUserInfo;
1110
import org.sopt.app.application.rank.RankCacheService;
11+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserAllCacheSyncEvent;
12+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserProfileCacheSyncEvent;
13+
import org.sopt.app.application.soptamp.SoptampEvent.SoptampUserScoreCacheSyncEvent;
1214
import org.sopt.app.application.user.UserWithdrawEvent;
15+
import org.sopt.app.common.event.EventPublisher;
1316
import org.sopt.app.common.exception.BadRequestException;
1417
import org.sopt.app.common.response.ErrorCode;
1518
import org.sopt.app.domain.entity.soptamp.SoptampUser;
@@ -28,6 +31,7 @@ public class SoptampUserService {
2831
private final SoptampUserRepository soptampUserRepository;
2932
private final AppjamUserRepository appjamUserRepository;
3033
private final RankCacheService rankCacheService;
34+
private final EventPublisher eventPublisher;
3135

3236
@Value("${makers.app.soptamp.appjam-mode:false}")
3337
private boolean appjamMode;
@@ -46,8 +50,7 @@ public SoptampUserInfo editProfileMessage(Long userId, String profileMessage) {
4650
SoptampUser soptampUser = soptampUserRepository.findByUserId(userId)
4751
.orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND));
4852
soptampUser.updateProfileMessage(profileMessage);
49-
rankCacheService.updateCachedUserInfo(soptampUser.getUserId(),
50-
CachedUserInfo.of(SoptampUserInfo.of(soptampUser)));
53+
eventPublisher.raise(SoptampUserProfileCacheSyncEvent.of(userId));
5154
return SoptampUserInfo.of(soptampUser);
5255
}
5356

@@ -102,8 +105,8 @@ private void updateSoptampUserNormal(SoptampUser registeredUser, PlatformUserInf
102105
findSoptPartByPartName(part),
103106
newNickname
104107
);
105-
rankCacheService.removeRank(userId);
106-
rankCacheService.createNewRank(userId);
108+
109+
eventPublisher.raise(SoptampUserAllCacheSyncEvent.of(userId));
107110
}
108111

109112
private void createSoptampUserNormal(PlatformUserInfoResponse profile, Long userId,
@@ -114,7 +117,7 @@ private void createSoptampUserNormal(PlatformUserInfoResponse profile, Long user
114117
SoptampUser newSoptampUser = createNewSoptampUser(userId, uniqueNickname, (long) profile.lastGeneration(),
115118
findSoptPartByPartName(part));
116119
soptampUserRepository.save(newSoptampUser);
117-
rankCacheService.createNewRank(userId);
120+
eventPublisher.raise(SoptampUserAllCacheSyncEvent.of(userId));
118121
}
119122

120123
private boolean isGenerationChanged(SoptampUser registeredUser, Long profileGeneration) {
@@ -155,12 +158,13 @@ private void upsertSoptampUserForAppjam(PlatformUserInfoResponse profile,
155158

156159
// 앱잼 변환 시점에 한 번 포인트 초기화
157160
registeredUser.initTotalPoints();
161+
eventPublisher.raise(SoptampEvent.SoptampUserAllCacheSyncEvent.of(userId));
158162
}
159163

160164
private void createSoptampUserAppjam(PlatformUserInfoResponse profile,
161165
Long userId,
162-
PlatformUserInfoResponse.SoptActivities latest) {
163-
166+
PlatformUserInfoResponse.SoptActivities latest
167+
) {
164168
String baseNickname = buildAppjamBaseNickname(profile, userId);
165169

166170
// 새 유저: 전체에서 중복 검사
@@ -179,6 +183,7 @@ private void createSoptampUserAppjam(PlatformUserInfoResponse profile,
179183
newSoptampUser.initTotalPoints(); // 새 시즌이니 0점부터
180184

181185
soptampUserRepository.save(newSoptampUser);
186+
eventPublisher.raise(SoptampUserAllCacheSyncEvent.of(userId));
182187
}
183188

184189
private boolean needsAppjamNicknameMigration(SoptampUser user) {
@@ -262,7 +267,7 @@ public void addPointByLevel(Long userId, Integer level) {
262267
soptampUser.addPointsByLevel(level);
263268

264269
if (!appjamMode) {
265-
rankCacheService.incrementScore(soptampUser.getUserId(), level);
270+
eventPublisher.raise(SoptampUserScoreCacheSyncEvent.of(userId));
266271
}
267272
}
268273

@@ -273,7 +278,7 @@ public void subtractPointByLevel(Long userId, Integer level) {
273278
soptampUser.subtractPointsByLevel(level);
274279

275280
if (!appjamMode) {
276-
rankCacheService.decreaseScore(soptampUser.getUserId(), level);
281+
eventPublisher.raise(SoptampUserScoreCacheSyncEvent.of(userId));
277282
}
278283
}
279284

@@ -284,7 +289,7 @@ public void initPoint(Long userId) {
284289
soptampUser.initTotalPoints();
285290
soptampUserRepository.save(soptampUser);
286291
if (!appjamMode) {
287-
rankCacheService.initScore(soptampUser.getUserId());
292+
eventPublisher.raise(SoptampUserScoreCacheSyncEvent.of(userId));
288293
}
289294
}
290295

src/main/java/org/sopt/app/application/stamp/ClapService.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.sopt.app.domain.entity.soptamp.Stamp;
1414
import org.sopt.app.interfaces.postgres.ClapRepository;
1515
import org.sopt.app.interfaces.postgres.StampRepository;
16+
import org.springframework.beans.factory.annotation.Value;
1617
import org.springframework.dao.DataIntegrityViolationException;
1718
import org.springframework.data.domain.Page;
1819
import org.springframework.data.domain.Pageable;
@@ -25,6 +26,9 @@
2526
@Slf4j
2627
public class ClapService {
2728

29+
@Value("${makers.app.soptamp.appjam-mode:false}")
30+
private boolean appjamMode;
31+
2832
private static final int MAX_RETRY = 3;
2933
private final EventPublisher eventPublisher;
3034
private final ClapRepository clapRepository;
@@ -62,8 +66,10 @@ public int addClap(Long userId, Long stampId, int increment) {
6266
if (applied > 0) {
6367
stampRepository.incrementClapCountReturning(stampId, applied);
6468
final int newClapTotal = oldClapTotal + applied;
65-
// TODO: 앱잼탬프 운영 기간동안 박수 알림 방지를 위해 주석 처리. 별도 박수 API를 구현 및 연결하기에 시간이 부족하여 주석처리함. 추후 복원하기
66-
// eventPublisher.raise(ClapEvent.of(stamp.getUserId(), stampId, oldClapTotal, newClapTotal));
69+
70+
if(!appjamMode){
71+
eventPublisher.raise(ClapEvent.of(stamp.getUserId(), stampId, oldClapTotal, newClapTotal));
72+
}
6773
}
6874
return applied;
6975
}

0 commit comments

Comments
 (0)