Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@

## 6. 최종 성능 개선 목표 및 달성 현황 요약

| 구분 | 목표치 (Thresholds) | v1 (기존) | v4 (최종) | 결과 |
| :--- | :--- |:----------| :--- | :--- |
| **p(95) Latency** | **2.0s 미만** | 6.71s | **2.56s** | **미달 (CPU 자원 임계)** |
| **p(99) Latency** | **5.0s 미만** | **7.94s** | **3.98s** | **통과** |
| **Error Rate** | **1.0% 미만** | 0.005% | **0.009%** | **통과** |
| 구분 | 목표치 (Thresholds) | v1 (기존) | v4 (최종) | 결과 |
| :--- | :--- |:--------| :--- | :--- |
| **p(95) Latency** | **2.0s 미만** | 6.71s | **2.56s** | **미달 (CPU 자원 임계)** |
| **p(99) Latency** | **5.0s 미만** | 7.94s | **3.98s** | **통과** |
| **Error Rate** | **1.0% 미만** | 0.005% | **0.009%** | **통과** |

**향후 목표**:
로직, 인프라, 캐싱 최적화를 통해 비약적인 성능 향상을 거두었으나, 1,000 VU 환경에서 단일 인스턴스의 CPU 부하로 인해 p(95) 2.0s 목표에는 미달했습니다. 향후 **인스턴스 확장(Scale-out)** 및 **로드밸런서(ALB)** 적용을 통해 자원 부하를 분산하고 최종 목표 지표를 달성할 예정입니다.
16 changes: 11 additions & 5 deletions src/main/java/com/backend/farmon/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;

@Configuration
public class RedisConfig {

Expand Down Expand Up @@ -81,15 +83,19 @@ public RedisTemplate<String, String> autoSearchLogRedisTemplate() {
}

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(java.time.Duration.ofSeconds(60))
public RedisCacheManager redisCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
RedisCacheConfiguration homeCommunity = base.entryTtl(Duration.ofSeconds(60));
RedisCacheConfiguration popularExpert = base.entryTtl(Duration.ofMinutes(5));

return RedisCacheManager.builder(cf)
.cacheDefaults(base)
.withCacheConfiguration("home:community", homeCommunity)
.withCacheConfiguration("home:popularExpertColumn", popularExpert)
.build();
Comment on lines 86 to 98
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

다중 캐시 TTL 구성이 적절합니다.

캐시별 TTL 분리(home:community 60초, home:popularExpertColumn 5분)가 데이터 특성에 맞게 잘 설정되었습니다.

다만, base 설정에 기본 TTL이 없어 명시적으로 등록되지 않은 캐시는 무제한 TTL을 갖게 됩니다. 향후 다른 캐시가 추가될 경우를 대비해 기본 TTL 설정을 고려해 보세요.

🔧 기본 TTL 추가 제안
 RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
+        .entryTtl(Duration.ofMinutes(10)) // 기본 TTL
         .disableCachingNullValues()
         .serializeKeysWith(...)
         .serializeValuesWith(...);
📝 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 RedisCacheManager redisCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
RedisCacheConfiguration homeCommunity = base.entryTtl(Duration.ofSeconds(60));
RedisCacheConfiguration popularExpert = base.entryTtl(Duration.ofMinutes(5));
return RedisCacheManager.builder(cf)
.cacheDefaults(base)
.withCacheConfiguration("home:community", homeCommunity)
.withCacheConfiguration("home:popularExpertColumn", popularExpert)
.build();
public RedisCacheManager redisCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 기본 TTL
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
RedisCacheConfiguration homeCommunity = base.entryTtl(Duration.ofSeconds(60));
RedisCacheConfiguration popularExpert = base.entryTtl(Duration.ofMinutes(5));
return RedisCacheManager.builder(cf)
.cacheDefaults(base)
.withCacheConfiguration("home:community", homeCommunity)
.withCacheConfiguration("home:popularExpertColumn", popularExpert)
.build();
🤖 Prompt for AI Agents
In @src/main/java/com/backend/farmon/config/RedisConfig.java around lines 86 -
99, The Redis cache configuration currently defines `base` without a default TTL
which leaves unspecified caches with unlimited TTL; update the
`redisCacheManager` method to set an explicit default TTL on `base` (e.g., call
entryTtl(...) on `base` before deriving `homeCommunity` and `popularExpert`) so
all caches created from `RedisCacheManager.builder(cf)` inherit a safe default
TTL while preserving the per-cache overrides (`homeCommunity`, `popularExpert`)
and keep the `withCacheConfiguration("home:community", ...)` /
`withCacheConfiguration("home:popularExpertColumn", ...)` calls unchanged.

}
}
30 changes: 11 additions & 19 deletions src/main/java/com/backend/farmon/converter/HomeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.backend.farmon.domain.User;
import com.backend.farmon.dto.home.HomePostRow;
import com.backend.farmon.dto.home.HomeResponse;
import com.backend.farmon.dto.home.PopularExpertPostRow;

