✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 도입 및 DTO 조회 구조 개선#23
Conversation
홈 화면 인기 전문가 칼럼 조회 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을 길게 가져가더라도 좋아요 이벤트에 즉시 반영되는 구조 확보
📝 WalkthroughWalkthroughRedis 캐시 설정에 다중 TTL을 추가하고, 인기 전문가 칼럼 조회에 Changes
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>
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
README.mdsrc/main/java/com/backend/farmon/config/RedisConfig.javasrc/main/java/com/backend/farmon/converter/HomeConverter.javasrc/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.javasrc/main/java/com/backend/farmon/service/LikeService/LikeServiceImpl.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.
예외 처리
- @ControllerAdvice / @ExceptionHandler 사용 여부
- RuntimeException 남용 여부
- 로그에 민감 정보 노출 여부
코드 품질 & 구조
- 단일 책임 원칙(SRP) 위반
- 중복 코드
- DTO ↔ Entity 변환 책임 위치
- Service 계층 비대화 여부
Spring Web & REST API
- HTTP 상태 코드 적절성
- 컨트롤러 비즈니스 로직 포함 여부
JPA & Query 성능
- EAGER 로딩 남용
- N+1 문제 가능성
- fetch join / batch size 개선 포인트
트랜잭션
- @transactional 위치
- readOnly 적용 여부
- 트랜잭션 범위 적절성
보안
- 민감 정보 로그 출력 여부
- @Valid / Bean Validation 사용 여부
- 인증/인가 우회 가능성
Files:
src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.javasrc/main/java/com/backend/farmon/converter/HomeConverter.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.javasrc/main/java/com/backend/farmon/config/RedisConfig.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.javasrc/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 문제가 해결되었습니다.필요한 필드(작성자명, 전문가 프로필 이미지, 첫 번째 게시글 이미지)를 한 번의 쿼리로 조회하는 구조가 잘 구현되었습니다. 서브쿼리를 활용한 첫 번째 이미지 조회 로직도 적절합니다.
| 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(); |
There was a problem hiding this comment.
🧹 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.
| 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.
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
Show resolved
Hide resolved
| // 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; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 테스트 DB 및 database 관련 설정 확인
rg -n "h2|hsql|postgres|mysql|dialect|database" --type yaml --type properties -i | head -100Repository: 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 -20Repository: 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 -C3Repository: 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 -20Repository: 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 -30Repository: 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/nullRepository: 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 -20Repository: 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 -20Repository: mmije0ng/FarmON_BE_Refactor
Length of output: 53
MySQL 전용 FIELD() 함수 사용으로 인한 데이터베이스 호환성 문제
FIELD() 함수는 MySQL에서만 지원되는 함수입니다. 프로젝트 의존성에 H2 데이터베이스가 포함되어 있어, 테스트 환경이나 개발 환경에서 H2를 사용할 경우 이 쿼리는 실패합니다.
권장사항:
- 데이터베이스 독립적인 정렬 방식으로 변경 (Java 메모리 정렬 또는 QueryDSL의 CaseBuilder 활용)
- 또는 데이터베이스별 dialect에 따른 조건부 쿼리 구현
- 또는 애플리케이션 수준에서 popularPostsIdList 순서에 따른 정렬 로직 추가
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
Outdated
Show resolved
Hide resolved
| LikeCount like = likeCountRepository.findByUserIdAndPostId(userId, originalPost.getId()); | ||
| if (like == null) { | ||
| throw new IllegalAccessException("좋아요를 누른 적이 없습니다!"); | ||
| throw new GeneralException(ErrorStatus.LIKE_TYPE_NOT_SAVED); // 필요 시 별도 에러 코드 권장 | ||
| } |
There was a problem hiding this comment.
좋아요 미존재 시 부적절한 에러 코드 사용
좋아요가 존재하지 않는 경우에도 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.
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
Outdated
Show resolved
Hide resolved
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 위험을 제거함.
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
src/main/java/com/backend/farmon/config/RedisConfig.javasrc/main/java/com/backend/farmon/dto/home/HomePostRow.javasrc/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.
예외 처리
- @ControllerAdvice / @ExceptionHandler 사용 여부
- RuntimeException 남용 여부
- 로그에 민감 정보 노출 여부
코드 품질 & 구조
- 단일 책임 원칙(SRP) 위반
- 중복 코드
- DTO ↔ Entity 변환 책임 위치
- Service 계층 비대화 여부
Spring Web & REST API
- HTTP 상태 코드 적절성
- 컨트롤러 비즈니스 로직 포함 여부
JPA & Query 성능
- EAGER 로딩 남용
- N+1 문제 가능성
- fetch join / batch size 개선 포인트
트랜잭션
- @transactional 위치
- readOnly 적용 여부
- 트랜잭션 범위 적절성
보안
- 민감 정보 로그 출력 여부
- @Valid / Bean Validation 사용 여부
- 인증/인가 우회 가능성
Files:
src/main/java/com/backend/farmon/dto/home/PopularExpertPostRow.javasrc/main/java/com/backend/farmon/config/RedisConfig.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.javasrc/main/java/com/backend/farmon/dto/home/HomePostRow.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.javasrc/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 방지
| @Getter | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public class PopularExpertPostRow { | ||
| private Long postId; | ||
| private String title; | ||
| private String content; | ||
| private String writer; | ||
| private String profileImageUrl; | ||
| private String firstImageStoredFileName; | ||
| } |
There was a problem hiding this comment.
🧹 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).
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
Show resolved
Hide resolved
QueryDSL 인기 전문가 칼럼 조회 로직에서 assert 기반 보호 코드를 제거하고, pinned(고정 노출) ID 존재 여부(hasPinned)에 따라 정렬 Expression을 조건부로 생성하도록 개선함. - JVM 옵션(-da)에 따라 비활성화될 수 있는 assert 사용 제거 - pinnedFirstOrderExpr / pinnedInnerOrderExpr를 hasPinned=true인 경우에만 생성 - 불필요한 Expression 객체 생성 및 상수 바인딩 방지 - orderBy 분기 구조를 유지하면서 런타임 안정성과 가독성 개선 리뷰 지적 사항을 반영하여 QueryDSL 정렬 로직의 의도를 명확히 하고 비즈니스 로직 레벨에서의 안전성을 강화함.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.javasrc/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: 아래 기준에 따라 Java 코드를 줄 단위로 상세히 리뷰해 주세요.
문제가 있는 코드 라인에는 직접 코멘트를 남겨 주세요.
예외 처리
- @ControllerAdvice / @ExceptionHandler 사용 여부
- RuntimeException 남용 여부
- 로그에 민감 정보 노출 여부
코드 품질 & 구조
- 단일 책임 원칙(SRP) 위반
- 중복 코드
- DTO ↔ Entity 변환 책임 위치
- Service 계층 비대화 여부
Spring Web & REST API
- HTTP 상태 코드 적절성
- 컨트롤러 비즈니스 로직 포함 여부
JPA & Query 성능
- EAGER 로딩 남용
- N+1 문제 가능성
- fetch join / batch size 개선 포인트
트랜잭션
- @transactional 위치
- readOnly 적용 여부
- 트랜잭션 범위 적절성
보안
- 민감 정보 로그 출력 여부
- @Valid / Bean Validation 사용 여부
- 인증/인가 우회 가능성
Files:
src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryCustom.javasrc/main/java/com/backend/farmon/service/PostService/PostQueryServiceImpl.javasrc/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'에 버전을 포함한 것은 향후 스키마 변경 시 무효화 전략에 유용합니다.PopularExpertPostRowDTO 프로젝션으로 전환하여 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 내부에서는 입력 순서 유지
- 그 외에는 좋아요 수 → 최신순 정렬
| var pinnedInnerOrderExpr = Expressions.numberTemplate( | ||
| Integer.class, | ||
| "FIELD({0}, {1})", | ||
| post.id, | ||
| Expressions.constant(popularPostsIdList) | ||
| ); |
There was a problem hiding this comment.
🧩 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
fiRepository: 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 -20Repository: 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 -20Repository: 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.
| private static final Integer POPULAR_EXPERT_POST_LIMIT=6; | ||
| private static final long EXPERT_COLUMN_POST_ID = 4L; |
There was a problem hiding this comment.
🧹 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.
#️⃣연관된 이슈
📝작업 내용
홈 화면 인기 전문가 칼럼 조회 API의 성능 및 캐시 안정성을 개선함.
home:popularExpertColumn, key=list:v1)로 저장하도록 추가함조회 구조 개선(불필요 쿼리 방지):
List<Post>조회 후 Converter에서 연관 엔티티(user,expert,postImgs)에 접근하는 구조였음Converter 책임 분리:
기대 효과:
스크린샷 (선택)
캐시된 데이터가 없을 경우 쿼리
인기 전문가 칼럼 조회 Redis 캐시 및 DTO 조회 구조 개선
핵심 요약
성능
구조
안정성
변경 파일(요약)
기대 효과