Skip to content

Commit 62590c4

Browse files
authored
Merge pull request #157 from dnd-side-project/refactor/#156-Infra-Refactor
Refactor/#156 infra refactor
2 parents 456a7db + cc0c850 commit 62590c4

File tree

11 files changed

+149
-129
lines changed

11 files changed

+149
-129
lines changed

build.gradle

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,16 @@ dependencies {
5050
// hibernate
5151
implementation 'org.springframework.boot:spring-boot-starter-validation'
5252

53-
// SQS
54-
implementation platform('software.amazon.awssdk:bom:2.21.0') // 버전은 최신 안정화
53+
// AWS
54+
implementation platform('software.amazon.awssdk:bom:2.25.19')
5555
implementation 'software.amazon.awssdk:sqs'
56-
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs:3.1.0'
57-
58-
// dynamoDB
59-
implementation platform('software.amazon.awssdk:bom:2.25.19') // 최신 BOM으로 통일
56+
implementation 'software.amazon.awssdk:netty-nio-client'
6057
implementation 'software.amazon.awssdk:dynamodb'
58+
implementation 'software.amazon.awssdk:lambda'
6159
implementation 'software.amazon.awssdk:auth'
6260

63-
// lambda
64-
implementation 'software.amazon.awssdk:lambda:2.20.130'
61+
// Spring Cloud AWS
62+
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs:3.1.0'
6563

6664
// discord
6765
implementation 'com.github.napstr:logback-discord-appender:1.0.0'

src/main/java/com/dnd/reevserver/domain/alert/controller/AlertController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ public class AlertController {
2222
public ResponseEntity<AlertListResponseDto> getUserAlerts(
2323
@AuthenticationPrincipal String userId,
2424
@RequestParam(defaultValue = "0") int page,
25-
@RequestParam(defaultValue = "20") int size,
26-
@RequestParam(defaultValue = "false") boolean onlyUnread) {
27-
return ResponseEntity.ok(alertService.getUserAlertList(userId, page, size, onlyUnread));
25+
@RequestParam(defaultValue = "20") int size) {
26+
return ResponseEntity.ok(alertService.getUserAlertList(userId, page, size));
2827
}
2928

3029
@Operation(summary = "알림 읽음 처리")

src/main/java/com/dnd/reevserver/domain/alert/repository/AlertRepository.java

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,80 +14,81 @@
1414
public class AlertRepository {
1515

1616
private final StringRedisTemplate redisTemplate;
17-
private static final Duration ALERT_TTL = Duration.ofDays(7);
1817
private final ObjectMapper objectMapper;
1918

20-
private String getAlertKey(String userId) {
21-
return "alert:" + userId;
22-
}
19+
private static final Duration READ_TTL = Duration.ofDays(7);
20+
private static final String READ_MARK_PREFIX = "read:";
21+
private static final String ALERT_MARK_PREFIX = "alert:";
2322

24-
private String getIsReadKey(String userId) {
25-
return "alert:" + userId + ":isread";
26-
}
27-
28-
public void saveAlert(String userId, String messageId, String messageJson) {
29-
redisTemplate.opsForList().leftPush(getAlertKey(userId), messageJson);
30-
redisTemplate.opsForHash().put(getIsReadKey(userId), messageId, "false");
31-
refreshTtl(userId);
23+
private String getAlertKey(String userId) {
24+
return ALERT_MARK_PREFIX + userId;
3225
}
3326

34-
private void refreshTtl(String userId) {
35-
redisTemplate.expire(getAlertKey(userId), ALERT_TTL);
36-
redisTemplate.expire(getIsReadKey(userId), ALERT_TTL);
27+
private String getReadKey(String userId, String messageId) {
28+
return ALERT_MARK_PREFIX + userId + ":" + READ_MARK_PREFIX + messageId;
3729
}
3830

39-
public List<String> getAlertsByPage(String userId, int page, int size) {
31+
public List<String> getAlertsByPage(String userId, int page, int size, long totalCnt) {
4032
int start = page * size;
41-
int end = start + size - 1;
33+
int end = Math.min(start + size - 1, (int) totalCnt - 1);
4234
return redisTemplate.opsForList().range(getAlertKey(userId), start, end);
4335
}
4436

45-
public Object getIsRead(String userId, String messageId) {
46-
return redisTemplate.opsForHash().get(getIsReadKey(userId), messageId);
37+
public boolean isRead(String userId, String messageId) {
38+
return Boolean.TRUE.equals(redisTemplate.hasKey(getReadKey(userId, messageId)));
4739
}
4840

4941
public void markAsRead(String userId, String messageId) {
50-
redisTemplate.opsForHash().put(getIsReadKey(userId), messageId, "true");
42+
redisTemplate.opsForValue().set(getReadKey(userId, messageId), "true", READ_TTL);
5143
}
5244

5345
public long getUnreadCount(String userId) {
54-
return redisTemplate.opsForHash().values(getIsReadKey(userId)).stream()
55-
.filter(val -> "false".equals(val))
56-
.count();
46+
List<String> allAlerts = redisTemplate.opsForList().range(getAlertKey(userId), 0, -1);
47+
if (allAlerts == null) return 0;
48+
49+
return allAlerts.stream().filter(json -> {
50+
try {
51+
AlertMessageResponseDto dto = objectMapper.readValue(json, AlertMessageResponseDto.class);
52+
return !isRead(userId, dto.messageId());
53+
} catch (Exception e) {
54+
return false;
55+
}
56+
}).count();
5757
}
5858

5959
public long getTotalCount(String userId) {
6060
return redisTemplate.opsForList().size(getAlertKey(userId));
6161
}
6262

63-
public List<String> getAllAlerts(String userId) {
64-
return redisTemplate.opsForList().range(getAlertKey(userId), 0, -1);
65-
}
66-
6763
public void deleteAlert(String userId, String messageId) {
6864
String alertKey = getAlertKey(userId);
69-
String isReadKey = getIsReadKey(userId);
7065

71-
// 리스트에서 해당 메시지를 제거 (O(n))
7266
List<String> allAlerts = redisTemplate.opsForList().range(alertKey, 0, -1);
7367
if (allAlerts != null) {
7468
for (String json : allAlerts) {
7569
try {
7670
AlertMessageResponseDto dto = objectMapper.readValue(json, AlertMessageResponseDto.class);
7771
if (dto.messageId().equals(messageId)) {
7872
redisTemplate.opsForList().remove(alertKey, 1, json);
73+
redisTemplate.delete(getReadKey(userId, messageId));
7974
break;
8075
}
8176
} catch (Exception ignored) {}
8277
}
8378
}
84-
85-
// isread에서도 제거
86-
redisTemplate.opsForHash().delete(isReadKey, messageId);
8779
}
8880

8981
public void deleteAllAlerts(String userId) {
90-
redisTemplate.delete(getAlertKey(userId));
91-
redisTemplate.delete(getIsReadKey(userId));
82+
String alertKey = getAlertKey(userId);
83+
List<String> allAlerts = redisTemplate.opsForList().range(alertKey, 0, -1);
84+
if (allAlerts != null) {
85+
for (String json : allAlerts) {
86+
try {
87+
AlertMessageResponseDto dto = objectMapper.readValue(json, AlertMessageResponseDto.class);
88+
redisTemplate.delete(getReadKey(userId, dto.messageId()));
89+
} catch (Exception ignored) {}
90+
}
91+
}
92+
redisTemplate.delete(alertKey);
9293
}
93-
}
94+
}

src/main/java/com/dnd/reevserver/domain/alert/service/AlertMessageListener.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/main/java/com/dnd/reevserver/domain/alert/service/AlertService.java

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.time.LocalDateTime;
1111
import java.util.List;
1212
import java.util.Objects;
13-
import java.util.stream.Collectors;
1413

1514
@Service
1615
@RequiredArgsConstructor
@@ -24,20 +23,17 @@ public void sendMessage(String messageId, String userId, String content, LocalDa
2423
alertSqsProducer.send(messageId, userId, content, timestamp, retrospectId);
2524
}
2625

27-
public AlertListResponseDto getUserAlertList(String userId, int page, int size, boolean onlyUnread) {
28-
List<String> rawMessages = onlyUnread
29-
? alertRepository.getAllAlerts(userId) // 전체 가져와서 필터링
30-
: alertRepository.getAlertsByPage(userId, page, size);
31-
26+
public AlertListResponseDto getUserAlertList(String userId, int page, int size) {
3227
long totalCnt = alertRepository.getTotalCount(userId);
3328
long unreadCnt = alertRepository.getUnreadCount(userId);
3429

30+
List<String> rawMessages = alertRepository.getAlertsByPage(userId, page, size, totalCnt);
31+
3532
List<AlertMessageResponseDto> allAlerts = rawMessages.stream()
3633
.map(json -> {
3734
try {
3835
AlertMessageResponseDto dto = objectMapper.readValue(json, AlertMessageResponseDto.class);
39-
Object readValue = alertRepository.getIsRead(userId, dto.messageId());
40-
boolean isRead = "true".equals(readValue);
36+
boolean isRead = alertRepository.isRead(userId, dto.messageId());
4137
return new AlertMessageResponseDto(
4238
dto.messageId(), dto.userId(), dto.content(), dto.timestamp(), dto.retrospectId(), isRead
4339
);
@@ -46,25 +42,15 @@ public AlertListResponseDto getUserAlertList(String userId, int page, int size,
4642
}
4743
})
4844
.filter(Objects::nonNull)
49-
.collect(Collectors.toList());
50-
51-
List<AlertMessageResponseDto> filtered = onlyUnread
52-
? allAlerts.stream().filter(a -> !a.isRead()).toList()
53-
: allAlerts;
54-
55-
int totalPage = (int) Math.ceil((double) filtered.size() / size);
56-
int fromIndex = page * size;
57-
int toIndex = Math.min(fromIndex + size, filtered.size());
58-
List<AlertMessageResponseDto> pageList = (fromIndex < filtered.size())
59-
? filtered.subList(fromIndex, toIndex)
60-
: List.of();
45+
.toList();
6146

47+
int totalPage = (int) Math.ceil((double) totalCnt / size);
6248
boolean hasNext = page < totalPage - 1;
6349
boolean hasPrev = page > 0;
6450

6551
return new AlertListResponseDto(
6652
userId,
67-
pageList,
53+
allAlerts,
6854
totalCnt,
6955
unreadCnt,
7056
page,

src/main/java/com/dnd/reevserver/domain/alert/service/AlertSqsProducer.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
1010

1111
import java.time.LocalDateTime;
12+
import java.util.concurrent.Semaphore;
13+
import java.util.concurrent.TimeUnit;
1214

1315
@Component
1416
@RequiredArgsConstructor
@@ -17,11 +19,20 @@ public class AlertSqsProducer {
1719
private final SqsAsyncClient sqsAsyncClient;
1820
private final ObjectMapper objectMapper;
1921

22+
private static final int MAX_CONCURRENT_SENDS = 20; // 동시에 보낼 수 있는 최대 개수
23+
private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_SENDS);
24+
2025
@Value("${cloud.aws.sqs.queue-name}")
2126
private String queueUrl;
2227

2328
public void send(String messageId, String userId, String content, LocalDateTime timestamp, Long retrospectId) {
2429
try {
30+
boolean acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS); // 대기 최대 2초
31+
32+
if (!acquired) {
33+
throw new RuntimeException("SQS 전송 대기열 초과: 메시지 전송을 건너뜁니다.");
34+
}
35+
2536
AlertMessageResponseDto dto = new AlertMessageResponseDto(messageId, userId, content, timestamp, retrospectId, false);
2637
String json = objectMapper.writeValueAsString(dto);
2738

@@ -30,9 +41,17 @@ public void send(String messageId, String userId, String content, LocalDateTime
3041
.messageBody(json)
3142
.build();
3243

33-
sqsAsyncClient.sendMessage(request);
44+
sqsAsyncClient.sendMessage(request)
45+
.whenComplete((resp, ex) -> {
46+
semaphore.release(); // 완료 후 반드시 해제
47+
if (ex != null) {
48+
System.err.println("SQS 전송 실패: " + ex.getMessage());
49+
}
50+
});
51+
3452
} catch (Exception e) {
53+
semaphore.release(); // 예외 시에도 해제
3554
throw new RuntimeException("SQS 메시지 전송 중 오류 발생", e);
3655
}
3756
}
38-
}
57+
}

src/main/java/com/dnd/reevserver/domain/like/service/LikeSqsProducer.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,52 @@
66
import software.amazon.awssdk.services.sqs.SqsAsyncClient;
77
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
88

9+
import java.util.concurrent.Semaphore;
10+
import java.util.concurrent.TimeUnit;
11+
912
@Component
1013
@RequiredArgsConstructor
1114
public class LikeSqsProducer {
1215

1316
private final SqsAsyncClient sqsAsyncClient;
1417

18+
private static final int MAX_CONCURRENT_SENDS = 20;
19+
private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_SENDS);
20+
1521
@Value("${cloud.aws.sqs.queue-like-name}")
1622
private String queueUrl;
1723

1824
public void sendToggleLikeEvent(Long retrospectId, String userId) {
19-
String body = """
20-
{
21-
"retrospectId": "%s",
22-
"userId": "%s"
25+
try {
26+
boolean acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS);
27+
28+
if (!acquired) {
29+
throw new RuntimeException("SQS 좋아요 전송 제한 초과: 메시지를 건너뜁니다.");
2330
}
24-
""".formatted(retrospectId, userId);
2531

26-
SendMessageRequest request = SendMessageRequest.builder()
27-
.queueUrl(queueUrl)
28-
.messageBody(body)
29-
.build();
32+
String body = """
33+
{
34+
"retrospectId": "%s",
35+
"userId": "%s"
36+
}
37+
""".formatted(retrospectId, userId);
38+
39+
SendMessageRequest request = SendMessageRequest.builder()
40+
.queueUrl(queueUrl)
41+
.messageBody(body)
42+
.build();
43+
44+
sqsAsyncClient.sendMessage(request)
45+
.whenComplete((resp, ex) -> {
46+
semaphore.release();
47+
if (ex != null) {
48+
System.err.println("좋아요 SQS 전송 실패: " + ex.getMessage());
49+
}
50+
});
3051

31-
sqsAsyncClient.sendMessage(request);
52+
} catch (Exception e) {
53+
semaphore.release();
54+
throw new RuntimeException("SQS 좋아요 메시지 전송 중 오류 발생", e);
55+
}
3256
}
3357
}

src/main/java/com/dnd/reevserver/domain/statistics/repository/StatisticsRedisRepository.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.springframework.data.redis.core.StringRedisTemplate;
66
import org.springframework.stereotype.Repository;
77

8-
import java.util.Comparator;
98
import java.util.List;
109
import java.util.Map;
1110

src/main/java/com/dnd/reevserver/global/config/redis/RedisConfig.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.springframework.beans.factory.annotation.Value;
44
import org.springframework.context.annotation.Bean;
55
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.data.redis.connection.RedisPassword;
67
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
78
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
89
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@@ -17,12 +18,20 @@ public class RedisConfig {
1718
@Value("${spring.data.redis.port}")
1819
private int port;
1920

21+
@Value("${spring.data.redis.username}")
22+
private String username;
23+
24+
@Value("${spring.data.redis.password}")
25+
private String password;
26+
2027
@Bean
2128
public LettuceConnectionFactory redisConnectionFactory() {
29+
// 인증 포함 설정
2230
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(host, port);
31+
redisConfig.setUsername(username);
32+
redisConfig.setPassword(RedisPassword.of(password));
2333

2434
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
25-
.useSsl() // TLS 활성화
2635
.build();
2736

2837
return new LettuceConnectionFactory(redisConfig, clientConfig);

0 commit comments

Comments
 (0)