import java.util.List;
import java.util.Optional;
Expand All @@ -27,33 +28,24 @@ public static HomeResponse.PostListDTO toPostListDTO(List<HomePostRow> rows) {
return new HomeResponse.PostListDTO(list);
}

public static HomeResponse.PopularPostListDTO toPopularPostListDTO(List<Post> postList) {
List<HomeResponse.PopularPostDetailDTO> popularPostDetailDTOList = postList.stream()
.map(HomeConverter::toPopularPostDetailListDTO)
public static HomeResponse.PopularPostListDTO toPopularPostListDTO(List<PopularExpertPostRow> rows) {
List<HomeResponse.PopularPostDetailDTO> popularPostDetailDTOList = rows.stream()
.map(HomeConverter::toPopularPostDetailDTOF)
.toList();

return HomeResponse.PopularPostListDTO.builder()
.popularPostList(popularPostDetailDTOList)
.build();
}

public static HomeResponse.PopularPostDetailDTO toPopularPostDetailListDTO(Post post) {
public static HomeResponse.PopularPostDetailDTO toPopularPostDetailDTOF(PopularExpertPostRow row) {
return HomeResponse.PopularPostDetailDTO.builder()
.popularPostId(post.getId())
.popularPostTitle(post.getPostTitle())
.popularPostContent(post.getPostContent())
.writer(post.getUser().getUserName())
.profileImage(Optional.ofNullable(post.getUser())
.map(User::getExpert)
.map(Expert::getProfileImageUrl)
.orElse(null))
.popularPostImage(
Optional.ofNullable(post.getPostImgs())
.filter(list -> !list.isEmpty()) // 리스트가 비어 있지 않은 경우만 처리
.map(list -> list.get(0)) // 첫 번째 이미지 가져오기
.map(PostImg::getStoredFileName) // 파일명 가져오기
.orElse(null) // 없으면 null 반환
)
.popularPostId(row.getPostId())
.popularPostTitle(row.getTitle())
.popularPostContent(row.getContent())
.writer(row.getWriter())
.profileImage(row.getProfileImageUrl())
.popularPostImage(row.getFirstImageStoredFileName())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.backend.farmon.dto.home;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class PopularExpertPostRow {
private Long postId;
private String title;
private String content;
private String writer;
private String profileImageUrl;
private String firstImageStoredFileName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.backend.farmon.domain.Post;
import com.backend.farmon.dto.home.HomePostRow;
import com.backend.farmon.dto.home.PopularExpertPostRow;
import com.backend.farmon.dto.post.PostType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -20,7 +21,7 @@ public interface PostRepositoryCustom {
List<HomePostRow> findTopPostsByPostTypeWithCounts(PostType postType, int limit);

// 인기 전문가 칼럼 6개 조회
List<Post> findTop6ExpertColumnPostsByPostId(List<Long> popularPostsIdList);
List<PopularExpertPostRow> findTop6ExpertColumnRowsByPopularIds(List<Long> popularPostsIdList);

// 필터링없이 조회
Page<Post> findAllByBoardId(Long boardId, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import com.backend.farmon.apiPayload.exception.GeneralException;
import com.backend.farmon.domain.*;
import com.backend.farmon.dto.home.HomePostRow;
import com.backend.farmon.dto.home.PopularExpertPostRow;
import com.backend.farmon.dto.post.PostType;
import com.backend.farmon.repository.BoardRepository.BoardRepository;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
Expand All @@ -33,8 +35,10 @@ public class PostRepositoryImpl implements PostRepositoryCustom {
private final QBoard board = QBoard.board;
private final QPost originalPost = new QPost("originalPost");
private final QComment comment = QComment.comment;

private final QCrop crop = QCrop.crop;
private final QUser user = QUser.user;
private final QExpert expert = QExpert.expert;
private final QPostImg postImg = QPostImg.postImg;

/**
* 공통 select (원본 게시글 기준) + 좋아요/댓글 count를 한 번에 가져오기 위한 프로젝션
Expand Down Expand Up @@ -142,33 +146,68 @@ public List<HomePostRow> findTopPostsByPostTypeWithCounts(PostType postType, int

// 인기 전문가 칼럼 6개 조회
@Override
public List<Post> findTop6ExpertColumnPostsByPostId(List<Long> popularPostsIdList) {
return queryFactory.select(post)
public List<PopularExpertPostRow> findTop6ExpertColumnRowsByPopularIds(List<Long> popularPostsIdList) {
// 첫 번째 이미지만 가져오기 위한 서브쿼리: 해당 post의 postImg 중 가장 작은 id
QPostImg pi2 = new QPostImg("pi2");

boolean hasPinned = popularPostsIdList != null && !popularPostsIdList.isEmpty();

// 인기 ID 우선 정렬(1) / 나머지(2)
// pinnedFirst = 1이면 상단, 2면 하단
var pinnedFirstOrderExpr = hasPinned
? new CaseBuilder()
.when(post.id.in(popularPostsIdList)).then(1)
.otherwise(2)
: null;

// pinned list 내부 정렬 (MySQL: FIELD)
// pinnedIds가 있으면 FIELD(post.id, [ids]) ASC 로 pinned 내부 순서를 유지
var pinnedInnerOrderExpr = hasPinned
? Expressions.numberTemplate(
Integer.class,
"FIELD({0}, {1})",
post.id,
Expressions.constant(popularPostsIdList)
)
: null;
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
# 테스트 DB 및 database 관련 설정 확인
rg -n "h2|hsql|postgres|mysql|dialect|database" --type yaml --type properties -i | head -100

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 105


🏁 Script executed:

#!/bin/bash
# application.yml/properties 파일 찾기
find . -type f \( -name "application*.yml" -o -name "application*.properties" \) | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# PostRepositoryImpl 파일 확인 및 FIELD 함수 사용 확인
find . -type f -name "PostRepositoryImpl.java" -exec cat -n {} \;

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 16730


🏁 Script executed:

#!/bin/bash
# QueryDSL MySQL 함수 사용 패턴 검색
rg -n "FIELD\|Expressions\.numberTemplate" --type java -C3

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# YAML/YML 파일 찾기
find . -type f \( -name "*.yml" -o -name "*.yaml" \) | grep -E "(application|config)" | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# pom.xml 또는 build.gradle에서 DB 의존성 확인
find . -type f \( -name "pom.xml" -o -name "build.gradle" \) -exec cat {} \; 2>/dev/null | grep -E "h2|mysql|postgres|hsql|database" -i | head -30

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 158


🏁 Script executed:

#!/bin/bash
# src/main/resources 폴더의 설정 파일 확인
ls -la src/main/resources/ 2>/dev/null
ls -la src/test/resources/ 2>/dev/null

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# 모든 application 설정 파일 내용 확인
find . -path "*/resources/application*.yml" -o -path "*/resources/application*.yaml" -o -path "*/resources/application*.properties" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# 테스트 설정 파일 찾기
find . -path "*/test/resources/*" -type f | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# PostRepository 관련 테스트 파일 찾기
find . -type f -name "*PostRepository*Test*.java" -o -name "*Test*PostRepository*.java"

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


🏁 Script executed:

#!/bin/bash
# build.gradle 전체 내용 확인하여 H2 범위(scope) 확인
find . -name "build.gradle" -exec cat {} \; | grep -A 5 -B 5 "h2"

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 525


🏁 Script executed:

#!/bin/bash
# JPA Dialect 설정 확인
rg -n "dialect|Dialect" --type java | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 53


MySQL 전용 FIELD() 함수 사용으로 인한 데이터베이스 호환성 문제

FIELD() 함수는 MySQL에서만 지원되는 함수입니다. 프로젝트 의존성에 H2 데이터베이스가 포함되어 있어, 테스트 환경이나 개발 환경에서 H2를 사용할 경우 이 쿼리는 실패합니다.

권장사항:

  1. 데이터베이스 독립적인 정렬 방식으로 변경 (Java 메모리 정렬 또는 QueryDSL의 CaseBuilder 활용)
  2. 또는 데이터베이스별 dialect에 따른 조건부 쿼리 구현
  3. 또는 애플리케이션 수준에서 popularPostsIdList 순서에 따른 정렬 로직 추가


return queryFactory
.select(Projections.constructor(
PopularExpertPostRow.class,
post.id,
post.postTitle,
post.postContent,
user.userName,
expert.profileImageUrl,
postImg.storedFileName
))
.from(post)
.join(post.board, board).fetchJoin()
.leftJoin(post.postlikes, likeCount)
.where(
board.postType.eq(PostType.EXPERT_COLUMN) // 전문가 칼럼 조건
.and(
popularPostsIdList != null && !popularPostsIdList.isEmpty()
? post.id.in(popularPostsIdList).or(post.id.notIn(popularPostsIdList))
: null
)
.join(post.board, board)
.join(post.user, user)
.leftJoin(user.expert, expert)
.leftJoin(likeCount).on(likeCount.post.id.eq(post.id))
// 첫 이미지 1개만 LEFT JOIN
.leftJoin(postImg).on(postImg.id.eq(
JPAExpressions.select(pi2.id.min())
.from(pi2)
.where(pi2.post.id.eq(post.id))
))
.where(board.postType.eq(PostType.EXPERT_COLUMN))
.groupBy(
post.id,
post.postTitle,
post.postContent,
user.userName,
expert.profileImageUrl,
postImg.storedFileName
)
.groupBy(post)
.orderBy(
// 인기 게시글 우선 정렬
popularPostsIdList != null && !popularPostsIdList.isEmpty()
? Expressions.stringTemplate("CASE WHEN {0} IN ({1}) THEN 1 ELSE 2 END", post.id, Expressions.constant(popularPostsIdList)).asc()
: null,
// popularPostsIdList 내부 정렬
popularPostsIdList != null && !popularPostsIdList.isEmpty()
? Expressions.stringTemplate("FIELD({0}, {1})", post.id, Expressions.constant(popularPostsIdList)).asc()
: null,
likeCount.count().desc(), // 좋아요 개수 내림차순
post.createdAt.desc() // 작성일 내림차순
pinnedFirstOrderExpr != null ? pinnedFirstOrderExpr.asc() : null,
pinnedInnerOrderExpr != null ? pinnedInnerOrderExpr.asc() : null,
likeCount.id.countDistinct().desc(),
post.createdAt.desc()
)
.limit(6) // 6개 제한
.limit(6)
.fetch();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,64 @@ public class LikeServiceImpl {
private final LikeCountRepository likeCountRepository;
private final UserAuthorizationUtil userAuthorizationUtil;

// 추가: 캐시 수동 evict를 위한 주입
// 캐시 수동 evict
private final CacheManager cacheManager;

// 캐시 무효화 (커밋 이후 실행)
/**
* 캐시 무효화 (트랜잭션 커밋 이후 실행)
*/
private void evictHomeCommunityCacheAfterCommit(PostType postType) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
evictHomeCommunityCacheNow(postType);
evictHomeCommunityCachesNow(postType);
return;
}

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
evictHomeCommunityCacheNow(postType);
evictHomeCommunityCachesNow(postType);
}
});
}

// 캐시 무효화 (즉시 실행)
private void evictHomeCommunityCacheNow(PostType postType) {
Cache cache = cacheManager.getCache("home:community");
if (cache == null) return;
/**
* 캐시 무효화 (즉시 실행)
* - home:community:popular (POPULAR 전용 캐시) 추가 무효화
* - home:community (기존 캐시)도 함께 무효화
*/
private void evictHomeCommunityCachesNow(PostType postType) {
// 1) 기존 캐시(home:community) 무효화
Cache communityCache = cacheManager.getCache("home:community");
if (communityCache != null) {
// POPULAR은 항상 무효화
communityCache.evict("category:POPULAR");

// 특정 타입 캐시만 무효화 (ALL/POPULAR 제외)
if (postType != null && postType != PostType.ALL && postType != PostType.POPULAR) {
communityCache.evict("category:" + postType.name());
}
}

// POPULAR은 항상 무효화
cache.evict("category:POPULAR");
// 2) POPULAR 전용 캐시(home:community:popular)도 같이 무효화 (TTL 길게 가져갈 경우 필수)
Cache popularCache = cacheManager.getCache("home:community:popular");
if (popularCache != null) {
popularCache.evict("category:POPULAR");
}

// default 케이스(특정 타입)만 무효화하고 싶다면 ALL/POPULAR은 제외
if (postType != null && postType != PostType.ALL && postType != PostType.POPULAR) {
cache.evict("category:" + postType.name());
// 3) (선택) 인기 전문가 칼럼 별도 캐시를 쓰는 경우 함께 무효화
// PostType.EXPERT_COLUMN의 인기 칼럼 리스트가 likeCount 기반 정렬에 영향받는 구조라면 추천
if (postType == PostType.EXPERT_COLUMN) {
Cache popularExpertCache = cacheManager.getCache("home:popularExpertColumn");
if (popularExpertCache != null) {
popularExpertCache.evict("list:v1");
}
}
}

private void validateRole() throws IllegalAccessException {
/**
* 권한 검증: checked exception 사용하지 않음 (GeneralException으로 통일)
*/
private void validateRole() {
String currentUserRole = userAuthorizationUtil.getCurrentUserRole();
if (!"FARMER".equals(currentUserRole) && !"EXPERT".equals(currentUserRole)) {
throw new GeneralException(ErrorStatus.UNAUTHORIZED_ACCESS);
Expand All @@ -77,7 +102,7 @@ private Post getOriginalPost(Post post) {

// 좋아요 추가
@Transactional
public void postLikeUp(Long userId, Long postId) throws IllegalAccessException {
public void postLikeUp(Long userId, Long postId) {
validateRole();

User user = userRepository.findById(userId)
Expand All @@ -95,11 +120,10 @@ public void postLikeUp(Long userId, Long postId) throws IllegalAccessException {
throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED);
}

// 중복 좋아요 방지 (originalPostId가 같은 모든 게시물 체크)
for (Post relatedPost : relatedPosts) {
if (likeCountRepository.findByUserIdAndPostId(userId, relatedPost.getId()) != null) {
throw new IllegalAccessException("이미 좋아요를 눌렀습니다!");
}
// 중복 좋아요 방지 (원본 id 기준으로 한 번만 체크하는 게 더 안전/효율적)
// 기존 로직 유지가 필요하면 relatedPosts 전체를 검사해도 되지만, like는 originalPost에 저장하므로 originalPost만 확인해도 충분함
if (likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId()) != null) {
throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED); // 필요 시 별도 에러 코드 권장
}

// 원본 게시물 기준으로 좋아요 저장
Expand All @@ -117,14 +141,14 @@ public void postLikeUp(Long userId, Long postId) throws IllegalAccessException {
postRepository.saveAll(relatedPosts);
postRepository.flush();

// 커밋 이후 캐시 무효화 (POPULAR + 해당 타입)
// 커밋 이후 캐시 무효화 (POPULAR + 해당 타입 + popular 캐시)
PostType postType = originalPost.getBoard().getPostType();
evictHomeCommunityCacheAfterCommit(postType);
}

// 좋아요 감소
@Transactional
public void postLikeDown(Long userId, Long postId) throws IllegalAccessException {
public void postLikeDown(Long userId, Long postId) {
validateRole();

User user = userRepository.findById(userId)
Expand All @@ -138,7 +162,7 @@ public void postLikeDown(Long userId, Long postId) throws IllegalAccessException
// 좋아요 찾기 (원본 게시물 기준)
LikeCount like = likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId());
if (like == null) {
throw new IllegalAccessException("좋아요를 누른 적이 없습니다!");
throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED); // 필요 시 별도 에러 코드 권장
}
Comment on lines 163 to 166
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

좋아요 미존재 시 부적절한 에러 코드 사용

좋아요가 존재하지 않는 경우에도 LIKE_TYPE_NOT_SAVED를 사용하고 있습니다. 이 에러 코드는 "저장되지 않음"을 의미하지만, 실제로는 "삭제할 좋아요가 없음"을 나타내야 합니다. LIKE_NOT_FOUND와 같은 별도 에러 코드 사용을 권장합니다.

🐛 개선 제안
 LikeCount like = likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId());
 if (like == null) {
-    throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED);
+    throw new GeneralException(ErrorStatus.LIKE_NOT_FOUND);
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java
around lines 163 - 166, In LikeServiceImpl, the block that checks
likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId()) throws
GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED) for a missing like; change
this to throw a more appropriate error (e.g.,
GeneralException(ErrorStatus.LIKE_NOT_FOUND)) to reflect "like not found" when
deleting, and if ErrorStatus.LIKE_NOT_FOUND doesn't exist add that enum/constant
to ErrorStatus and use it in the GeneralException to accurately represent the
condition.


// 좋아요 삭제
Expand All @@ -155,7 +179,7 @@ public void postLikeDown(Long userId, Long postId) throws IllegalAccessException
postRepository.saveAll(relatedPosts);
postRepository.flush();

// 커밋 이후 캐시 무효화 (POPULAR + 해당 타입)
// 커밋 이후 캐시 무효화 (POPULAR + 해당 타입 + popular 캐시)
PostType postType = originalPost.getBoard().getPostType();
evictHomeCommunityCacheAfterCommit(postType);
}
Expand Down
Loading
Loading