Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.example.solidconnection.cache;

import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL;
import static com.example.solidconnection.community.post.service.RedisConstants.LOCK_TIMEOUT_MS;
import static com.example.solidconnection.community.post.service.RedisConstants.MAX_WAIT_TIME_MS;
import static com.example.solidconnection.community.post.service.RedisConstants.REFRESH_LIMIT_PERCENT;
import static com.example.solidconnection.redis.RedisConstants.CREATE_CHANNEL;
import static com.example.solidconnection.redis.RedisConstants.LOCK_TIMEOUT_MS;
import static com.example.solidconnection.redis.RedisConstants.MAX_WAIT_TIME_MS;
import static com.example.solidconnection.redis.RedisConstants.REFRESH_LIMIT_PERCENT;

import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
import com.example.solidconnection.cache.manager.CacheManager;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.solidconnection.common.config.redis;

import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL;
import static com.example.solidconnection.redis.RedisConstants.CREATE_CHANNEL;

import com.example.solidconnection.cache.CacheUpdateListener;
import org.springframework.beans.factory.annotation.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public enum ErrorCode {
INVALID_BOARD_CODE(HttpStatus.BAD_REQUEST.value(), "잘못된 게시판 코드입니다."),
INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."), // todo: NOT_FOUND로 통일 필요
INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."),
DUPLICATE_POST_CREATE_REQUEST(HttpStatus.BAD_REQUEST.value(), "게시글이 이미 생성 중입니다."),
CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."),
CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."),
INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION;
import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES;
import static com.example.solidconnection.common.exception.ErrorCode.DUPLICATE_POST_CREATE_REQUEST;
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_ACCESS;
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_CATEGORY;
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
Expand All @@ -23,7 +24,6 @@
import com.example.solidconnection.s3.service.S3Service;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.util.RedisUtils;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
Expand All @@ -40,14 +40,19 @@ public class PostCommandService {
private final BoardRepository boardRepository;
private final SiteUserRepository siteUserRepository;
private final S3Service s3Service;
private final RedisService redisService;
private final RedisUtils redisUtils;
private final PostRedisManager postRedisManager;

@Transactional
public PostCreateResponse createPost(long siteUserId, PostCreateRequest postCreateRequest,
List<MultipartFile> imageFile) {
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));

// 중복 생성 방지
if (!postRedisManager.isPostCreationAllowed(siteUserId)) {
throw new CustomException(DUPLICATE_POST_CREATE_REQUEST);
}

// 유효성 검증
validatePostCategory(postCreateRequest.postCategory());
validateFileSize(imageFile);
Expand Down Expand Up @@ -104,8 +109,7 @@ public PostDeleteResponse deletePostById(long siteUserId, Long postId) {
validateQuestion(post);

removePostImages(post);
// cache out
redisService.deleteKey(redisUtils.getPostViewCountRedisKey(postId));
postRedisManager.deleteViewCountCache(postId);
postRepository.deleteById(post.getId());

return new PostDeleteResponse(postId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.siteuser.repository.UserBlockRepository;
import com.example.solidconnection.util.RedisUtils;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
Expand All @@ -42,8 +41,7 @@ public class PostQueryService {
private final SiteUserRepository siteUserRepository;
private final UserBlockRepository userBlockRepository;
private final CommentService commentService;
private final RedisService redisService;
private final RedisUtils redisUtils;
private final PostRedisManager postRedisManager;

@Transactional(readOnly = true)
public List<PostListResponse> findPostsByCodeAndPostCategoryOrderByCreatedAtDesc(String code, String category, Long siteUserId) {
Expand Down Expand Up @@ -81,10 +79,7 @@ public PostFindResponse findPostById(long siteUserId, Long postId) {
List<PostFindPostImageResponse> postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList());
List<PostFindCommentResponse> commentFindResultDTOList = commentService.findCommentsByPostId(siteUser.getId(), postId);

// caching && 어뷰징 방지
if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), postId))) {
redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId));
}
postRedisManager.incrementViewCountIfFirstAccess(siteUser.getId(), postId);

