Skip to content

✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 도입 및 DTO 조회 구조 개선#23

Merged
mmije0ng merged 4 commits intodevelopfrom
refactor/k6
Jan 8, 2026
Merged

✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 도입 및 DTO 조회 구조 개선#23
mmije0ng merged 4 commits intodevelopfrom
refactor/k6

Conversation

@mmije0ng
Copy link
Owner

@mmije0ng mmije0ng commented Jan 8, 2026

#️⃣연관된 이슈

📝작업 내용

홈 화면 인기 전문가 칼럼 조회 API의 성능 및 캐시 안정성을 개선함.

  • 인기 전문가 칼럼 조회 결과를 Redis 캐시(home:popularExpertColumn, key=list:v1)로 저장하도록 추가함
  • 캐시 저장 대상을 엔티티가 아닌 DTO로 통일하여 직렬화 및 LAZY 로딩 이슈를 제거함
  • 좋아요 변경 시 트랜잭션 커밋 이후(afterCommit)에 관련 홈 캐시를 무효화하도록 처리하여 데이터 정합성을 보장함

조회 구조 개선(불필요 쿼리 방지):

  • 기존에는 List<Post> 조회 후 Converter에서 연관 엔티티(user, expert, postImgs)에 접근하는 구조였음
  • 환경/설정/데이터 상태에 따라 추가 쿼리가 1~N개 발생할 수 있어(항상 N+1은 아니지만) N+1 가능성이 존재했음
  • QueryDSL DTO 프로젝션(전용 Row)을 도입하여 필요한 필드만 한 번의 쿼리로 조회하도록 변경함
  • 작성자명, 전문가 프로필 이미지, 첫 번째 게시글 이미지 정보를 조회 단계에서 함께 가져오도록 수정함

Converter 책임 분리:

  • Post 엔티티 기반 변환 로직과 Row(DTO projection) 기반 변환 로직을 분리함
  • 성능 민감 영역(홈 화면 인기 전문가 칼럼)에서는 엔티티 접근을 제거하고 Row 기반 변환만 사용하도록 정리함

기대 효과:

  • 인기 전문가 칼럼 조회 시 쿼리 수를 1회로 고정하여 성능 변동성을 줄임
  • 캐시 miss 시에도 안정적인 조회 성능 보장
  • 캐시 TTL을 길게 가져가더라도 좋아요 이벤트에 즉시 반영되는 구조 확보

스크린샷 (선택)

스크린샷 2026-01-08 오후 2 58 46

캐시된 데이터가 없을 경우 쿼리

2026-01-08T14:58:34.917+09:00  INFO 31844 --- [nio-8080-exec-5] c.b.f.c.s.JWTAuthenticationFilter        : Authenticated User ID: 1, Role: FARMER
Hibernate: 
    /* select
        post.id,
        post.postTitle,
        post.postContent,
        user.userName,
        expert.profileImageUrl,
        postImg.storedFileName 
    from
        Post post   
    inner join
        post.board as board   
    inner join
        post.user as user   
    left join
        user.expert as expert   
    left join
        LikeCount likeCount with likeCount.post.id = post.id   
    left join
        PostImg postImg with postImg.id = (
            select
                min(pi2.id) 
            from
                PostImg pi2 
            where
                pi2.post.id = post.id
        ) 
    where
        board.postType = ?1 
    group by
        post.id,
        post.postTitle,
        post.postContent,
        user.userName,
        expert.profileImageUrl,
        postImg.storedFileName 
    order by
        case 
            when (post.id = ?2) then ?3 
            else 2 
        end asc,
        FIELD(post.id, ?4) asc,
        count(distinct likeCount.id) desc,
        post.createdAt desc */ select
            p1_0.post_id,
            p1_0.post_title,
            p1_0.post_content,
            u1_0.user_name,
            e1_0.profile_image_url,
            p3_0.stored_file_name 
        from
            post p1_0 
        join
            board b1_0 
                on b1_0.id=p1_0.board_id 
        join
            user u1_0 
                on u1_0.id=p1_0.user_id 
        left join
            expert e1_0 
                on u1_0.id=e1_0.user_id 
        left join
            like_count l1_0 
                on l1_0.post_id=p1_0.post_id 
        left join
            post_img p3_0 
                on p3_0.img_id=(
                    select
                        min(p4_0.img_id) 
                from
                    post_img p4_0 
                where
                    p4_0.post_id=p1_0.post_id
            ) 
        where
            b1_0.post_type=? 
        group by
            p1_0.post_id,
            p1_0.post_title,
            p1_0.post_content,
            u1_0.user_name,
            e1_0.profile_image_url,
            p3_0.stored_file_name 
        order by
            case 
                when (p1_0.post_id=?) then ? 
                else 2 
            end asc,
            field(p1_0.post_id,(?)) asc,
            count(distinct l1_0.id) desc,
            p1_0.created_at desc limit ?

인기 전문가 칼럼 조회 Redis 캐시 및 DTO 조회 구조 개선

핵심 요약

  • 홈 화면 인기 전문가 칼럼 조회를 엔티티 대신 QueryDSL DTO(Row) 프로젝션(PopularExpertPostRow)으로 단일 쿼리로 조회하도록 변경하고, 결과를 Redis 캐시("home:popularExpertColumn", key="list:v1")에 저장하도록 추가.
  • 좋아요 변경 시 트랜잭션 커밋 후(afterCommit) 관련 홈 캐시를 무효화하도록 처리하여 캐시·데이터 일관성 보장.

