[Feat] 가장 많이 검색된 책 조회 api 개발 및 책 상세 검색 시 검색카운트 업데이트 로직 추가#49
Conversation
@ EnableAsync 추가 (#48)
Walkthrough책 검색 순위 기능이 추가되었습니다. 책 상세 조회 시 ISBN과 검색 일자를 Redis에 저장하고, 매일 자정 스케줄러가 Redis의 책 검색 순위를 집계하여 전날 기준 Top 5를 저장합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Controller as BookQueryController
participant Service as BookMostSearchService
participant Redis as BookRedisQueryPort
participant UserPort as UserCommandPort
User->>Controller: GET /books/most-searched (userId)
Controller->>Service: getMostSearchedBooks(userId)
Service->>UserPort: findById(userId)
Service->>Redis: getYesterdayBookRankInfos(yesterday)
Redis-->>Service: List<BookRankInfo>
Service-->>Controller: BookMostSearchResult
Controller-->>User: BaseResponse<GetBookMostSearchResponse>
sequenceDiagram
participant Scheduler as BookMostSearchRankService (스케줄러)
participant RedisCmd as BookRedisCommandPort
participant RedisQry as BookRedisQueryPort
participant BookRepo as BookCommandPort
participant BookApi as BookApiQueryPort
Scheduler->>RedisQry: getBookSearchCountTopN(yesterday, 5)
RedisQry-->>Scheduler: List<ISBN, count>
Scheduler->>RedisCmd: deleteBookSearchRank(yesterday)
Scheduler->>RedisCmd: deleteBookSearchCount(yesterday)
alt Top 5 존재
loop 각 ISBN
Scheduler->>BookRepo: findByIsbn(isbn)
alt DB에 없음
Scheduler->>BookApi: findDetailBookByIsbn(isbn)
end
end
Scheduler->>RedisCmd: saveBookSearchRank(ISBNs, counts, yesterday)
Scheduler->>RedisCmd: saveBookSearchRankDetail(BookRankInfos, yesterday)
end
Assessment against linked issues
Suggested reviewers
Poem
✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (14)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java (1)
6-12: Spring Data JPA의 In 쿼리 메서드 사용 적절함Spring Data JPA의
findBy{FieldName}In패턴을 올바르게 사용하였습니다. 배치 조회를 위한 효율적인 접근 방법입니다.성능 최적화 고려사항
대량의 ISBN 리스트를 처리할 때 성능 문제가 발생할 수 있습니다. 필요시 페이징이나 배치 크기 제한을 고려해보세요.
src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java (1)
8-12: save 메서드 추가 적절함CommandPort에 save 메서드를 추가하는 것은 CQRS 패턴과 일치합니다. Long 반환 타입도 새로 생성된 엔티티의 ID를 반환하는 일반적인 패턴을 따릅니다.
예외 처리 고려사항
save 작업 시 발생할 수 있는 예외상황(중복 ISBN, 유효하지 않은 Book 데이터 등)에 대한 처리 방안을 구현체에서 고려해야 합니다.
src/main/java/konkuk/thip/ThipServerApplication.java (1)
6-12: 스케줄링 및 비동기 처리 활성화 적절함책 검색 랭킹 시스템의 일일 집계 작업을 위해
@EnableScheduling과@EnableAsync어노테이션을 추가하는 것은 적절합니다.스케줄링 관련 모범 사례 제안
스케줄링 작업의 안정성을 위해 다음 사항들을 고려해보세요:
- 스케줄러 예외 처리 및 로깅
- 중복 실행 방지를 위한 락 메커니즘
- 스케줄러 설정의 외부 구성 파일화
src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java (1)
9-9: 정적 팩토리 메서드의 매개변수 간격을 개선하세요.매개변수 사이에 공백이 없어 가독성이 떨어집니다.
- public static BookIsSavedResult of( String isbn,boolean isSaved) { + public static BookIsSavedResult of(String isbn, boolean isSaved) {src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java (1)
6-9: 필드명을 더 명확하게 변경하세요.
type이라는 필드명은 저장/삭제 작업을 나타내기에 명확하지 않습니다. 더 의미있는 이름을 사용하세요.public record PostBookIsSavedRequest( @NotNull(message = "type은 필수입니다.") - boolean type + boolean isSave ) {또는 더 명확한 이름으로:
public record PostBookIsSavedRequest( - @NotNull(message = "type은 필수입니다.") - boolean type + @NotNull(message = "저장 여부는 필수입니다.") + boolean shouldSave ) {src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java (1)
3-3: 사용되지 않는 import 제거 필요BookDetailSearchResult import가 사용되지 않고 있습니다.
-import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult;src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (1)
20-26: 배치 쿼리 구현이 효율적입니다.ISBN 목록으로 여러 책을 한 번에 조회하는 배치 쿼리 방식으로 성능을 최적화했습니다. 스트림을 통한 엔티티 변환도 적절합니다.
Java 16 이상을 사용하는 경우 더 간결한 구문으로 개선할 수 있습니다:
- .collect(Collectors.toList()); + .toList();src/main/java/konkuk/thip/config/RedisConfig.java (2)
16-20: 환경 변수 검증 및 기본값 설정 권장Redis 연결 설정에서 환경 변수 누락 시 명확한 오류 메시지를 제공하는 것이 좋습니다.
다음과 같이 개선하는 것을 권장합니다:
- @Value("${spring.data.redis.host}") + @Value("${spring.data.redis.host:localhost}") private String host; - @Value("${spring.data.redis.port}") + @Value("${spring.data.redis.port:6379}") private int port;또는
@ConfigurationProperties를 사용하여 더 체계적으로 관리:@ConfigurationProperties(prefix = "spring.data.redis") @Data public class RedisProperties { private String host = "localhost"; private int port = 6379; private String password; private int database = 0; }
23-30: 프로덕션 환경을 위한 Redis 설정 개선 제안현재 설정은 기본적인 기능에는 충분하지만, 프로덕션 환경에서는 추가 설정이 필요할 수 있습니다.
다음 개선사항들을 고려해 보세요:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); + + // Hash key, value 시리얼라이저 설정 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + // 일반적인 key:value의 경우 시리얼라이저 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + // 트랜잭션 지원 활성화 + redisTemplate.setEnableTransactionSupport(true); + return redisTemplate; }또한 연결 풀 설정도 고려해 보세요:
@Bean public RedisConnectionFactory redisConnectionFactory() { LettuceConnectionFactory factory = new LettuceConnectionFactory(host, port); // 연결 풀 설정 등 추가 가능 return factory; }src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java (1)
11-17: Record에서 @builder 어노테이션 제거 권장Java Record는 이미 모든 필드를 위한 생성자를 제공하므로
@Builder어노테이션이 불필요합니다.다음과 같이 개선하는 것을 권장합니다:
- @Builder public record BookRankInfo( int rank, String title, String imageUrl, String isbn ) {}Record의 기본 생성자를 사용하면 더 간단하고 명확합니다:
// 사용 예시 BookRankInfo bookRankInfo = new BookRankInfo(1, "책 제목", "이미지 URL", "ISBN");만약 빌더 패턴이 정말 필요하다면, 별도의 빌더 클래스를 생성하거나 정적 팩토리 메서드를 추가하는 것이 더 적절합니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (1)
36-42: 배치 처리 성능 개선 제안현재 구현은 각 ISBN-score 쌍을 개별적으로 Redis에 추가하고 있습니다. 대량의 데이터를 처리할 때 성능상 이슈가 있을 수 있습니다.
다음과 같이 한 번에 모든 데이터를 추가하도록 개선할 수 있습니다:
@Override public void saveBookSearchRank(List<String> isbns, List<Double> scores, LocalDate date) { String redisKey = makeRedisKey(searchRankPrefix, date); - for (int i = 0; i < isbns.size(); i++) { - redisTemplate.opsForZSet().add(redisKey, isbns.get(i), scores.get(i)); - } + Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>(); + for (int i = 0; i < isbns.size(); i++) { + tuples.add(ZSetOperations.TypedTuple.of(isbns.get(i), scores.get(i))); + } + redisTemplate.opsForZSet().add(redisKey, tuples); redisTemplate.expire(redisKey, Duration.ofDays(7)); }이 방법은 Redis에 대한 네트워크 호출 횟수를 줄여 성능을 향상시킵니다.
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java (1)
42-46: 임시 구현에 대한 TODO 추가를 고려하세요"삭제 전략 도입 전"이라는 주석은 이 구현이 임시적임을 나타냅니다. 향후 변경 계획이 있다면 TODO 주석으로 명확히 표시하는 것이 좋습니다.
또한
deleteBook메서드는 삭제할 엔티티의 존재 여부를 확인하지 않습니다. 존재하지 않는 엔티티를 삭제하려고 할 때의 동작을 명확히 하거나, 삭제 전 존재 여부를 확인하는 로직을 추가하는 것을 고려해보세요.src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (1)
23-24: DateTimeFormatter를 static final로 선언하세요
DateTimeFormatter는 thread-safe하고 불변 객체이므로 static final로 선언하는 것이 성능상 유리합니다.- private final DateTimeFormatter DAILY_KEY_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter DAILY_KEY_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd");src/main/java/konkuk/thip/book/application/service/BookSavedService.java (1)
40-49: 외부 API 호출이 트랜잭션 내에서 실행됩니다네이버 API 호출이 트랜잭션 경계 내에서 실행되고 있습니다. 외부 API의 응답 시간이 길어질 경우 DB 커넥션을 오래 점유할 수 있으므로, 필요시 트랜잭션 경계를 조정하는 것을 고려해보세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (36)
src/main/java/konkuk/thip/ThipServerApplication.java(1 hunks)src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java(1 hunks)src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java(3 hunks)src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java(1 hunks)src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java(1 hunks)src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java(0 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookSavedService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookSearchService.java(3 hunks)src/main/java/konkuk/thip/book/domain/Book.java(2 hunks)src/main/java/konkuk/thip/book/domain/SavedBooks.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/config/RedisConfig.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java(1 hunks)src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java(1 hunks)src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java
🧰 Additional context used
🧠 Learnings (18)
src/main/java/konkuk/thip/ThipServerApplication.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (2)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java (2)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/config/RedisConfig.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
🧬 Code Graph Analysis (2)
src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java (1)
src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1)
RequiredArgsConstructor(18-123)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java (1)
src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1)
RequiredArgsConstructor(18-123)
🔇 Additional comments (46)
src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java (1)
4-7: 메서드 시그니처 적절함Command 패턴에 맞게 상태 변경 작업을 수행하는 메서드들로 적절하게 설계되었습니다. 메서드명도 명확하고 직관적입니다.
src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java (1)
5-7: 인터페이스 설계가 잘 되어 있습니다.메서드 시그니처가 명확하고 매개변수 이름이 적절하며, 반환 타입도 올바른 계층의 DTO를 사용하고 있습니다.
src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java (1)
5-7: 인터페이스 설계는 좋으나 의존성 문제가 있습니다.인터페이스 자체의 설계는 명확하고 적절하지만, 반환 타입인
BookMostSearchResult가 웹 계층에 의존하는 문제가 있습니다. 해당 DTO의 아키텍처 위반을 먼저 수정하세요.src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java (2)
3-3: 적절한 도메인 객체 importSavedBooks 도메인 클래스를 올바르게 import하였습니다.
7-7: CQRS 패턴을 따르는 QueryPort 메서드 추가사용자 ID로 저장된 책 목록을 조회하는 메서드가 올바르게 추가되었습니다. 학습된 컨벤션에 따라 QueryPort에 도메인 엔티티를 반환하는 메서드를 추가한 것이 적절합니다.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (2)
10-10: Redis 기반 검색 횟수 추적을 위한 의존성 추가BookRedisCommandPort 의존성이 올바르게 추가되어 검색 랭킹 기능을 지원합니다.
92-94: 검색 횟수 증가 로직 추가책 상세 정보 조회 시 Redis에 검색 횟수를 증가시키는 로직이 적절한 위치에 배치되었습니다. 외부 API 호출 후, 데이터베이스 존재 여부 확인 전에 실행되는 것이 논리적으로 올바릅니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (2)
3-3: JPA 엔티티 import 추가BookJpaEntity import가 올바르게 추가되었습니다.
25-29: 책 저장 메서드 구현도메인 엔티티를 JPA 엔티티로 변환하여 저장하고 생성된 ID를 반환하는 로직이 올바르게 구현되었습니다. 매퍼를 통한 변환과 저장된 엔티티의 ID 반환이 적절합니다.
src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java (1)
7-15: 적절한 응답 DTO 구현Record 클래스를 사용한 불변 DTO 구현과 정적 팩토리 메서드가 올바르게 작성되었습니다. @builder 어노테이션으로 빌더 패턴도 제공하고 있어 좋습니다.
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java (2)
6-6: List import 추가List 타입 사용을 위한 import가 올바르게 추가되었습니다.
10-11: 저장된 책 관리를 위한 JPA 메서드 추가사용자 ID와 책 ID를 이용한 삭제 메서드와 사용자 ID로 저장된 책 목록을 조회하는 메서드가 Spring Data JPA 컨벤션에 맞게 올바르게 추가되었습니다.
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (2)
5-5: 새로운 의존성과 응답 타입 추가가 적절합니다.가장 많이 검색된 책 조회 기능을 위한 새로운 의존성과 응답 타입이 올바르게 추가되었습니다.
Also applies to: 8-8, 21-21
43-48: 새로운 엔드포인트 구현이 올바릅니다.가장 많이 검색된 책을 조회하는 엔드포인트가 기존 패턴과 일관되게 구현되었습니다. 사용자 ID를 받아서 처리하는 구조도 적절합니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (1)
13-18: 어댑터 클래스 구조가 올바릅니다.CQRS 패턴에 맞는 Query 전용 어댑터로 적절하게 구현되었습니다. 의존성 주입과 어노테이션 사용도 올바릅니다.
src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java (1)
7-10: Redis 쿼리 포트 인터페이스 설계가 적절합니다.Redis Sorted Set 데이터 구조에 적합한 반환 타입과 명확한 메서드 명명으로 잘 설계되었습니다. 날짜 기반 조회와 상위 N개 제한으로 성능도 고려되었습니다.
src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java (3)
13-13: 유효성 검사 어노테이션 추가가 적절합니다.
@Validated어노테이션 추가로 컨트롤러 레벨에서 유효성 검사가 활성화되어 데이터 무결성과 보안이 강화되었습니다.
18-18: 새로운 UseCase 의존성 추가가 올바릅니다.책 저장 상태 변경 기능을 위한
BookSavedUseCase의존성이 적절하게 추가되었습니다.
20-27: 새로운 엔드포인트 구현이 올바릅니다.책 저장 상태 변경을 위한 POST 엔드포인트가 RESTful API 설계 원칙에 맞게 구현되었습니다. ISBN 패턴 검증과 요청 본문 검증도 적절하게 적용되었습니다.
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java (1)
27-29: 새로운 의존성 추가가 적절합니다.사용자 검증과 책 엔티티 변환을 위한
UserJpaRepository와BookMapper의존성이 올바르게 추가되었습니다.src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
52-54: 오류 코드 정의가 적절합니다새로 추가된 세 개의 오류 코드들이 책 저장 기능에 적합하며, 기존 패턴과 일관성을 유지하고 있습니다:
- HTTP 상태 코드 선택이 적절함
- 오류 메시지가 명확하고 사용자 친화적임
- 코드 번호가 순차적으로 할당됨
src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java (1)
6-11: 포트 인터페이스 설계가 적절합니다Redis 명령 작업을 위한 인터페이스가 잘 설계되었습니다:
- 메서드명이 명확하고 목적을 잘 나타냄
- CQRS 패턴의 CommandPort 컨벤션을 따름
- 파라미터 타입이 적절함
다만 추후 구현체에서 다음 사항들을 고려해 보세요:
saveBookSearchRank메서드에서 isbns와 scores 리스트의 길이 검증- null 체크 및 빈 리스트 처리
- 예외 상황에 대한 명확한 문서화
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java (1)
19-21: 정적 팩토리 메서드 구현이 적절합니다
BookMostSearchResult에서GetBookMostSearchResponse로의 변환을 위한 정적 팩토리 메서드가 잘 구현되어 있습니다. 도메인 객체와 웹 응답 객체 간의 명확한 분리를 제공합니다.src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java (4)
21-23: 테스트 클래스 설정이 적절합니다.SpringBootTest와 test 프로필을 사용하여 통합 테스트 환경을 올바르게 구성했습니다.
25-40: 의존성 주입이 적절하게 구성되어 있습니다.Redis 관련 포트들과 RedisTemplate, 그리고 설정값들이 올바르게 주입되어 있습니다.
42-53: 테스트 데이터 설정이 잘 구성되어 있습니다.어제 날짜 기준으로 3개의 테스트 ISBN 데이터를 서로 다른 점수로 설정하여 랭킹 테스트에 적합한 환경을 만들었습니다.
55-88: 종합적인 랭킹 집계 로직 검증이 잘 구현되어 있습니다.테스트가 다음을 모두 검증하고 있어 매우 포괄적입니다:
- Top 5 조회 기능
- 기존 키 삭제 기능
- 새로운 랭킹 저장 기능
- 저장된 데이터의 정확성 (비어있지 않음, 최대 5개, 내림차순 정렬, 테스트 데이터 포함)
src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java (5)
21-27: 서비스 클래스 구성이 적절합니다.필요한 포트들이 모두 주입되어 있고, 의존성 구조가 깔끔합니다.
29-40: 빈 결과 처리에 대한 PM 상의 필요성을 잘 표현했습니다.랭킹 데이터가 없을 때의 처리 방식을 PM과 상의 후 결정한다는 주석이 비즈니스 요구사항 불확실성을 잘 나타냅니다.
42-51: DB 조회 및 매핑 로직이 효율적입니다.한 번의 배치 조회로 모든 ISBN을 조회하고 Map으로 변환하여 효율적인 접근이 가능합니다.
53-75: 랭킹 순서 유지 및 폴백 처리가 잘 구현되어 있습니다.Redis의 랭킹 순서를 유지하면서 각 책에 대해 DB에서 찾지 못할 경우 Naver API로 폴백하는 로직이 적절합니다.
77-79: 결과 반환 로직이 적절합니다.생성된 랭킹 정보 리스트를 적절한 도메인 객체로 래핑하여 반환합니다.
src/main/java/konkuk/thip/book/domain/Book.java (5)
7-7: 필요한 import가 적절히 추가되었습니다.Objects 클래스를 사용하여 null-safe한 equals/hashCode 구현을 위해 필요한 import입니다.
31-37: ISBN 기반 equals 구현이 적절합니다.도메인 특성상 ISBN을 기준으로 한 동등성 비교가 타당하며, null 체크와 타입 체크가 포함된 안전한 구현입니다.
39-42: hashCode 구현이 equals와 일관성을 유지합니다.equals에서 사용하는 필드(ISBN)와 동일한 필드로 hashCode를 구현하여 equals/hashCode 계약을 준수합니다.
44-56: 정적 팩토리 메서드가 잘 구현되어 있습니다.ID 없이 Book 객체를 생성하는 명확한 의도가 드러나며, 모든 필드를 매개변수로 받아 완전한 객체 생성이 가능합니다.
58-70: 불변 객체 패턴을 잘 구현했습니다.기존 객체의 모든 필드를 복사하면서 ID만 변경하는 방식으로 불변 객체의 특성을 유지합니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (5)
14-27: Redis 어댑터 구성이 적절합니다.필요한 의존성들이 올바르게 주입되어 있고, 날짜 포맷터와 설정값들이 잘 구성되어 있습니다.
29-33: 검색 카운트 증가 로직이 효율적입니다.Redis Sorted Set의 incrementScore를 사용하여 원자적으로 검색 카운트를 증가시키는 방식이 적절합니다.
35-42: 랭킹 저장 로직이 잘 구현되어 있습니다.배치로 데이터를 저장하고 7일 TTL을 설정하여 자동 정리되도록 하는 방식이 효율적입니다.
44-52: 삭제 메서드들이 적절히 구현되어 있습니다.공통 헬퍼 메서드를 활용하여 중복 코드를 제거하고 일관성을 유지합니다.
54-62: 헬퍼 메서드들이 잘 구현되어 있습니다.키 생성과 삭제 로직이 재사용 가능하도록 잘 분리되어 있습니다.
src/main/java/konkuk/thip/book/domain/SavedBooks.java (4)
10-12: 불변 컬렉션 사용이 적절합니다.Set을 사용하여 중복을 방지하고 불변성을 보장하는 설계가 좋습니다.
14-22: 중복 검증 로직이 효과적입니다.List를 Set으로 변환하여 크기 비교를 통해 중복을 감지하는 방식이 간단하고 효과적입니다. Collections.unmodifiableSet을 사용하여 불변성도 보장합니다.
24-29: 비즈니스 규칙 검증이 명확합니다.이미 저장된 책을 중복 저장하는 것을 방지하는 비즈니스 규칙이 명확하게 구현되어 있습니다.
31-36: 삭제 가능성 검증이 적절합니다.저장되지 않은 책을 삭제하려고 할 때 적절한 예외를 발생시키는 비즈니스 규칙이 잘 구현되어 있습니다.
| @Override | ||
| public SavedBooks findByUserId(Long userId) { | ||
|
|
||
| UserJpaEntity user = userJpaRepository.findById(userId) | ||
| .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
| List<SavedBookJpaEntity> savedBookEntities = savedBookJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); | ||
|
|
||
| // SavedBookJpaEntity에서 BookJpaEntity를 꺼내 도메인 Book으로 변환 | ||
| List<Book> books = savedBookEntities.stream() | ||
| .map(entity -> bookMapper.toDomainEntity(entity.getBookJpaEntity())) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| return new SavedBooks(books); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
사용자별 저장된 책 조회 로직이 올바릅니다.
사용자 존재 여부 확인, 예외 처리, 그리고 도메인 객체 변환이 적절하게 구현되었습니다.
N+1 쿼리 문제가 발생할 수 있는지 확인해 주세요. SavedBookJpaEntity에서 BookJpaEntity를 가져올 때 지연 로딩으로 인한 성능 이슈가 있을 수 있습니다.
🏁 Script executed:
#!/bin/bash
# 설명: SavedBookJpaEntity와 BookJpaEntity 간의 관계 설정 확인
# 예상 결과: fetch 전략이 EAGER이거나 @EntityGraph 사용 여부 확인
# SavedBookJpaEntity 클래스에서 BookJpaEntity 관계 설정 확인
rg -A 10 -B 5 "class SavedBookJpaEntity"
# BookJpaEntity 관계 어노테이션 확인
rg -A 5 -B 5 "@ManyToOne\|@OneToOne\|@JoinColumn" --type javaLength of output: 1677
🏁 Script executed:
#!/bin/bash
# SavedBookJpaEntity에서 BookJpaEntity 매핑(fetch 전략) 확인
rg -n "@ManyToOne" -C3 src/main/java/konkuk/thip/saved/adapter/out/jpa/SavedBookJpaEntity.javaLength of output: 441
N+1 쿼리 최적화 필요
SavedBookJpaEntity에서 BookJpaEntity 매핑이 모두 fetch = FetchType.LAZY로 설정되어 있어, 아래 로직을 호출할 때 저장된 책 수만큼 추가 쿼리가 발생할 수 있습니다.
- src/main/java/konkuk/thip/saved/adapter/out/jpa/SavedBookJpaEntity.java
‑@ManyToOne(fetch = FetchType.LAZY) private BookJpaEntity bookJpaEntity; - src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java
‑List<SavedBookJpaEntity> findByUserJpaEntity_UserId(Long userId);
성능 이슈 방지를 위해 다음 중 하나를 적용하세요.
- Repository 메소드에
@EntityGraph추가@EntityGraph(attributePaths = "bookJpaEntity") List<SavedBookJpaEntity> findByUserJpaEntity_UserId(Long userId);
- JPQL
fetch join사용@Query("select s from SavedBookJpaEntity s join fetch s.bookJpaEntity where s.userJpaEntity.userId = :userId") List<SavedBookJpaEntity> findByUserJpaEntity_UserId(@Param("userId") Long userId);
- Hibernate 배치 사이즈(batch size) 설정
위 최적화 중 하나를 적용해 N+1 쿼리를 방지해주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java
around lines 37 to 50, the current code triggers N+1 query issues because
BookJpaEntity is lazily loaded for each SavedBookJpaEntity. To fix this, modify
the SavedBookJpaRepository method findByUserJpaEntity_UserId to eagerly fetch
the associated BookJpaEntity by either adding @EntityGraph(attributePaths =
"bookJpaEntity") annotation or rewriting the method with a JPQL fetch join
query. This change will load all related BookJpaEntity instances in a single
query, preventing multiple additional queries and improving performance.
| @SpringBootTest | ||
| @AutoConfigureMockMvc | ||
| @ActiveProfiles("test") | ||
| class BookChangeSavedControllerTest { |
There was a problem hiding this comment.
클래스명과 파일명이 일치하지 않습니다
파일명은 BookIsSavedControllerTest.java이지만 클래스명은 BookChangeSavedControllerTest입니다. 일관성을 위해 둘 중 하나를 수정해야 합니다.
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java
at line 38, the class name BookChangeSavedControllerTest does not match the file
name BookIsSavedControllerTest.java. Rename the class to
BookIsSavedControllerTest to ensure consistency between the class name and the
file name.
…serched # Conflicts: # src/main/java/konkuk/thip/book/application/service/BookSearchService.java
| - name: 🚀 Start Redis | ||
| uses: supercharge/redis-github-action@1.7.0 | ||
| with: | ||
| redis-version: 7 |
There was a problem hiding this comment.
Redis 액션 버전 태그 고정으로 공급망 위험 완화
supercharge/redis-github-action@1.7.0 태그는 mutable tag 입니다.
SHA-pinned 즉 supercharge/redis-github-action@<commit-sha> 형식으로 고정하면, 제3자 공격으로 태그가 다른 커밋을 가리켜도 빌드가 영향을 받지 않습니다.
🤖 Prompt for AI Agents
In .github/workflows/ci-workflow.yml around lines 54 to 57, the Redis GitHub
Action uses a mutable tag '1.7.0' which poses a supply chain risk. Replace the
version tag with a specific commit SHA to pin the action to an immutable
reference, preventing unexpected changes from affecting the build. Find the
commit SHA corresponding to version 1.7.0 and update the 'uses' field to
'supercharge/redis-github-action@<commit-sha>'.
| run: | | ||
| mkdir -p ${{ env.RESOURCE_PATH }} | ||
| mkdir -p ${{ env.TEST_RESOURCE_PATH }} | ||
| echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml | ||
| echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml | ||
|
|
||
| echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml | ||
| echo "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
echo → printf로 치환해 다중 라인 secret 디코딩 신뢰성 향상
echo는 개행·백슬래시 해석 등으로 secret 내용이 변형될 가능성이 있습니다. printf '%s' 를 사용하면 내용을 그대로 전달할 수 있어 안전합니다.
-echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml
-echo "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml
+printf '%s' "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml
+printf '%s' "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml추가로, 디코딩 실패 시 조기에 잡을 수 있도록 set -euo pipefail 선언을 고려해 보세요.
📝 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.
| run: | | |
| mkdir -p ${{ env.RESOURCE_PATH }} | |
| mkdir -p ${{ env.TEST_RESOURCE_PATH }} | |
| echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml | |
| echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml | |
| echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml | |
| echo "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml | |
| run: | | |
| mkdir -p ${{ env.RESOURCE_PATH }} | |
| mkdir -p ${{ env.TEST_RESOURCE_PATH }} | |
| printf '%s' "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml | |
| printf '%s' "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml |
🤖 Prompt for AI Agents
In .github/workflows/ci-workflow.yml around lines 45 to 50, replace the use of
echo with printf '%s' to decode the base64 secrets to prevent modification of
multiline secret content due to echo's interpretation of newlines and
backslashes. Additionally, add set -euo pipefail at the start of the run script
to ensure the workflow fails early on any error, including decoding failures.
buzz0331
left a comment
There was a problem hiding this comment.
복잡한 로직이였을텐데 고생하셨습니다~ 리뷰 몇가지 남겼는데 확인 부탁드릴게요!
음 그리고 BookRankInfo 같은 경우 Redis에 어떻게 보면 영속화시키는 것이기 때문에 book안의 하나의 도메인(예를들어, RankingBook)으로 분류해도 되지 않을까.. 라는 생각이 들기는 한데 다른 분들은 어떻게 생각하시는지 궁금합니다! @seongjunnoh @hd0rable
|
|
||
| public interface BookJpaRepository extends JpaRepository<BookJpaEntity, Long> { | ||
| Optional<BookJpaEntity> findByIsbn(String isbn); | ||
| List<BookJpaEntity> findByIsbnIn(List<String> isbnList); |
오호 네이버 api를 사용할때 따로 dto를 이용하는 것처럼 레디스로 랭킹정보를 받아올때도 이와같이 사용하자는말씀이신건가요?? 전 좋은것같습니닷!! |
엇 제가 말을 조금 헷갈리게 드린 것 같아요 |
아아 넵넵 확인했습니다! 좋은것같습니다 해당방식으로 수정해볼게욥 |
seongjunnoh
left a comment
There was a problem hiding this comment.
고생하셨습니다! 리뷰 확인부탁드립니다!
| import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; | ||
|
|
||
| public interface BookMostSearchUseCase { | ||
| BookMostSearchResult getMostSearchedBooks(Long userId); |
There was a problem hiding this comment.
p2 : userId 메서드는 사용되지 않고 있습니다!
| // 일반적인 key:value의 경우 시리얼라이저 | ||
| redisTemplate.setKeySerializer(new StringRedisSerializer()); | ||
| redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); |
| @Override | ||
| public void saveBookSearchRank(List<String> isbns, List<Double> scores, LocalDate date) { | ||
| String redisKey = makeRedisKey(searchRankPrefix, date); | ||
| for (int i = 0; i < isbns.size(); i++) { | ||
| redisTemplate.opsForZSet().add(redisKey, isbns.get(i), scores.get(i)); | ||
| } | ||
| redisTemplate.expire(redisKey, Duration.ofDays(7)); | ||
| } |
There was a problem hiding this comment.
p3 : 서버 <-> Redis 서버간에 N번의 왕복이 발생하는 것 보다는 Redis 파이프라이닝을 활용하면 성능이 개선될 수 있을 것 같습니다!
예시코드
@Override
public void saveBookSearchRank(List<String> isbns, List<Double> scores, LocalDate date) {
String redisKey = makeRedisKey(searchRankPrefix, date);
// 파이프라인 블록 시작
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
byte[] rawKey = redisTemplate.getStringSerializer().serialize(redisKey);
for (int i = 0; i < isbns.size(); i++) {
byte[] rawValue = redisTemplate.getStringSerializer().serialize(isbns.get(i));
double score = scores.get(i);
// Redis ZADD 명령
connection.zAdd(rawKey, score, rawValue);
}
// TTL 설정도 파이프라인에 포함 가능
connection.expire(rawKey, Duration.ofDays(7).getSeconds());
return null; // 리턴 값은 무시됨
});
// 파이프라인 블록 종료 — 이 시점에 한 번에 Redis로 전송
}There was a problem hiding this comment.
그런데 다시 생각해보니 5개만 저장하는거니 거기서 거기인거 같네요 ㅎ
그냥 이런게 있다~~ 라고 넘어가셔도 될 듯 합니다
음 그런데 redis에 저장하는 데이터를 dto 로 정의하는게 아니라, 따로 도메인 레벨? 로 정의하려고 하시는 이유가 있나요? |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java (1)
3-3: 클린 아키텍처 의존성 규칙 위반애플리케이션 계층이 웹 어댑터 계층의 클래스를 import하는 것은 의존성 규칙 위반입니다.
BookRankInfo를 별도로 정의해주세요.src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (1)
53-63: topN 파라미터 검증을 추가하세요
topN파라미터가 0 이하의 값일 때 예상치 못한 동작이 발생할 수 있습니다. 특히topN - 1이 음수가 되면 RedisreverseRangeWithScores호출에서 문제가 될 수 있습니다.private List<Map.Entry<String, Double>> getTopNFromZSet(String prefix, LocalDate date, int topN) { + if (topN <= 0) { + return Collections.emptyList(); + } String dateStr = date.format(DAILY_KEY_FORMATTER); String redisKey = prefix + dateStr;
🧹 Nitpick comments (2)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (1)
72-72: 메서드 포맷팅 개선 필요메서드 본문이 한 줄에 작성되어 가독성이 떨어집니다.
- public void deleteBookSearchCount(LocalDate date) { deleteZSetKey(searchCountPrefix, date); } + public void deleteBookSearchCount(LocalDate date) { + deleteZSetKey(searchCountPrefix, date); + } - public void deleteBookSearchRankDetail(LocalDate date) { deleteZSetKey(searchRankDetailPrefix, date); } + public void deleteBookSearchRankDetail(LocalDate date) { + deleteZSetKey(searchRankDetailPrefix, date); + }Also applies to: 75-75
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (1)
29-30: DateTimeFormatter를 static final로 선언하세요
DAILY_KEY_FORMATTER는 불변 상수이므로 static final로 선언하는 것이 메모리 효율적입니다.- private final DateTimeFormatter DAILY_KEY_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter DAILY_KEY_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd");
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java(0 hunks)src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java(1 hunks)src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookSavedService.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookSearchService.java(3 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java
- src/main/java/konkuk/thip/book/application/service/BookSearchService.java
- src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java
- src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java
- src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java
🧰 Additional context used
🧠 Learnings (5)
src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (8)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java (1)
47-47: 메서드명 변경이 적절합니다.
findDetailBookByKeyword에서findDetailBookByIsbn으로 변경된 것이 파라미터의 실제 의미(ISBN)를 더 명확하게 표현합니다.src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java (1)
8-8: 인터페이스 메서드명 변경이 적절합니다.메서드명이 실제 기능과 파라미터를 더 명확하게 표현하도록 개선되었습니다.
src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java (1)
22-22: 구현체 메서드명 변경이 인터페이스와 일관되게 적용되었습니다.메서드명이 실제 파라미터와 기능을 더 명확하게 표현하도록 개선되었습니다.
src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java (1)
20-30: 구현이 깔끔합니다!전날의 인기 검색 도서를 조회하는 로직이 명확하게 구현되었습니다. Redis에서 데이터가 없는 경우 빈 리스트가 반환되도록 처리되어 있어 적절합니다.
src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java (1)
9-13: CQRS 패턴에 맞는 적절한 인터페이스 설계입니다.Redis 조회 작업을 위한 메서드들이 명확하게 정의되어 있습니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java (1)
46-52: TTL 설정이 적절합니다.순위 데이터에 7일 TTL을 설정하여 자동 만료되도록 한 것이 좋습니다. 데이터가 5개뿐이므로 파이프라이닝 없이도 성능상 문제없습니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java (2)
66-81: JSON 역직렬화 오류 처리가 적절합니다Redis에서 가져온 JSON 데이터의 null/blank 체크와 Jackson 역직렬화 실패 시 적절한 예외 처리가 구현되어 있습니다.
ExternalApiException을 통한 오류 코드 전달도 일관성 있게 처리되었습니다.
56-62: Redis ZSet 연산과 null 처리가 올바르게 구현되었습니다
reverseRangeWithScores를 사용한 상위 N개 조회와 null 결과에 대한 방어적 처리가 잘 구현되어 있습니다. Stream API를 활용한 데이터 변환도 적절합니다.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java (5)
32-46: DateTimeFormatter를 static final로 변경하고 필드 순서를 개선하세요.
DateTimeFormatter는 인스턴스 상태를 사용하지 않으므로 static final로 선언하는 것이 더 효율적입니다. 또한 가독성을 위해 필드 순서를 개선하는 것을 권장합니다.+ private static final DateTimeFormatter DAILY_KEY_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Value("${app.redis.search-count-prefix}") + private String searchCountPrefix; + + @Value("${app.redis.search-rank-prefix}") + private String searchRankPrefix; + + @Value("${app.redis.search-rank-detail-prefix}") + private String searchRankDetailPrefix; + private final RedisTemplate<String, String> redisTemplate; - private final DateTimeFormatter DAILY_KEY_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMdd"); - - @Value("${app.redis.search-count-prefix}") - private String searchCountPrefix; - - @Value("${app.redis.search-rank-prefix}") - private String searchRankPrefix; - - @Value("${app.redis.search-rank-detail-prefix}") - private String searchRankDetailPrefix; - private final ObjectMapper objectMapper;
93-100: 더 간결한 방식으로 ZSet에 데이터를 추가하세요.현재 구현도 동작하지만, IntStream을 사용하면 더 간결하고 읽기 쉬운 코드가 됩니다.
@Override public void saveBookSearchRank(List<String> isbns, List<Double> scores, LocalDate date) { String redisKey = makeRedisKey(searchRankPrefix, date); - for (int i = 0; i < isbns.size(); i++) { - redisTemplate.opsForZSet().add(redisKey, isbns.get(i), scores.get(i)); - } + IntStream.range(0, isbns.size()) + .forEach(i -> redisTemplate.opsForZSet().add(redisKey, isbns.get(i), scores.get(i))); redisTemplate.expire(redisKey, Duration.ofDays(7)); }추가로 필요한 import:
import java.util.stream.IntStream;
120-120: 일관된 메서드 포맷팅을 유지하세요.다른 메서드들과 일관성을 위해 별도 줄로 분리하세요.
- public void deleteBookSearchCount(LocalDate date) { deleteZSetKey(searchCountPrefix, date); } + public void deleteBookSearchCount(LocalDate date) { + deleteZSetKey(searchCountPrefix, date); + }
125-128: 메서드명을 더 정확하게 변경하세요.
deleteZSetKey는 ZSet뿐만 아니라 String 타입 키도 삭제하므로 메서드명이 부정확합니다.-private void deleteZSetKey(String prefix, LocalDate date) { +private void deleteRedisKey(String prefix, LocalDate date) { String redisKey = makeRedisKey(prefix, date); redisTemplate.delete(redisKey); }그리고 호출하는 곳들도 함께 수정:
- deleteZSetKey(searchRankPrefix, date); + deleteRedisKey(searchRankPrefix, date);
70-85: 순위 데이터를 도메인 모델로 관리하는 것을 고려하세요.PR 코멘트에서 논의된 것처럼, Redis에 7일간 저장되는 순위 데이터는 단순 DTO가 아닌 도메인 모델(예:
RankingBook)로 관리하는 것이 좋습니다. 이는 비즈니스 로직과 데이터 일관성을 더 잘 표현할 수 있습니다.도메인 모델 도입 시 고려사항:
- Book 패키지 내에
RankingBook엔티티 생성- ISBN뿐만 아니라 제목, 이미지 URL 등 추가 정보 포함
- Redis 저장 시 도메인 모델을 사용하여 데이터 일관성 보장
Also applies to: 103-112
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java
🧰 Additional context used
🧠 Learnings (1)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
| @Override | ||
| public void incrementBookSearchCount(String isbn, LocalDate date) { | ||
| String redisKey = makeRedisKey(searchCountPrefix, date); | ||
| redisTemplate.opsForZSet().incrementScore(redisKey, isbn, 1.0); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
검색 카운트 키에도 TTL을 설정하여 데이터 누적을 방지하세요.
스케줄러가 이전 날짜의 카운트를 삭제하지만, 스케줄러 실패 시 키가 무한정 누적될 수 있습니다. 안전성을 위해 TTL을 설정하는 것을 권장합니다.
@Override
public void incrementBookSearchCount(String isbn, LocalDate date) {
String redisKey = makeRedisKey(searchCountPrefix, date);
redisTemplate.opsForZSet().incrementScore(redisKey, isbn, 1.0);
+ // 스케줄러 실패에 대비한 안전장치로 8일 TTL 설정
+ redisTemplate.expire(redisKey, Duration.ofDays(8));
}📝 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.
| @Override | |
| public void incrementBookSearchCount(String isbn, LocalDate date) { | |
| String redisKey = makeRedisKey(searchCountPrefix, date); | |
| redisTemplate.opsForZSet().incrementScore(redisKey, isbn, 1.0); | |
| } | |
| @Override | |
| public void incrementBookSearchCount(String isbn, LocalDate date) { | |
| String redisKey = makeRedisKey(searchCountPrefix, date); | |
| redisTemplate.opsForZSet().incrementScore(redisKey, isbn, 1.0); | |
| // 스케줄러 실패에 대비한 안전장치로 8일 TTL 설정 | |
| redisTemplate.expire(redisKey, Duration.ofDays(8)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java
around lines 87 to 91, the incrementBookSearchCount method sets the search count
in Redis without a TTL, risking indefinite data accumulation if the scheduler
fails. Modify the method to set an appropriate TTL on the Redis key after
incrementing the score to ensure the key expires automatically and prevents
unbounded growth.
| @Override | ||
| public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> bookRankDetails, LocalDate date) { | ||
| String redisKey = makeRedisKey(searchRankDetailPrefix, date); | ||
| String detailJson; | ||
| try { | ||
| detailJson = objectMapper.writeValueAsString(bookRankDetails); | ||
| } catch (JsonProcessingException e) { | ||
| throw new ExternalApiException(JSON_PROCESSING_ERROR); | ||
| } | ||
| redisTemplate.opsForValue().set(redisKey, detailJson); | ||
| } |
There was a problem hiding this comment.
상세 순위 데이터에도 TTL을 설정해야 합니다.
PR 목표에 따르면 순위 데이터는 7일 TTL을 가져야 하는데, 상세 데이터에는 TTL이 설정되지 않아 데이터가 영구적으로 남을 수 있습니다.
@Override
public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> bookRankDetails, LocalDate date) {
String redisKey = makeRedisKey(searchRankDetailPrefix, date);
String detailJson;
try {
detailJson = objectMapper.writeValueAsString(bookRankDetails);
} catch (JsonProcessingException e) {
throw new ExternalApiException(JSON_PROCESSING_ERROR);
}
redisTemplate.opsForValue().set(redisKey, detailJson);
+ redisTemplate.expire(redisKey, Duration.ofDays(7));
}📝 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.
| @Override | |
| public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> bookRankDetails, LocalDate date) { | |
| String redisKey = makeRedisKey(searchRankDetailPrefix, date); | |
| String detailJson; | |
| try { | |
| detailJson = objectMapper.writeValueAsString(bookRankDetails); | |
| } catch (JsonProcessingException e) { | |
| throw new ExternalApiException(JSON_PROCESSING_ERROR); | |
| } | |
| redisTemplate.opsForValue().set(redisKey, detailJson); | |
| } | |
| @Override | |
| public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> bookRankDetails, LocalDate date) { | |
| String redisKey = makeRedisKey(searchRankDetailPrefix, date); | |
| String detailJson; | |
| try { | |
| detailJson = objectMapper.writeValueAsString(bookRankDetails); | |
| } catch (JsonProcessingException e) { | |
| throw new ExternalApiException(JSON_PROCESSING_ERROR); | |
| } | |
| redisTemplate.opsForValue().set(redisKey, detailJson); | |
| redisTemplate.expire(redisKey, Duration.ofDays(7)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java
around lines 102 to 112, the saveBookSearchRankDetail method saves detailed rank
data to Redis without setting a TTL, causing the data to persist indefinitely.
Modify the redisTemplate.opsForValue().set call to include a TTL of 7 days so
that the detailed rank data expires appropriately.
|
|
||
| @Test | ||
| @DisplayName("오늘 랭킹 Top 5를 정상적으로 조회한다") | ||
| @DisplayName("어제 랭킹 Top 5를 정상적으로 조회한다") |
| // given: 어제 날짜의 랭킹 상세정보(JSON)를 Redis에 저장 | ||
| yesterday = LocalDate.now().minusDays(1); |
| @Builder | ||
| public record BookRankInfo( | ||
| int rank, | ||
| String title, | ||
| String imageUrl, | ||
| String isbn | ||
| ) {} |
There was a problem hiding this comment.
엇 전 Result, response 각각의 inner class 를 얘기한 거긴 한데, 완전히 중복되는 코드라 의미없다고 생각하신 거면 지금 상태도 괜찮습니다!!
조회로직시에 result, response 를 어디까지 분리할지를 한번 얘기나눠봐도 좋을 것 같습니다
There was a problem hiding this comment.
앗 넵!! 제가 착각햇나봐요 한번 얘기해보면 좋을것같습니다!
| } | ||
| LocalDate yesterday = LocalDate.now().minusDays(1); | ||
|
|
||
| List<BookMostSearchResult.BookRankInfo> bookRankInfos = bookRedisQueryPort.getYesterdayBookRankInfos(yesterday); |
There was a problem hiding this comment.
redis에 책 상세정보까지 저장하도록 수정하니 service 조회로직이 훨씬 간결해져서 좋은 것 같습니다!



#️⃣ 연관된 이슈
📝 작업 내용
search:count:YYYYMMDD, Value:isbn이 저장되어 score를 1 증가시킵니다.search:rank:YYYYMMDD, Value:score(해당 ISBN의 검색 횟수)이 저장 됩니다.📸 스크린샷
정상적으로 순위가 반환되는 것을 확인할수 있습니다!
💬 리뷰 요구사항
스케줄러로 전날의 랭킹의 정보를 집계하는 것은 클라이언트,사용자가 관여하는것이 아닌 오로직 서버의 로직이라고 생각하여 유즈케이스를 작성하지않았는데 이방식이 맞는걸까욥??
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
/books/most-searchedAPI 엔드포인트가 추가되었습니다.버그 수정
테스트
환경설정