return PostFindResponse.from(
post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.example.solidconnection.community.post.service;

import static com.example.solidconnection.redis.RedisConstants.POST_CREATE_PREFIX;
import static com.example.solidconnection.redis.RedisConstants.VALIDATE_POST_CREATE_TTL;
import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX;
import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_TTL;
import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_KEY_PREFIX;

import com.example.solidconnection.redis.RedisService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class PostRedisManager {

private final RedisService redisService;

public Long getPostIdFromPostViewCountRedisKey(String key) {
return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length()));
}

public Long getAndDeleteViewCount(String key) {
return redisService.getAndDelete(key);
}

public void deleteViewCountCache(Long postId) {
String key = getPostViewCountRedisKey(postId);
redisService.deleteKey(key);
}

public void incrementViewCountIfFirstAccess(long siteUserId, Long postId) {
String validateKey = getValidatePostViewCountRedisKey(siteUserId, postId);
boolean isFirstAccess = redisService.isPresent(validateKey, VALIDATE_VIEW_COUNT_TTL.getValue());

if (isFirstAccess) {
String viewCountKey = getPostViewCountRedisKey(postId);
redisService.increaseViewCount(viewCountKey);
}
}

public String getPostViewCountRedisKey(Long postId) {
return VIEW_COUNT_KEY_PREFIX.getValue() + postId;
}

public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) {
return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId;
}

public boolean isPostCreationAllowed(Long siteUserId) {
String key = getPostCreateRedisKey(siteUserId);
return redisService.isPresent(key, VALIDATE_POST_CREATE_TTL.getValue());
}