성능

  • 작성자명, 전문가 프로필 이미지, 게시글의 첫 이미지 등 필요한 필드를 단일 쿼리로 조회해 N+1 문제와 불필요한 연관 조회 제거. 캐시 miss 시에도 쿼리 수를 1회로 고정해 응답 성능의 변동성 감소.
  • pinned(고정) 순서 보존을 위한 CASE/FIELD 기반 정렬 도입으로 정렬 안정성 및 제공된 pinned 순서 유지.

구조

  • PopularExpertPostRow DTO 추가 및 PostRepository(인터페이스/구현) 반환 타입을 List → List로 변경. 쿼리 로직과 메서드명(limit 인자 포함) 조정.
  • HomeConverter를 Row 기반 변환 경로로 분리(엔티티 기반 변환과 책임 분리). 성능 민감 영역(홈 인기)은 Row 변환만 사용하도록 정리해 직렬화/LAZY 문제 회피.
  • Redis 설정에 기본 캐시 구성과 home:popularExpertColumn 전용 TTL(예: 5분) 추가.

안정성

  • 좋아요 이벤트 처리 로직에서 IllegalAccessException 제거, GeneralException 기반 일관된 예외 처리 적용.
  • 좋아요 발생 시 afterCommit에서 홈 관련 여러 캐시(home:community 계열 및 home:popularExpertColumn(list:v1) 포함)를 적절히 무효화해 긴 TTL 사용 시에도 즉시 반영 보장.

변경 파일(요약)

  • RedisConfig: 캐시별 TTL 및 직렬화 설정 추가.
  • dto.home.PopularExpertPostRow: DTO 추가.
  • repository/PostRepository(PostRepositoryCustom/Impl): DTO 프로젝션 쿼리 구현, 메서드 시그니처/정렬 로직 변경.
  • service/PostQueryServiceImpl: @Cacheable("home:popularExpertColumn", key="'list:v1'") 적용 및 조회 로직 수정.
  • converter/HomeConverter: Row 기반 변환으로 리팩토링.
  • service/LikeServiceImpl: 확장된 캐시 무효화 및 예외 처리 정리.

기대 효과

  • 캐시 miss 포함 환경에서도 안정적으로 1회 쿼리로 필요한 데이터 수집 가능 → 성능 예측성 향상.
  • DTO 중심 직렬화로 LAZY 관련 이슈 제거 및 Redis 캐싱 안정화.
  • 좋아요 등 이벤트 발생 시 데이터 정합성을 유지하는 즉시성 있는 캐시 무효화 보장.

홈 화면 인기 전문가 칼럼 조회 API의 성능과 캐시 안정성을 개선함.

- 인기 전문가 칼럼 조회 결과를 Redis 캐시(home:popularExpertColumn)로 저장하도록 추가함
- 캐시 저장 대상을 엔티티가 아닌 DTO로 통일하여 직렬화 및 LAZY 로딩 이슈를 제거함
- 좋아요 변경 시 트랜잭션 커밋 이후(afterCommit)에 캐시를 무효화하도록 처리하여 데이터 정합성을 보장함

조회 구조 개선:
- 기존에는 List<Post> 조회 후 Converter에서 연관 엔티티(user, expert, postImgs)에 접근하는 구조였음
- 환경/설정에 따라 추가 쿼리가 1~N개 발생할 수 있는 구조였으며, 데이터 규모에 따라 N+1 가능성이 존재했음
- QueryDSL DTO 프로젝션(전용 Row)을 도입하여 필요한 필드만 한 번의 쿼리로 조회하도록 변경함
- 첫 번째 이미지, 작성자 정보, 전문가 프로필 이미지를 조회 단계에서 함께 가져오도록 수정함

Converter 책임 분리:
- Post 엔티티 기반 변환 로직과 Row(DTO projection) 기반 변환 로직을 분리함
- 캐시 및 성능 민감 영역에서는 엔티티 접근을 완전히 제거함

결과:
- 인기 전문가 칼럼 조회 시 쿼리 수를 1회로 고정
- 캐시 miss 시에도 안정적인 성능 보장
- 캐시 TTL을 길게 가져가더라도 좋아요 이벤트에 즉시 반영되는 구조 확보
@mmije0ng mmije0ng self-assigned this Jan 8, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Redis 캐시 설정에 다중 TTL을 추가하고, 인기 전문가 칼럼 조회에 @Cacheable("home:popularExpertColumn", key="'list:v1'")를 도입했으며, 조회용 DTO/프로젝션으로 Repository를 리팩토링하고 Like 서비스의 캐시 무효화 범위를 확대했습니다.

Changes

Cohort / File(s) 변경 사항
Redis 캐시 설정
\src/main/java/com/backend/farmon/config/RedisConfig.java``
공용 기본 RedisCacheConfiguration(기본 TTL 60s) 추가 및 직렬화 설정 통일. home:popularExpertColumn에 대해 5분 TTL 별도 등록. bean 파라미터명 변경(redisConnectionFactorycf).
인기 게시물 DTO 및 매퍼
\src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java`, `src/main/java/com/backend/farmon/converter/HomeConverter.java``
새로운 PopularExpertPostRow DTO 추가. HomeConverter의 인기 변환 시 시그니처가 List<Post>List<PopularExpertPostRow>로 변경되고 매핑 필드가 row 기반으로 조정됨(메서드명·시그니처 변경 포함).
저장소 계층 리팩토링
\src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java`, `src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java``
API 및 구현을 findTop6ExpertColumnPostsByPostId(...)findTopExpertColumnRowsByPopularIds(List<Long>, int)로 변경. QueryDSL 프로젝션을 사용해 PopularExpertPostRow 반환, 핀 우선 정렬(FIELD/CaseBuilder), 이미지 서브쿼리 및 조인/그룹핑 추가.
쿼리 서비스 캐싱
\src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java``
findPopularExpertColumnPosts()@Cacheable(cacheNames = "home:popularExpertColumn", key = "'list:v1'", unless = "#result == null") 추가. 상수화된 limit 및 ID 사용, 리포지토리 호출을 row 기반 API로 교체.
Like 서비스 캐시 제거 로직
\src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java``
캐시 제거 메서드명 evictHomeCommunityCacheNowevictHomeCommunityCachesNow, 제거 대상 확대(home:community:POPULAR, home:community:popular, EXPERT_COLUMN 시 home:popularExpertColumn 조건부). 예외 처리 방식 변경(checked → GeneralException) 및 좋아요 중복 처리 로직 단순화.
기타(문서/포맷)
\README.md`, `src/main/java/com/backend/farmon/dto/home/HomePostRow.java``
README 성능 표 포맷팅(공백/정렬) 수정(값 불변). HomePostRow 포맷팅 미세 변경(무의미).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant PSI as PostQueryServiceImpl
    participant CM as CacheManager
    participant PRep as PostRepository
    participant DB as Database

    Client->>PSI: findPopularExpertColumnPosts()
    PSI->>CM: check cache ("home:popularExpertColumn","list:v1")
    alt cache hit
        CM-->>PSI: cached List<PopularExpertPostRow>
    else cache miss
        PSI->>PRep: findTopExpertColumnRowsByPopularIds([4L], 6)
        PRep->>DB: Query (projection -> PopularExpertPostRow)
        DB-->>PRep: rows
        PRep-->>PSI: List<PopularExpertPostRow>
        PSI->>CM: put cache ("home:popularExpertColumn","list:v1")
    end
    PSI-->>Client: Return List<PopularExpertPostRow>
Loading
sequenceDiagram
    actor User
    participant LSI as LikeServiceImpl
    participant LR as LikeRepository
    participant DB as Database
    participant CM as CacheManager

    User->>LSI: postLikeUp(userId, postId)
    LSI->>LR: check existing like for (userId, postId)
    alt like exists
        LSI-->>User: throw GeneralException(LIKE_TYPE_NOT_SAVED)
    else
        LSI->>LR: save Like
        LR->>DB: INSERT like
        DB-->>LR: success
        LSI->>CM: evict "home:community:POPULAR" and "home:community:popular"
        alt postType == EXPERT_COLUMN
            LSI->>CM: evict "home:popularExpertColumn" (key: "list:v1")
        end
        LSI-->>User: Success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 캐시가 깔리고 조회는 가볍게,
인기 칼럼은 메모리에 반짝이며,
핀된 글 먼저, 이미지는 한 장만,
좋아요 누르면 캐시를 부지런히 털고,
당근 모양 코드가 살랑 춤추네!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경의 핵심을 명확하게 반영하고 있습니다: Redis 캐시 도입과 DTO 조회 구조 개선이라는 두 가지 주요 개선사항을 간결하게 요약했습니다.
Description check ✅ Passed PR 설명이 제시된 템플릿의 모든 필수 섹션(연관된 이슈, 작업 내용)을 포함하고 있으며, 변경사항을 상세하게 설명하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java (2)

89-94: 역할 문자열 하드코딩 개선 권장

"FARMER", "EXPERT" 문자열이 하드코딩되어 있습니다. 역할이 추가되거나 변경될 경우 유지보수가 어려워질 수 있습니다. Enum이나 상수로 관리하는 것을 권장합니다.

♻️ 개선 제안
// 상수 또는 Enum 정의 예시
private static final Set<String> ALLOWED_ROLES = Set.of("FARMER", "EXPERT");

private void validateRole() {
    String currentUserRole = userAuthorizationUtil.getCurrentUserRole();
    if (!ALLOWED_ROLES.contains(currentUserRole)) {
        throw new GeneralException(ErrorStatus.UNAUTHORIZED_ACCESS);
    }
}

119-127: 동일한 에러 코드 사용으로 오류 원인 구분 불가

자기 자신 게시물에 좋아요 시도(Line 120)와 중복 좋아요 시도(Line 126) 모두 LIKE_TYPE_NOT_SAVED를 사용하고 있습니다. 클라이언트나 디버깅 시 오류 원인을 구분할 수 없습니다. 주석에서도 "별도 에러 코드 권장"이라고 명시되어 있으니, 각각 다른 에러 코드(예: SELF_LIKE_NOT_ALLOWED, LIKE_ALREADY_EXISTS)를 사용하는 것을 권장합니다.

🐛 개선 제안
 if (user.equals(post.getUser())) {
-    throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED);
+    throw new GeneralException(ErrorStatus.SELF_LIKE_NOT_ALLOWED);
 }

 if (likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId()) != null) {
-    throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED);
+    throw new GeneralException(ErrorStatus.LIKE_ALREADY_EXISTS);
 }
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (3)

119-119: 디버그용 로그가 남아있습니다.

log.info("에러1")는 디버깅 아티팩트로 보입니다. 제거하거나 의미 있는 메시지로 변경해 주세요.

🧹 디버그 로그 제거
-        log.info("에러1");

140-140: crops가 null일 경우 NPE가 발생할 수 있습니다.

crops가 null이면 crops.toString() 호출 시 NullPointerException이 발생합니다.

🐛 NPE 방지
-        log.info(crops.toString());
+        log.info("crops: {}", crops);

103-103: 중복된 @Transactional(readOnly = true) 어노테이션입니다.