public String getPostCreateRedisKey(Long siteUserId) {
return POST_CREATE_PREFIX.getValue() + siteUserId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.example.solidconnection.community.post.domain.Post;
import com.example.solidconnection.community.post.repository.PostRepository;
import com.example.solidconnection.util.RedisUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
Expand All @@ -17,14 +16,14 @@
public class UpdateViewCountService {

private final PostRepository postRepository;
private final RedisService redisService;
private final RedisUtils redisUtils;
private final PostRedisManager postRedisManager;

@Transactional
@Async
public void updateViewCount(String key) {
Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key);
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Post post = postRepository.getById(postId);
postRepository.increaseViewCount(postId, redisService.getAndDelete(key));
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
postRepository.increaseViewCount(postId, viewCount);
}
Comment on lines 23 to 28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the UpdateViewCountService file
find . -name "UpdateViewCountService.java" -type f

Repository: solid-connection/solid-connect-server

Length of output: 175


🏁 Script executed:

# If found, read the full file with line numbers
cat -n src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java 2>/dev/null || echo "File not found, searching for it..."

Repository: solid-connection/solid-connect-server

Length of output: 1331


🏁 Script executed:

# Search for PostRedisManager to understand getAndDeleteViewCount
rg -n 'class PostRedisManager' -A 50 --type=java

Repository: solid-connection/solid-connect-server

Length of output: 5607


🏁 Script executed:

# Search for getAndDeleteViewCount implementation
rg -n 'getAndDeleteViewCount' --type=java -B 3 -A 10

Repository: solid-connection/solid-connect-server

Length of output: 2848


🏁 Script executed:

# Search for increaseViewCount implementation
rg -n 'increaseViewCount' --type=java -B 3 -A 10

Repository: solid-connection/solid-connect-server

Length of output: 13808


불필요한 DB 조회 제거 및 null 안전성 처리가 필요합니다.

아래 두 가지 개선 사항을 적용하세요:

  1. 미사용 엔티티 조회 제거 (라인 25): Post post = postRepository.getById(postId);로 조회한 엔티티가 이후에 사용되지 않습니다. 불필요한 DB 쿼리이므로 제거하세요.

  2. Null 안전성 처리 (라인 26–27): getAndDeleteViewCount(key)는 Redis 키가 만료되거나 존재하지 않을 경우 null을 반환할 수 있습니다. 이 값이 null일 때 increaseViewCount()에 전달되면 SQL 쿼리가 예상치 못한 동작을 할 수 있으므로, null 체크를 추가하세요.

수정 제안
     `@Transactional`
     `@Async`
     public void updateViewCount(String key) {
         Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
-        Post post = postRepository.getById(postId);
         Long viewCount = postRedisManager.getAndDeleteViewCount(key);
-        postRepository.increaseViewCount(postId, viewCount);
+        if (viewCount != null && viewCount > 0) {
+            postRepository.increaseViewCount(postId, viewCount);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void updateViewCount(String key) {
Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key);
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Post post = postRepository.getById(postId);
postRepository.increaseViewCount(postId, redisService.getAndDelete(key));
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
postRepository.increaseViewCount(postId, viewCount);
}
public void updateViewCount(String key) {
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
if (viewCount != null && viewCount > 0) {
postRepository.increaseViewCount(postId, viewCount);
}
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java`
around lines 23 - 28, Remove the unnecessary DB fetch and add null-safety: in
updateViewCount, drop the unused postRepository.getById(postId) call and instead
call postRedisManager.getAndDeleteViewCount(key) into a Long viewCount variable,
then guard against null (e.g., if viewCount == null || viewCount == 0 return; or
treat null as 0) before calling postRepository.increaseViewCount(postId,
viewCount); ensure you still obtain postId via
postRedisManager.getPostIdFromPostViewCountRedisKey(key).

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.solidconnection.community.post.service;
package com.example.solidconnection.redis;

import lombok.Getter;

Expand All @@ -15,7 +15,10 @@ public enum RedisConstants {
REFRESH_LOCK_PREFIX("refresh_lock:"),
LOCK_TIMEOUT_MS("10000"),
MAX_WAIT_TIME_MS("3000"),
CREATE_CHANNEL("create_channel");
CREATE_CHANNEL("create_channel"),

POST_CREATE_PREFIX("post_create_lock:"),
VALIDATE_POST_CREATE_TTL("5");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.example.solidconnection.community.post.service;
package com.example.solidconnection.redis;

import static com.example.solidconnection.community.post.service.RedisConstants.VALIDATE_VIEW_COUNT_TTL;
import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_TTL;
import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_TTL;
import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_TTL;
Comment on lines +3 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# RedisService.java에서 두 상수의 실제 사용 여부 확인
echo "=== VALIDATE_VIEW_COUNT_TTL 사용 확인 ==="
fd "RedisService.java" --type f -x grep -n "VALIDATE_VIEW_COUNT_TTL" {}

echo -e "\n=== VIEW_COUNT_TTL 사용 확인 ==="
fd "RedisService.java" --type f -x grep -n "VIEW_COUNT_TTL" {}

echo -e "\n=== 전체 import 문 확인 ==="
fd "RedisService.java" --type f -x head -20 {}

Repository: solid-connection/solid-connect-server

Length of output: 1325


VALIDATE_VIEW_COUNT_TTL 미사용 import 제거 필요합니다.

확인 결과:

  1. VALIDATE_VIEW_COUNT_TTL - 파일 내에서 실제로 사용되지 않으며 import 문에만 존재합니다. 제거해야 합니다.

  2. VIEW_COUNT_TTL - 29번째 줄에서 VIEW_COUNT_TTL.getValue() 형태로 활발히 사용 중입니다. 이 import는 유지해야 합니다.

3번째 줄의 VALIDATE_VIEW_COUNT_TTL import를 삭제하시기 바랍니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/example/solidconnection/redis/RedisService.java` around
lines 3 - 4, Remove the unused static import VALIDATE_VIEW_COUNT_TTL from
RedisService.java; keep the existing static import for VIEW_COUNT_TTL (used as
VIEW_COUNT_TTL.getValue()) and ensure no other references to
VALIDATE_VIEW_COUNT_TTL remain in the class (remove any accidental leftover
usages if present).


import java.util.Collections;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -37,9 +37,9 @@ public Long getAndDelete(String key) {
return Long.valueOf(redisTemplate.opsForValue().getAndDelete(key));
}

public boolean isPresent(String key) {
public boolean isPresent(String key, String ttl) {
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(key, "1", Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()), TimeUnit.SECONDS));
.setIfAbsent(key, "1", Long.parseLong(ttl), TimeUnit.SECONDS));
}

public boolean isKeyExists(String key) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.solidconnection.scheduler;

import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_KEY_PATTERN;
import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_KEY_PATTERN;

import com.example.solidconnection.community.post.service.UpdateViewCountService;
import com.example.solidconnection.util.RedisUtils;
Expand Down
18 changes: 2 additions & 16 deletions src/main/java/com/example/solidconnection/util/RedisUtils.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.example.solidconnection.util;

import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_LOCK_PREFIX;
import static com.example.solidconnection.community.post.service.RedisConstants.REFRESH_LOCK_PREFIX;
import static com.example.solidconnection.community.post.service.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX;
import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_KEY_PREFIX;
import static com.example.solidconnection.redis.RedisConstants.CREATE_LOCK_PREFIX;
import static com.example.solidconnection.redis.RedisConstants.REFRESH_LOCK_PREFIX;

import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -39,18 +37,6 @@ public Long getExpirationTime(String key) {
return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
}

public String getPostViewCountRedisKey(Long postId) {
return VIEW_COUNT_KEY_PREFIX.getValue() + postId;
}

public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) {
return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId;
}