클래스 레벨(Line 37)에 이미 @Transactional(readOnly = true)가 선언되어 있으므로, 메서드 레벨의 동일한 어노테이션은 제거해도 됩니다. Lines 125, 146, 170, 192, 262도 동일합니다.

🤖 Fix all issues with AI agents
In @src/main/java/com/backend/farmon/config/RedisConfig.java:
- Around line 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.

In @src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java:
- Around line 6-15: The DTO PopularExpertPostRow lacks a no-args constructor
required for Redis GenericJackson2JsonRedisSerializer deserialization; add a
public no-argument constructor (e.g., via Lombok @NoArgsConstructor or an
explicit public PopularExpertPostRow() {}) so instances can be created during
deserialization while keeping the existing @AllArgsConstructor and @Getter.

In
@src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java:
- Around line 204-209: The orderBy call can receive null OrderSpecifier values
(pinnedFirstOrderExpr, pinnedInnerOrderExpr) which causes NPEs; update the
ordering to build a list/stream of OrderSpecifier expressions (include
pinnedFirstOrderExpr.asc() and pinnedInnerOrderExpr.asc() only when non-null,
plus likeCount.id.countDistinct().desc() and post.createdAt.desc()), filter out
nulls, then pass the resulting non-null array or varargs to orderBy (e.g.,
collect to List<OrderSpecifier<?>> or use
Stream.of(...).filter(Objects::nonNull).toArray(OrderSpecifier[]::new)) so no
nulls are provided to orderBy.
- Around line 39-41: The QPostImg static import
(com.backend.farmon.domain.QPostImg.postImg) conflicts with the instance field
named postImg; resolve by either removing the static import and using the
instance field (private final QPostImg postImg) everywhere, or rename the field
to a non-conflicting identifier (e.g., postImgEntity or qPostImg) and update all
usages of postImg in PostRepositoryImpl to the new name; ensure imports and
references to QPostImg/postImg are consistent after the change.

In @src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java:
- Around line 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.

In
@src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java:
- Around line 76-80: Extract the hardcoded board ID in
PostQueryServiceImpl::findPopularExpertColumnPosts into a named constant (e.g.,
private static final long EXPERT_COLUMN_BOARD_ID = 4L) and replace List.of(4L)
with List.of(EXPERT_COLUMN_BOARD_ID) so the magic number is documented and
changeable in one place; update references to popularPostsIdList accordingly.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0631dc8 and d6826a0.

📒 Files selected for processing (8)
  • README.md
  • src/main/java/com/backend/farmon/config/RedisConfig.java
  • src/main/java/com/backend/farmon/converter/HomeConverter.java
  • src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
  • src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.

  1. 예외 처리

    • @ControllerAdvice / @ExceptionHandler 사용 여부
    • RuntimeException 남용 여부
    • 로그에 민감 정보 노출 여부
  2. 코드 품질 & 구조

    • 단일 책임 원칙(SRP) 위반
    • 중복 코드
    • DTO ↔ Entity 변환 책임 위치
    • Service 계층 비대화 여부
  3. Spring Web & REST API

    • HTTP 상태 코드 적절성
    • 컨트롤러 비즈니스 로직 포함 여부
  4. JPA & Query 성능

    • EAGER 로딩 남용
    • N+1 문제 가능성
    • fetch join / batch size 개선 포인트
  5. 트랜잭션

    • @transactional 위치
    • readOnly 적용 여부
    • 트랜잭션 범위 적절성
  6. 보안

    • 민감 정보 로그 출력 여부
    • @Valid / Bean Validation 사용 여부
    • 인증/인가 우회 가능성

Files:

  • src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java
  • src/main/java/com/backend/farmon/converter/HomeConverter.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
  • src/main/java/com/backend/farmon/config/RedisConfig.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
  • src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java
🧬 Code graph analysis (2)
src/main/java/com/backend/farmon/converter/HomeConverter.java (1)
src/main/java/com/backend/farmon/dto/home/HomeResponse.java (1)
  • HomeResponse (8-149)
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (1)
src/main/java/com/backend/farmon/converter/HomeConverter.java (1)
  • HomeConverter (14-81)
🔇 Additional comments (7)
src/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.java (3)

35-50: 트랜잭션 커밋 이후 캐시 무효화 패턴 적절함

TransactionSynchronization.afterCommit()을 사용하여 트랜잭션 커밋 이후에만 캐시를 무효화하는 패턴이 적절합니다. 동기화가 활성화되지 않은 경우 즉시 실행으로 폴백하는 처리도 안전합니다.


52-84: 캐시 무효화 범위 확장 적절함

POPULAR 캐시와 해당 PostType 캐시를 함께 무효화하고, EXPERT_COLUMN의 경우 별도 캐시(home:popularExpertColumn)도 무효화하는 구조가 적절합니다. PR 목표에 부합하는 캐시 정합성 보장 로직입니다.


187-191: 읽기 전용 트랜잭션 적절히 적용됨

조회 메서드에 @Transactional(readOnly = true)가 적절히 적용되어 있습니다.

README.md (1)

116-120: 성능 개선 보고서가 잘 정리되어 있습니다.

v1 → v4 단계별 최적화 과정과 결과가 명확하게 문서화되었습니다. 향후 스케일아웃 계획도 적절히 언급되어 있습니다.

src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java (1)

23-24: DTO 프로젝션 기반 메서드 시그니처 변경이 적절합니다.

List<Post> 대신 List<PopularExpertPostRow>를 반환하도록 변경하여 LAZY 로딩 문제와 직렬화 이슈를 방지합니다. 메서드명에 Rows를 포함하여 반환 타입을 명확히 한 점도 좋습니다.