public Long getPostIdFromPostViewCountRedisKey(String key) {
return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length()));
}

public String generateCacheKey(String keyPattern, Object[] args) {
for (int i = 0; i < args.length; i++) {
// 키 패턴에 {i}가 포함된 경우에만 해당 인덱스의 파라미터를 삽입
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
import com.example.solidconnection.community.post.fixture.PostFixture;
import com.example.solidconnection.community.post.fixture.PostImageFixture;
import com.example.solidconnection.community.post.repository.PostRepository;
import com.example.solidconnection.redis.RedisService;
import com.example.solidconnection.s3.domain.UploadPath;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
import com.example.solidconnection.s3.service.S3Service;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.example.solidconnection.util.RedisUtils;
import jakarta.transaction.Transactional;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -57,7 +57,7 @@ class PostCommandServiceTest {
private RedisService redisService;

@Autowired
private RedisUtils redisUtils;
private PostRedisManager postRedisManager;

@Autowired
private PostRepository postRepository;
Expand Down Expand Up @@ -266,7 +266,7 @@ class 게시글_삭제_테스트 {
// given
String originImageUrl = "origin-image-url";
postImageFixture.게시글_이미지(originImageUrl, post);
String viewCountKey = redisUtils.getPostViewCountRedisKey(post.getId());
String viewCountKey = postRedisManager.getPostViewCountRedisKey(post.getId());
redisService.increaseViewCount(viewCountKey);

// when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
import com.example.solidconnection.community.post.dto.PostListResponse;
import com.example.solidconnection.community.post.fixture.PostFixture;
import com.example.solidconnection.community.post.fixture.PostImageFixture;
import com.example.solidconnection.redis.RedisService;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
import com.example.solidconnection.siteuser.fixture.UserBlockFixture;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.example.solidconnection.util.RedisUtils;
import java.time.ZonedDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -40,7 +40,7 @@ class PostQueryServiceTest {
private RedisService redisService;

@Autowired
private RedisUtils redisUtils;
private PostRedisManager postRedisManager;

@Autowired
private SiteUserFixture siteUserFixture;
Expand Down Expand Up @@ -176,8 +176,8 @@ void setUp() {
Comment comment2 = commentFixture.부모_댓글("댓글2", post, user);
List<Comment> comments = List.of(comment1, comment2);

String validateKey = redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId());
String viewCountKey = redisUtils.getPostViewCountRedisKey(post.getId());
String validateKey = postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId());
String viewCountKey = postRedisManager.getPostViewCountRedisKey(post.getId());

// when
PostFindResponse response = postQueryService.findPostById(user.getId(), post.getId());
Expand Down
Loading
Loading