src/main/java/com/backend/farmon/converter/HomeConverter.java (1)

31-50: DTO 프로젝션 기반 변환 로직이 잘 분리되었습니다.

Entity 기반 변환과 Row(DTO projection) 기반 변환을 명확히 분리하여 성능 민감 영역에서 Entity 접근을 제거한 점이 좋습니다.

다만, 메서드명 toPopularPostDetailDTOF의 마지막 F가 의도된 것인지 확인해 주세요. toPopularPostDetailDTO가 더 일관성 있어 보입니다.

src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java (1)

148-211: 단일 쿼리 DTO 프로젝션으로 N+1 문제가 해결되었습니다.

필요한 필드(작성자명, 전문가 프로필 이미지, 첫 번째 게시글 이미지)를 한 번의 쿼리로 조회하는 구조가 잘 구현되었습니다. 서브쿼리를 활용한 첫 번째 이미지 조회 로직도 적절합니다.

Comment on lines 86 to 99
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();
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.

Comment on lines 163 to 172
// 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 순서에 따른 정렬 로직 추가

Comment on lines 163 to 166
LikeCount like = likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId());
if (like == null) {
throw new IllegalAccessException("좋아요를 누른 적이 없습니다!");
throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED); // 필요 시 별도 에러 코드 권장
}
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.

RedisCacheManager 기본 TTL을 명확히 설정하고, 특정 캐시(home:popularExpertColumn)는 별도 TTL을 적용함.
DTO(Row) 직렬화/역직렬화 안정성을 위해 기본 생성자를 추가하고,
QueryDSL 조회 로직 및 서비스 조회 흐름을 정리함.
QueryDSL orderBy()에 null OrderSpecifier가 전달되지 않도록
pinned(고정 노출) ID 존재 여부에 따라 정렬 로직을 명시적으로 분기함.

- pinned ID가 있는 경우:
  - CASE 기반 pinned 우선 정렬
  - FIELD 함수로 pinned 내부 순서 유지
  - 좋아요 수 및 작성일 기준 보조 정렬 적용
- pinned ID가 없는 경우:
  - 좋아요 수 및 작성일 기준 정렬만 적용

이를 통해 조건부 정렬 로직의 가독성을 개선하고,
QueryDSL orderBy 호출 시 발생 가능한 NullPointerException 위험을 제거함.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (2)

140-141: NPE 위험: crops가 null일 때 toString() 호출 시 예외 발생

Line 137-139에서 crops == null 체크를 하지만, Line 141에서는 체크 없이 crops.toString()을 호출합니다.

🐛 수정 제안
-        log.info(crops.toString());
+        log.info("crops: {}", crops);

104-105: 중복 @Transactional(readOnly = true) 어노테이션

클래스 레벨(Line 36)에 이미 @Transactional(readOnly = true)가 선언되어 있으므로, 동일한 속성의 메서드 레벨 어노테이션(Lines 104, 126, 147, 171, 193, 263)은 중복입니다. 제거해도 동작에 영향 없습니다.

src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java (1)

236-241: fetchCount() 메서드 deprecation 경고

QueryDSL 5.x에서 fetchCount()는 deprecated되었습니다. 이 PR의 변경 범위는 아니지만, 향후 리팩토링 시 select(post.count()).from(post).where(...).fetchOne() 패턴으로 대체를 고려하세요.

🤖 Fix all issues with AI agents
In @src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java:
- Around line 7-17: The DTO PopularExpertPostRow should be made immutable: mark
all fields (postId, title, content, writer, profileImageUrl,
firstImageStoredFileName) as final and replace the Lombok constructor
annotations with either (a) an explicit all-args constructor plus a no-args
constructor for Jackson, or (b) use Lombok's @Value combined with @Jacksonized
to get an immutable, Jackson-friendly type; update/remove
@AllArgsConstructor/@NoArgsConstructor and keep the class-level annotations
consistent with your chosen approach (or keep the current combination if you
prefer mutability).

In
@src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java:
- Around line 147-165: In findTop6ExpertColumnRowsByPopularIds, remove the
assert and only construct pinnedInnerOrderExpr when hasPinned is true to avoid
unnecessary object creation and null access; use the existing hasPinned boolean
to guard creation (e.g., build pinnedInnerOrderExpr inside the same
if(hasPinned) block where it’s used) and ensure
Expressions.constant(popularPostsIdList) is only called when popularPostsIdList
is non-null; keep pinnedFirstOrderExpr logic but rely on hasPinned to decide
whether to include the pinnedInnerOrderExpr in the query.

In
@src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java:
- Around line 71-87: The code passes a board ID via
List.of(EXPERT_COLUMN_BOARD_ID) into
postRepository.findTop6ExpertColumnRowsByPopularIds but the parameter
popularPostsIdList and method name suggest a list of post IDs; update for
clarity by either renaming the local variable to something like
popularBoardsIdList (or pinnedBoardIds) and/or add a concise comment above the
call stating that this list contains board IDs used for pinned sorting, and if
feasible rename the repository parameter and method signature
(findTop6ExpertColumnRowsByPopularIds -> findTop6ExpertColumnRowsByBoardIds or
similar) to reflect board-id semantics.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6826a0 and c37359a.

📒 Files selected for processing (6)
  • src/main/java/com/backend/farmon/config/RedisConfig.java
  • src/main/java/com/backend/farmon/dto/home/HomePostRow.java
  • src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.

  1. 예외 처리

    • @ControllerAdvice / @ExceptionHandler 사용 여부
    • RuntimeException 남용 여부
    • 로그에 민감 정보 노출 여부
  2. 코드 품질 & 구조

    • 단일 책임 원칙(SRP) 위반
    • 중복 코드
    • DTO ↔ Entity 변환 책임 위치
    • Service 계층 비대화 여부
  3. Spring Web & REST API

    • HTTP 상태 코드 적절성
    • 컨트롤러 비즈니스 로직 포함 여부
  4. JPA & Query 성능

    • EAGER 로딩 남용
    • N+1 문제 가능성
    • fetch join / batch size 개선 포인트
  5. 트랜잭션

    • @transactional 위치
    • readOnly 적용 여부
    • 트랜잭션 범위 적절성
  6. 보안

    • 민감 정보 로그 출력 여부
    • @Valid / Bean Validation 사용 여부
    • 인증/인가 우회 가능성

Files:

  • src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java
  • src/main/java/com/backend/farmon/config/RedisConfig.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
  • src/main/java/com/backend/farmon/dto/home/HomePostRow.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
🧬 Code graph analysis (1)
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (1)
src/main/java/com/backend/farmon/converter/HomeConverter.java (1)
  • HomeConverter (14-81)
🔇 Additional comments (4)
src/main/java/com/backend/farmon/dto/home/HomePostRow.java (1)

5-13: LGTM!

record 구조가 불변 DTO에 적합하며, 필드 구성이 적절합니다.

src/main/java/com/backend/farmon/config/RedisConfig.java (1)

85-99: 캐시 구성이 적절합니다.

기본 TTL(60초)과 특정 캐시(home:popularExpertColumn)에 대한 별도 TTL(5분) 설정이 잘 구성되어 있습니다. disableCachingNullValues()로 null 캐싱을 방지한 것도 좋습니다.

GenericJackson2JsonRedisSerializer는 역직렬화 시 @class 타입 정보를 JSON에 포함하므로, PopularExpertPostRow@NoArgsConstructor가 필요합니다. PR의 DTO에 이미 추가되어 있어 호환성이 확보되었습니다.

src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java (1)

23-24: 인터페이스 변경이 적절합니다.

엔티티(Post) 대신 DTO(PopularExpertPostRow)를 반환하도록 변경하여 LAZY 로딩 이슈를 방지하고, limit 파라미터를 추가하여 유연성을 확보했습니다.

src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java (1)

166-215: QueryDSL 프로젝션 구현이 잘 되어 있습니다.

단일 쿼리로 필요한 모든 필드(작성자명, 전문가 프로필 이미지, 첫 번째 게시글 이미지)를 조회하여 N+1 문제를 해결했습니다.

  • LEFT JOIN을 적절히 사용하여 선택적 관계(expert, postImg) 처리
  • 첫 번째 이미지만 가져오기 위한 서브쿼리(min(id)) 활용
  • groupBy에 모든 비집계 필드 포함으로 정확한 그룹화
  • hasPinned 분기로 null orderBy expression 방지

Comment on lines +7 to +17
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PopularExpertPostRow {
private Long postId;
private String title;
private String content;
private String writer;
private String profileImageUrl;
private String firstImageStoredFileName;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

캐시 대상 DTO로 적절한 구조입니다.

@NoArgsConstructor(Jackson 역직렬화용)와 @AllArgsConstructor(QueryDSL projection용) 조합이 적절합니다.

불변성을 보장하려면 필드에 final 키워드를 추가하고 @RequiredArgsConstructor 대신 명시적 생성자를 사용하거나, Lombok의 @Value@Jacksonized를 조합하는 방안도 고려해 보세요. 현재 구조도 동작에 문제는 없습니다.

🤖 Prompt for AI Agents
In @src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.java around
lines 7 - 17, The DTO PopularExpertPostRow should be made immutable: mark all
fields (postId, title, content, writer, profileImageUrl,
firstImageStoredFileName) as final and replace the Lombok constructor
annotations with either (a) an explicit all-args constructor plus a no-args
constructor for Jackson, or (b) use Lombok's @Value combined with @Jacksonized
to get an immutable, Jackson-friendly type; update/remove
@AllArgsConstructor/@NoArgsConstructor and keep the class-level annotations
consistent with your chosen approach (or keep the current combination if you
prefer mutability).

@mmije0ng mmije0ng changed the title ✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 및 DTO 조회 구조 개선 ✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 도입 및 DTO 조회 구조 개선 Jan 8, 2026
QueryDSL 인기 전문가 칼럼 조회 로직에서 assert 기반 보호 코드를 제거하고,
pinned(고정 노출) ID 존재 여부(hasPinned)에 따라 정렬 Expression을 조건부로 생성하도록 개선함.

- JVM 옵션(-da)에 따라 비활성화될 수 있는 assert 사용 제거
- pinnedFirstOrderExpr / pinnedInnerOrderExpr를 hasPinned=true인 경우에만 생성
- 불필요한 Expression 객체 생성 및 상수 바인딩 방지
- orderBy 분기 구조를 유지하면서 런타임 안정성과 가독성 개선

리뷰 지적 사항을 반영하여 QueryDSL 정렬 로직의 의도를 명확히 하고
비즈니스 로직 레벨에서의 안전성을 강화함.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
@src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java:
- Around line 191-196: The usage of MySQL-specific FIELD in pinnedInnerOrderExpr
(created via Expressions.numberTemplate in PostRepositoryImpl) is incompatible
with H2 tests; update the code near the Expressions.numberTemplate call to add
the explicit comment lines noting this is MySQL-only (e.g., "MySQL 전용: FIELD 함수로
pinned ID 순서 유지" and "H2 테스트 환경에서는 호환되지 않음 (현재 미테스트)") so the
dependency/limitation is clear, and if DB-independence is required replace the
FIELD-based ordering with a QueryDSL CaseBuilder-based ordering implementation
that reproduces the pinned ID ordering logic.

In
@src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java:
- Around line 45-46: The hardcoded magic number EXPERT_COLUMN_POST_ID = 4L
should be documented or externalized: add a descriptive comment explaining which
board ID this represents (e.g., "expert column board ID (boards table)"), and
better, move it to configuration by replacing the static constant with a
configurable field injected from application.yml (e.g., property
app.expertColumnPostId with a sensible default 4) using @Value or
@ConfigurationProperties in PostQueryServiceImpl so environments can override
it; update any usages of EXPERT_COLUMN_POST_ID to reference the new injected
field name.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c37359a and 225eaad.

📒 Files selected for processing (3)
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.

  1. 예외 처리

    • @ControllerAdvice / @ExceptionHandler 사용 여부
    • RuntimeException 남용 여부
    • 로그에 민감 정보 노출 여부
  2. 코드 품질 & 구조

    • 단일 책임 원칙(SRP) 위반
    • 중복 코드
    • DTO ↔ Entity 변환 책임 위치
    • Service 계층 비대화 여부
  3. Spring Web & REST API

    • HTTP 상태 코드 적절성
    • 컨트롤러 비즈니스 로직 포함 여부
  4. JPA & Query 성능

    • EAGER 로딩 남용
    • N+1 문제 가능성
    • fetch join / batch size 개선 포인트
  5. 트랜잭션

    • @transactional 위치
    • readOnly 적용 여부
    • 트랜잭션 범위 적절성
  6. 보안

    • 민감 정보 로그 출력 여부
    • @Valid / Bean Validation 사용 여부
    • 인증/인가 우회 가능성

Files:

  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java
  • src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
  • src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
🧬 Code graph analysis (1)
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (1)
src/main/java/com/backend/farmon/converter/HomeConverter.java (1)
  • HomeConverter (14-81)
🔇 Additional comments (4)
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java (1)

72-86: 캐시 적용 및 DTO 프로젝션 전환 LGTM

  • @Cacheable 어노테이션이 적절히 적용되었으며, unless = "#result == null" 조건으로 null 결과 캐싱을 방지합니다.
  • 캐시 키 'list:v1'에 버전을 포함한 것은 향후 스키마 변경 시 무효화 전략에 유용합니다.
  • PopularExpertPostRow DTO 프로젝션으로 전환하여 N+1 문제를 제거하고, 엔티티 직렬화 이슈를 해결한 점이 좋습니다.
  • log.isDebugEnabled() 가드를 사용하여 불필요한 문자열 연산을 방지했습니다.
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.java (1)

23-24: API 개선 LGTM

  • List<Post> 대신 List<PopularExpertPostRow> DTO를 반환하도록 변경하여 레이어 간 책임 분리가 명확해졌습니다.
  • limit 파라미터를 추가하여 메서드의 재사용성이 향상되었습니다.
  • 네이밍(findTopExpertColumnRowsByPopularIds)이 반환 타입과 용도를 잘 표현합니다.
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java (2)

147-181: DTO 프로젝션 및 조인 전략 LGTM

  • Projections.constructor를 사용하여 필요한 필드만 조회하는 것이 좋습니다.
  • user, expert 조인으로 작성자 정보를, 서브쿼리 min(pi2.id)로 첫 번째 이미지만 효율적으로 가져옵니다.
  • groupBy에 모든 select 필드를 포함하여 집계 쿼리 요구사항을 충족합니다.

184-209: 조건부 정렬 로직 LGTM

hasPinned 플래그를 사용하여 null 전달을 방지하고, pinned 게시글이 있을 때만 우선 정렬 표현식을 생성하는 방식이 깔끔합니다.

  • CaseBuilder로 pinned 게시글 우선 정렬 (1 vs 2)
  • pinned 내부에서는 입력 순서 유지
  • 그 외에는 좋아요 수 → 최신순 정렬

Comment on lines +191 to +196
var pinnedInnerOrderExpr = Expressions.numberTemplate(
Integer.class,
"FIELD({0}, {1})",
post.id,
Expressions.constant(popularPostsIdList)
);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for database configurations across the project
echo "=== Database Configuration Files ==="
fd -e properties -e yaml | head -20

echo -e "\n=== Searching for datasource and database configs ==="
rg "spring.datasource|spring.jpa|h2|mysql" -t yaml -t properties -A 2

echo -e "\n=== Checking pom.xml for database dependencies ==="
if [ -f "pom.xml" ]; then
  rg "<artifactId>(h2|mysql|postgresql|mariadb|oracle)" pom.xml
fi

echo -e "\n=== Checking build.gradle for database dependencies ==="
if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
  rg "(h2|mysql|postgresql|mariadb|oracle)" build.gradle* 2>/dev/null || true
fi

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Search more specifically for application configuration files in src directory
echo "=== Application configuration files in src ==="
find src -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" | sort

echo -e "\n=== Content of test application properties ==="
find src -path "*test*" -name "application*.properties" -o -path "*test*" -name "application*.yml" -o -path "*test*" -name "application*.yaml" | xargs cat 2>/dev/null || echo "No test configs found"

echo -e "\n=== Main application configurations ==="
find src/main -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" | xargs cat 2>/dev/null || echo "No main configs found"

echo -e "\n=== Checking if PostRepositoryImpl is tested ==="
fd -e java | xargs rg -l "PostRepositoryImpl|PostRepository" | grep -i test

echo -e "\n=== Check build.gradle for test profile ==="
cat build.gradle 2>/dev/null | grep -A 5 -B 5 "test\|h2" || echo "Could not read build.gradle"

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 2073


🏁 Script executed:

#!/bin/bash
# Look at the PostRepositoryImpl file to understand the context better
echo "=== PostRepositoryImpl.java content around line 191-196 ==="
if [ -f "src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java" ]; then
  cat -n "src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java" | sed -n '180,210p'
else
  echo "File not found, searching for it..."
  find . -name "PostRepositoryImpl.java" -type f
fi

echo -e "\n=== Check if there are test files for PostRepository ==="
find . -path "*test*" -name "*PostRepository*Test*.java" -o -path "*test*" -name "*Post*RepositoryTest.java"

echo -e "\n=== Look for TestApplication or test config ==="
find . -path "*test*" -name "TestApplication.java" -o -path "*test*" -name "Test*.java" | head -10

echo -e "\n=== Check for @DataJpaTest or repository test annotations ==="
rg "@DataJpaTest|@SpringBootTest|testcontainers" -t java | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 1640


🏁 Script executed:

#!/bin/bash
# Check if there's a default test configuration when no explicit config exists
echo "=== Check for application.yml or application.properties in test resources ==="
ls -la src/test/resources/ 2>/dev/null || echo "test/resources not found"

echo -e "\n=== Look for spring profiles in test ==="
rg "spring.profiles.active|@ActiveProfiles" -t java

echo -e "\n=== Check FarmonApplicationTests.java ==="
cat -n src/test/java/com/backend/farmon/FarmonApplicationTests.java

echo -e "\n=== Search for any test that calls PostRepository methods ==="
rg -r "getPostsWithPopularByCategory|popularPostsIdList" --type=java src/test/

echo -e "\n=== Check Spring Boot default test database behavior ==="
grep -i "test.database\|datasource.url" src/test/resources/*.properties src/test/resources/*.yml 2>/dev/null || echo "No test datasource config found"

echo -e "\n=== Overall test directory structure ==="
find src/test -type f -name "*.java" -o -name "*.properties" -o -name "*.yml" | head -20

Repository: mmije0ng/FarmON_BE_Refactor

Length of output: 815


MySQL FIELD 함수 사용 - H2 테스트 환경과 호환성 문제

프로젝트의 build.gradle에 H2 데이터베이스가 포함되어 있으며, 테스트 환경에서 명시적인 datasource 설정이 없으므로 @SpringBootTest 실행 시 기본적으로 H2 인메모리 데이터베이스가 사용됩니다. MySQL 전용 함수인 FIELD({0}, {1})은 H2에서 작동하지 않아, 향후 이 코드 경로를 테스트하거나 테스트 범위를 확대할 경우 실패하게 됩니다.

  • 즉시 해결: 줄 190의 주석처럼 MySQL 종속성을 명시적으로 표시하고, 추가로 구현 내용을 명확히 하세요.
    // MySQL 전용: FIELD 함수로 pinned ID 순서 유지
    // H2 테스트 환경에서는 호환되지 않음 (현재 미테스트)
  • 장기 해결: DB 독립성이 필요하다면 QueryDSL의 CaseBuilder를 확장하여 순서를 지정하는 방식으로 대체하세요.
🤖 Prompt for AI Agents
In
@src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
around lines 191 - 196, The usage of MySQL-specific FIELD in
pinnedInnerOrderExpr (created via Expressions.numberTemplate in
PostRepositoryImpl) is incompatible with H2 tests; update the code near the
Expressions.numberTemplate call to add the explicit comment lines noting this is
MySQL-only (e.g., "MySQL 전용: FIELD 함수로 pinned ID 순서 유지" and "H2 테스트 환경에서는 호환되지
않음 (현재 미테스트)") so the dependency/limitation is clear, and if DB-independence is
required replace the FIELD-based ordering with a QueryDSL CaseBuilder-based
ordering implementation that reproduces the pinned ID ordering logic.

Comment on lines +45 to +46
private static final Integer POPULAR_EXPERT_POST_LIMIT=6;
private static final long EXPERT_COLUMN_POST_ID = 4L;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

매직 넘버 4L에 대한 문서화 또는 설정 외부화 검토 필요

EXPERT_COLUMN_POST_ID = 4L은 특정 게시판 ID를 하드코딩한 것으로 보입니다. 이 값이 어떤 게시판을 의미하는지 주석으로 명시하거나, 환경별로 다를 수 있다면 application.yml 등 외부 설정으로 관리하는 것이 유지보수에 유리합니다.

// 예: 전문가 칼럼 게시판 ID (boards 테이블 참조)
private static final long EXPERT_COLUMN_POST_ID = 4L;
🤖 Prompt for AI Agents
In
@src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
around lines 45 - 46, The hardcoded magic number EXPERT_COLUMN_POST_ID = 4L
should be documented or externalized: add a descriptive comment explaining which
board ID this represents (e.g., "expert column board ID (boards table)"), and
better, move it to configuration by replacing the static constant with a
configurable field injected from application.yml (e.g., property
app.expertColumnPostId with a sensible default 4) using @Value or
@ConfigurationProperties in PostQueryServiceImpl so environments can override
it; update any usages of EXPERT_COLUMN_POST_ID to reference the new injected
field name.

@mmije0ng mmije0ng merged commit 006bbaa into develop Jan 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 개발 ♻️ refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

♻️ Refactor: k6 도입 후 부하테스트 진행 ✨ Feat: 홈 화면 인기 칼럼 조회 Redis Cache 도입

1 participant